/* ============================================================
   Wed2Gether, marketing site - "Gallery, deepened" (rebranded 2026-09-01)

   Same site, new brand. The layout is the third design, "Gallery White"
   (2026-08-17): photography-first, near-white ground, monumental display
   type, one accent spent only on the primary action. What changed on
   2026-09-01 is the brand it wears: We Do's lilac family and Fraunces are
   out, Wed2Gether's purple, gold and Cormorant Garamond are in.

   This is Direction A of the three on the brand canvas ("A / Gallery,
   deepened"), the option that keeps the gallery and swaps its lilac for
   purple and gold. Its recorded downside is that it can read as the same
   site recoloured; the answers to that are the gold flourishes, the
   midnight-plum bands, the mark in the nav and the full ring lockup in the
   finale, none of which the lilac site had.

   THE BRAND SOURCE OF TRUTH IS `docs/BRAND-WED2GETHER.md` IN THE BRAIN
   REPO, not this file and not `REBRAND-PLAN-2026-08-25.md`, whose colour
   section was eyeballed off a phone screenshot and is wrong. Every hex
   below is Brad's own Coolors export or was sampled from his logo files.

   THE GOLD RULE, WHICH IS THE WHOLE PROBLEM. Gold in the logo reads as
   metal because it is a gradient with highlights. Flat gold on white is
   mustard: `#d9a541` is 2.2:1 on white and unreadable on a phone in
   daylight while looking fine in a mockup on a good monitor.

     --color-accent      #d9a541  hairlines, rules, marks, selection. NEVER text on paper.
     --color-accent-ink  #be8423  3.2:1 on white. Bullets, markers, underlines, icon strokes.
     --night-accent      #d9a541  8.6:1 on plum. Text, freely. The logo's own pairing.

   MIDNIGHT PLUM IS THE DARK GROUND, NOT BRAND PURPLE. `#420572` is a
   mid-tone; a full-bleed field of it is loud and the logo's gold ring loses
   its edge against it. Purple is a foreground colour, plum is a ground.

   Tokens are the brand layer. Raw hex outside this block is still banned.

   CLASS CONTRACTS. The ten generated pages (build.mjs) use .nav, .wordmark,
   .btn, .eyebrow, .display, .lede, .section__inner, .footer, .nav__keep and
   .nav__login from this file. Those names must keep working whatever the
   design is.

   Photography sits in a white mat with a hairline and a soft lift: see the
   .frame component (Brad, 2026-08-17: round the edges and give aesthetic
   frames to photos). Hairlines-not-shadows holds everywhere else.
   ============================================================ */

/* Cormorant Garamond replaces Fraunces, which retired with the old brand: it
   was picked for a lilac serif wordmark, and the new wordmark is wide-tracked
   geometric caps beside metallic gold. Cormorant runs SMALL and light for its
   px size, so every display step below is larger and tighter than the Fraunces
   original; do not "restore" the old numbers. Self-hosted, same as DM Sans:
   the site makes no external requests and the CSP has no font-src for one. */
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('assets/fonts/cormorant-garamond-latin-400-normal.woff2') format('woff2');
}
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('assets/fonts/cormorant-garamond-latin-400-italic.woff2') format('woff2');
}
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('assets/fonts/cormorant-garamond-latin-500-normal.woff2') format('woff2');
}
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('assets/fonts/cormorant-garamond-latin-600-normal.woff2') format('woff2');
}
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('assets/fonts/dm-sans-latin-400-normal.woff2') format('woff2');
}
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('assets/fonts/dm-sans-latin-500-normal.woff2') format('woff2');
}
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('assets/fonts/dm-sans-latin-700-normal.woff2') format('woff2');
}

:root {
  /* Brand. Contrast ratios are computed, not estimated. */
  --color-primary: #420572;            /* 14.0:1 on white */
  --color-primary-foreground: #fdfefe;
  --color-primary-lift: #683f94;       /* 7.7:1 on white. Hover, emphasis on tint */
  --color-accent: #d9a541;             /* gold. 2.2:1 on white: marks and rules ONLY */
  --color-accent-ink: #be8423;         /* deep gold. 3.2:1 on white: bullets, underlines */
  --color-accent-light: #efc96a;       /* gradient highlight only, never a flat fill */
  --color-accent-foreground: #2a1636;  /* 7.4:1 on gold, for ::selection */
  --color-background: #faf7fc;
  --color-surface: #ffffff;
  --color-border: #e6dcef;
  --color-foreground: #2a1636;         /* 16.6:1 on white */
  --color-muted: #655a75;              /* 6.4 white / 6.0 paper / 4.9 band */
  --color-soft: #f4eef9;
  /* SECTION GROUND, not a brand token. --color-background is the 3% tint and at
     section scale it reads as a printing fault beside white, which is why the
     bands ran together (Brad 2026-08-24: "more contrast between sections"). A
     deeper step of the same purple, still inside the palette family.
     --color-muted clears 4.5:1 on it (4.92). Do not darken it further without
     re-checking that. */
  --color-band: #e9ddf3;

  /* Night set - MIDNIGHT PLUM, the ground the dark lockup is drawn on. Sampled
     off the pixels of Logo_Dark.png, because the five-colour palette export
     does not name it. Used by the Feed band and the finale. */
  --night-bg: #1a0426;
  --night-surface: #2a0a3a;
  --night-border: #3d1a50;
  --night-foreground: #fdfefe;         /* 19.1:1 on plum */
  --night-muted: #cfc4da;              /* 11.6:1 on plum. Matches getwed2gether.com */
  --night-accent: #d9a541;             /* 8.6:1 on plum. Gold IS a text colour here */

  --radius: 1rem;
  /* Photo framing. Two tiers, and the nested one is ALWAYS smaller than its
     parent or the inner corner visibly fights the outer (the app's own rule in
     tokens.css). --radius-frame is the mat, --radius-photo the image inside it. */
  --radius-frame: 1.25rem;
  --radius-photo: 0.75rem;
  --shadow-frame: 0 18px 40px -22px rgba(42, 22, 54, 0.28);
  --font-sans: 'DM Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-display: 'Cormorant Garamond', Georgia, serif;
  --shadow-exhibit: 0 30px 60px -28px rgba(42, 22, 54, 0.3);

  /* Rhythm: gallery pacing is slower than the old site's */
  --section-y: clamp(5rem, 11vw, 11rem);
  --content-w: 78rem;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);

  /* Grain: inline SVG turbulence, ~4% - photography and finale only */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E");
}

/* ---------- Reset & base ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
[id] { scroll-margin-top: 4.5rem; }
body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: 1.0625rem;
  line-height: 1.6;
  color: var(--color-foreground);
  /* Surface white, not the app's lilac-tinted background: the gallery ground
     is paper, and the tint survives only inside the device exhibits. */
  background: var(--color-surface);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
h1, h2, h3, p, ul, ol, figure { margin: 0; }
ul, ol { padding: 0; list-style: none; }
a { color: inherit; }
img { max-width: 100%; display: block; }
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
.icon { width: 1.25rem; height: 1.25rem; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; flex: none; }
.icon--sm { width: 1rem; height: 1rem; }

:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 3px;
  border-radius: 4px;
}
.finale :focus-visible { outline-color: var(--night-accent); }

::selection { background: var(--color-accent); color: var(--color-accent-foreground); }

/* ---------- Type system ---------- */
/* ⚠️ AN EYEBROW IS A LABEL AND MUST HOLD ONE LINE (Brad 2026-08-24). The two
   long ones run 40 characters, and at 0.75rem/0.32em that is 2 lines under
   about 30rem of viewport, which reads as a broken heading rather than as a
   label. Size and tracking shrink with the viewport instead; nowrap makes a
   regression visible as overflow rather than as a silent second line. */
.eyebrow {
  /* ⚠️ THE TRACKING CANNOT BE A vw CLAMP. clamp(0.14em, 0.8vw, 0.32em) picks
     the vw term at narrow widths because 0.8vw is still bigger than 0.14em of
     a 9px font, so the letter-spacing REFUSED TO SHRINK and the 41-character
     eyebrow ran 332px wide at 320px. Worse, nowrap turns that into overflow
     rather than a wrap, and a centred parent bleeds it off BOTH edges: the
     hero's whole .section__inner measured 372px inside a 320px viewport and
     every child under it was reported as overflowing. Tracking in em (so it
     follows the font) plus one breakpoint is the fix. */
  font-size: clamp(0.5rem, 2.4vw, 0.75rem);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.32em;
  color: var(--color-muted);
  white-space: nowrap;
}
@media (max-width: 40rem) { .eyebrow { letter-spacing: 0.22em; } }
.display {
  font-family: var(--font-display);
  font-weight: 400;
  /* Sized for CORMORANT, not for Fraunces. Cormorant has a much smaller
     x-height, so the Fraunces numbers (2.4 to 4.25rem) set visibly smaller in
     the same box. Every display step on the site went up about 15% on
     2026-09-01 and the leading came in; restoring the old numbers makes the
     headlines look timid rather than making them "correct". */
  font-size: clamp(2.75rem, 6vw, 5rem);
  line-height: 1.02;
  letter-spacing: -0.018em;
  text-wrap: balance;
}
.display em { font-style: italic; color: var(--color-primary); }
.display--italic { font-style: italic; }
.lede {
  max-width: 36rem;
  color: var(--color-muted);
  font-size: 1.125rem;
}
.section__inner {
  max-width: var(--content-w);
  margin-inline: auto;
  padding-inline: clamp(1.25rem, 4vw, 3rem);
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: 3rem;
  padding: 0 1.75rem;
  border-radius: 999px;
  font-weight: 600;
  font-size: 1rem;
  text-decoration: none;
  white-space: nowrap;
  transition: transform 0.2s var(--ease-out), box-shadow 0.2s var(--ease-out), background-color 0.2s var(--ease-out), color 0.2s var(--ease-out);
}
.btn--primary {
  background: var(--color-primary);
  color: var(--color-primary-foreground);
}
.btn--primary:hover { transform: translateY(-2px); box-shadow: 0 14px 32px rgba(66, 5, 114, 0.35); }
.btn--primary:active { transform: translateY(0); }
/* Gallery secondary: ink hairline, not purple. Purple is the one action. */
.btn--outline {
  background: transparent;
  color: var(--color-foreground);
  box-shadow: inset 0 0 0 1px var(--color-foreground);
}
.btn--outline:hover { background: var(--color-foreground); color: var(--color-surface); transform: translateY(-2px); }
.btn--outline:active { transform: translateY(0); }
.finale .btn--outline { color: var(--night-foreground); box-shadow: inset 0 0 0 1px var(--night-foreground); }
.btn--ghost { color: var(--color-foreground); padding-inline: 1.25rem; }
.btn--big { min-height: 3.5rem; padding-inline: 2.25rem; font-size: 1.0625rem; }
.btn--nav { min-height: 2.5rem; padding-inline: 1.25rem; font-size: 0.9375rem; box-shadow: none; }

/* ---------- Nav ---------- */
.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  background: color-mix(in srgb, var(--color-surface) 86%, transparent);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid transparent;
  transition: border-color 0.3s;
}
.nav.is-scrolled { border-bottom-color: var(--color-border); }
/* Over the finale the glass goes dark */
.nav.is-night {
  background: color-mix(in srgb, var(--night-bg) 76%, transparent);
  color: var(--night-foreground);
}
.nav.is-night.is-scrolled { border-bottom-color: var(--night-border); }
.nav.is-night .nav__links a { color: var(--night-muted); }
.nav.is-night .nav__links a:hover { color: var(--night-foreground); }
.nav.is-night .nav__links a::after { background: var(--night-accent); }
.nav__inner {
  max-width: var(--content-w);
  margin-inline: auto;
  padding: 0.875rem clamp(1.25rem, 4vw, 3rem);
  display: flex;
  align-items: center;
  gap: 2rem;
}
/* ---------- Wordmark ----------
   ⚠️ THE WED2GETHER LOCKUP IS SQUARE (560x560), THE WE DO ONE WAS 2.9:1. A
   round lockup set to the old `height: 2.5rem` is a 40px circle carrying a
   wordmark, a tagline and a script line: it is unreadable, and it is the
   obvious thing to do when swapping the file. The nav therefore carries the
   MARK (the gold heart, cropped out of the light lockup and approved for this
   use on 2026-08-29) beside the name set as live text. The full ring lockup
   gets one proper appearance, at 14 to 20rem, in the finale.

   ⚠️ THE "2" IS DEEP GOLD `#be8423`, NOT `#d9a541`. Light gold on white is
   2.2:1. This is a logotype, which WCAG exempts, but it still has to be
   legible on a phone in daylight.

   ⚠️ NEVER LET THIS WRAP OR SHRINK (Brad's rule, and it holds at every
   width). `white-space: nowrap` alone is not enough: it lets the item be
   crushed narrow and clipped inside its own box without ever changing
   document scrollWidth, so an overflow sweep passes while the bar is visibly
   broken. `flex-shrink: 0` is the other half. */
.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  text-decoration: none;
  white-space: nowrap;
  flex-shrink: 0;
}
.wordmark__mark { display: block; height: 1.9rem; width: auto; flex: none; }
.wordmark__text {
  font-family: var(--font-sans);
  font-weight: 700;
  font-size: 1.0625rem;
  letter-spacing: 0.075em;
  line-height: 1;
  color: var(--color-foreground);
  white-space: nowrap;
}
.wordmark__text i { font-style: normal; color: var(--color-accent-ink); }
.nav.is-night .wordmark__text { color: var(--night-foreground); }
.nav.is-night .wordmark__text i { color: var(--night-accent); }
.wordmark--sm .wordmark__mark { height: 1.5rem; }
.wordmark--sm .wordmark__text { font-size: 0.9375rem; }
.nav__links {
  display: flex;
  gap: 1.75rem;
  margin-left: auto;
  font-size: 0.9375rem;
  font-weight: 500;
}
.nav__links a { text-decoration: none; color: var(--color-muted); position: relative; white-space: nowrap; }
.nav__links a::after {
  content: '';
  position: absolute;
  left: 0;
  right: 100%;
  bottom: -4px;
  height: 1px;
  background: var(--color-foreground);
  transition: right 0.25s var(--ease-out);
}
.nav__links a:hover { color: var(--color-foreground); }
.nav__links a:hover::after { right: 0; }

/* ---------- Hero: the overture ----------
   Its own room, deliberately unlike the paper below: a greyscale photograph
   ghosted under a white scrim that dissolves into the page, centered type at
   a calmer scale, and a 3D recede on scroll (transform set by main.js on
   [data-hero-stage]; the background drifts slower for depth). */
.hero {
  position: relative;
  overflow: hidden;
  min-height: min(92svh, 58rem);
  display: flex;
  align-items: center;
  justify-content: center;
  /* The 3D part: children transformed in Z need a camera */
  perspective: 60rem;
}
.hero__bg { position: absolute; inset: 0; }
.hero__bg img {
  width: 100%;
  height: 112%; /* headroom so the parallax drift never shows an edge */
  object-fit: cover;
  filter: grayscale(1) brightness(1.5) contrast(0.72);
  opacity: 0.34;
  will-change: transform;
}
/* The scrim: light veil overall, dissolving to solid paper at the foot so the
   hero hands over to the white page rather than ending at a line */
.hero__bg::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(46rem 30rem at 50% 42%, color-mix(in srgb, var(--color-surface) 55%, transparent), transparent 72%),
    linear-gradient(
      color-mix(in srgb, var(--color-surface) 62%, transparent) 0%,
      color-mix(in srgb, var(--color-surface) 30%, transparent) 45%,
      var(--color-surface) 96%
    );
}
.hero__fade { display: none; } /* the scrim's bottom stop does this job */
.hero__stage {
  position: relative;
  z-index: 1;
  text-align: center;
  padding-block: clamp(5rem, 12vh, 8rem);
  transform-style: preserve-3d;
  will-change: transform, opacity;
}
.hero__eyebrow { margin-bottom: 1.25rem; }
.hero__title {
  font-family: var(--font-display);
  font-weight: 400;
  /* Two lines now, not one word: the hero line is the brand's own tagline
     rather than the old product name, so it needs a size that survives
     "Everyone. Everything." on one line at desktop and wraps to three lines
     on a phone without touching the edges. */
  font-size: clamp(2.9rem, 8.6vw, 7.25rem);
  line-height: 0.98;
  letter-spacing: -0.022em;
  text-wrap: balance;
}
.hero__sub {
  margin: 1.5rem auto 0;
  max-width: 34rem;
  font-size: clamp(1.0625rem, 1.5vw, 1.25rem);
  color: var(--color-muted);
  text-wrap: pretty;
}
.hero__actions {
  margin-top: 2rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1rem;
  align-items: center;
  justify-content: center;
}
.hero__note {
  margin-top: 1rem;
  font-size: 0.8125rem;
  color: var(--color-muted);
}

/* Line-mask reveal (hero, once on load) */
/* ⚠️ THE PADDING IS WHAT STOPS "Together." LOSING THE TAIL OF ITS g.
   `overflow: hidden` is what makes the slide-up read as a line rising into
   place, and it clips whatever sits outside the box. The hero sets
   `line-height: 0.98`, which is tighter than Cormorant's descender needs, so
   the box ended below the baseline and the g, y and p of an italic display
   line were cut clean off.

   Padding grows the clipping box; the negative margin gives the space straight
   back, so nothing below moves. Do not "simplify" this to one property.

   The translate went 115% -> 130% in the same change. The span has to start
   fully below the box, and the box is now 0.16em taller than the span, so 115%
   left the top of the letters peeking on the first frame. */
.reveal-line {
  display: block;
  overflow: hidden;
  padding-bottom: 0.16em;
  margin-bottom: -0.16em;
}
.reveal-line > span { display: inline-block; }
.js .reveal-line > span {
  transform: translateY(130%);
  animation: line-up 0.9s var(--ease-out) both;
  animation-delay: var(--d, 0s);
}
.reveal-line:nth-of-type(1) > span { --d: 0.05s; }
.hero__title.reveal-line > span { --d: 0.13s; }
@keyframes line-up { to { transform: translateY(0); } }

/* ---------- .frame: a photograph with faded edges ----------
   Brad, 2026-09-08: "get rid of the white border around it, do something more
   aesthetic, maybe a purple undershadow or something", then "make it larger and
   add some transparency gradient around the edges". Picked from four framings
   and then four settings of the winner. This is the setting he picked, which
   was labelled A4 in the preview: bigger, edges faded, purple shadow, and on a
   wide screen the photograph runs off the right edge of the page.

   ⚠️ THIS SUPERSEDES THE WHITE MAT (2026-08-17), and the mat was itself Brad's
   instruction, so do not "restore" it as a regression. The mat's job was to
   separate the photograph from the tinted band; the fade and the purple shadow
   do that job now.

   ⚠️ IT IS SAFE TO CHANGE .frame ITSELF rather than fork a modifier, because
   the planning photograph is the ONLY caller left on the whole site and
   .frame--center has no callers at all. Check that before assuming it still
   holds: `grep -rn 'class="frame' index.html build.mjs`.

   ⚠️ THE SHADOW IS drop-shadow, NOT box-shadow, AND THAT IS FORCED. box-shadow
   is drawn from the element's BOX, so behind a faded edge you get a crisp
   rectangle of purple showing through the fade and the two treatments fight.
   filter: drop-shadow() is computed from the alpha channel, so it follows the
   fade. It sits on the figure while the mask sits on the image, which is what
   lets the figure's shadow see the masked silhouette.

   ⚠️ AND THE FADE IS WHY .wipe IS GONE FROM THIS IMAGE. .wipe animates
   clip-path, and clip-path plus mask on one element draws a hard clipped edge
   inside a soft one for the length of the animation. The section's own .reveal
   fades it in instead.

   Do NOT add an aspect-ratio to .frame__img. The photographs are 3:2 and
   forcing them into a squarer box is the exact defect the mat replaced. Where a
   crop IS wanted, set max-height + object-fit on a scoped modifier instead
   (see .plan__frame), never on the shared class. */
.frame {
  background: none;
  padding: 0;
  border: 0;
  /* Two shadows. The wide soft one is the lift off the page; the tight dark one
     under the bottom edge is the contact shadow, and it is what stops a big
     soft blur reading as fog. */
  filter:
    drop-shadow(0 22px 34px rgba(66, 5, 114, 0.5))
    drop-shadow(0 5px 10px rgba(26, 4, 38, 0.3));
}
.frame__img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 0;
  /* A single mask cannot fade four edges, so two linear gradients are
     intersected: one fades left and right, the other top and bottom, and the
     image shows only where BOTH are opaque.

     mask-composite is the modern keyword and -webkit-mask-composite: source-in
     is the same thing for older WebKit. Both are set: a browser that
     understands neither would honour only the last gradient and fade two edges
     out of four. */
  --f: 16px;
  -webkit-mask-image:
    linear-gradient(to right, transparent 0, #000 var(--f), #000 calc(100% - var(--f)), transparent 100%),
    linear-gradient(to bottom, transparent 0, #000 var(--f), #000 calc(100% - var(--f)), transparent 100%);
  mask-image:
    linear-gradient(to right, transparent 0, #000 var(--f), #000 calc(100% - var(--f)), transparent 100%),
    linear-gradient(to bottom, transparent 0, #000 var(--f), #000 calc(100% - var(--f)), transparent 100%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
.frame--center { margin: 0 auto 2.75rem; max-width: 30rem; }

/* ---------- Sections ----------
   ⚠️ THE GROUNDS ALTERNATE AND THE ORDER IS LOAD-BEARING. Reordering the
   sections in index.html without re-checking this leaves two neighbours on the
   same ground, which is the exact complaint this answers (Brad 2026-08-24:
   "more contrast between sections"). Top to bottom:

     hero        paper, photograph behind
     the-feed    NIGHT      .feedsec--night
     guests      paper      .gallery-sec
     planning    band       .gallery-sec--tint
     plans       paper      .plans-band
     trust       band       .trust
     finale      NIGHT      .finale

   Every seam is also a hairline, so the two paper-to-band changes still read
   as a change on a screen that is washing the tint out. */
.gallery-sec { padding-block: var(--section-y); }
.gallery-sec--tight { padding-block: clamp(3.5rem, 8vw, 7rem); }
.gallery-sec { border-top: 1px solid var(--color-border); }
.gallery-sec--tint { background: var(--color-band); }
.sec-head { max-width: 46rem; }
.sec-head .eyebrow { display: block; margin-bottom: 1.1rem; }
.sec-head .display { margin-bottom: 1.25rem; }

/* ---------- Scan: copy + the phone exhibit ---------- */
.scan__grid {
  display: grid;
  grid-template-columns: minmax(0, 6fr) minmax(0, 5fr);
  gap: clamp(2.5rem, 7vw, 7rem);
  align-items: center;
}
.scan__steps {
  margin-top: 2.25rem;
  counter-reset: step;
  display: grid;
  gap: 0;
  max-width: 34rem;
}
.scan__steps li {
  counter-increment: step;
  position: relative;
  padding: 1.1rem 0 1.1rem 3.25rem;
  color: var(--color-muted);
  border-top: 1px solid var(--color-border);
}
.scan__steps li:last-child { border-bottom: 1px solid var(--color-border); }
.scan__steps b { color: var(--color-foreground); font-weight: 700; display: block; }
.scan__steps li::before {
  content: counter(step, decimal-leading-zero);
  position: absolute;
  left: 0;
  top: 1.35rem;
  font-family: var(--font-display);
  font-style: italic;
  font-size: 1.25rem;
  /* Purple, NOT deep gold, and the brand canvas has it in gold. #be8423 is
     3.2:1 on white, which the brand doc allows for display over 24px and for
     icon strokes; a 20px numeral is neither. Purple is 14:1 and these
     numerals carry the order of the steps. */
  color: var(--color-primary);
}

/* The phone exhibit: upright, shadowed, with the table card leaning on it */
.exhibit {
  position: relative;
  justify-self: center;
  padding: 0 0 3.25rem 2.5rem;
}
.exhibit .tablecard {
  pointer-events: none; /* decorative (aria-hidden): must never eat a click */
  position: absolute;
  left: 0;
  /* ⚠️ RAISED CLEAR OF THE TAB BAR, AND THE Z-INDEX IS THE OTHER HALF OF IT.
     The phone grew a real tab bar when the mocks were traced from the app on
     2026-08-24, and the card ran straight through it. Two separate faults:
     the card's bottom sat 56px BELOW the bar's top, and `.tabbar` carries
     `z-index: 4` while the card had `auto`, so the bar painted over the card
     and clipped "Scan to join the celebration" to "Sc". Raising it fixes the
     overlap; the z-index makes sure that stays fixed if either box moves. */
  bottom: 7.5rem;
  z-index: 5;
  /* Trimmed from 13.5rem when the card was raised. Clearing the tab bar moved
     it up 4rem, and at the old size it took the bottom 40% of the screen with
     it: the Vendors row, the reaction pills and the trivia standings all went
     behind the prop. The card only has to read as a printed card. */
  width: 11.75rem;
  padding: 1.1rem 1.2rem;
  transform: rotate(-3deg);
}
.phone {
  width: min(19rem, 74vw);
  aspect-ratio: 9 / 19;
  background: var(--color-foreground);
  border-radius: 2.9rem;
  padding: 0.55rem;
  box-shadow: var(--shadow-exhibit);
  position: relative;
}
.phone::after { /* dynamic island */
  content: '';
  position: absolute;
  top: 1.15rem;
  left: 50%;
  transform: translateX(-50%);
  width: 5.5rem;
  height: 1.05rem;
  border-radius: 999px;
  background: var(--color-foreground);
  z-index: 3;
}
.phone__screen {
  position: relative;
  height: 100%;
  /* ⚠️ THE MOCK CARRIES ITS OWN TEXT COLOUR AND MUST. Inside .feedsec--night
     the section sets `color: var(--night-foreground)`, which every one of
     these white cards inherited: the labels came out near-white on white and
     "A note", the captions and the song titles were invisible on the page
     while looking perfect in the markup. The app has no dark theme, so the
     phone is always on the light palette wherever the phone is placed. */
  color: var(--color-foreground);
  border-radius: 2.4rem;
  overflow: hidden;
  background: linear-gradient(170deg, var(--color-soft), var(--color-background) 45%);
}

/* The phone's screen frame. `.app` is the column every guest screen fills;
   the header, the body and the tab bar are the three bands inside it. */
.app {
  /* ⚠️ 2.15rem CLEARS THE DYNAMIC ISLAND WITH ABOUT 8px TO SPARE, AND IT IS THE
     FLOOR, NOT A PREFERENCE. `.phone::after` starts 1.15rem from the phone's
     edge and runs 1.05rem, and `.phone__screen` begins 0.55rem in, so the
     island ends 1.65rem down. Trim below that and the first line of every
     screen slides under the notch. */
  padding: 2.15rem 0.7rem 0;
  height: 100%;
  display: flex;
  flex-direction: column;
}
.flourish {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  margin-top: 0.55rem;
}
.flourish i { width: 2.2rem; height: 1px; background: var(--color-accent); }
.flourish b { width: 0.32rem; height: 0.32rem; background: var(--color-accent); transform: rotate(45deg); }

/* The printed table card */
.tablecard {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 0.75rem;
  box-shadow: var(--shadow-exhibit);
  padding: 1.4rem 1.5rem;
  text-align: center;
}
/* The mark on the printed table card. The brand canvas puts it above the
   names, and it is the one thing that makes the prop read as this brand's
   card rather than any wedding QR card. */
.tablecard__mark { height: 1.75rem; width: auto; margin: 0 auto 0.55rem; }
.tablecard__eyebrow { font-size: 0.5rem; text-transform: uppercase; letter-spacing: 0.28em; color: var(--color-muted); }
.tablecard__names { font-family: var(--font-display); font-style: italic; font-size: 1.45rem; margin-top: 0.3rem; }
.tablecard__names em { color: var(--color-primary); }
.tablecard .flourish { margin-block: 0.55rem 0.9rem; }
.qr { width: 5.75rem; height: 5.75rem; margin-inline: auto; color: var(--color-foreground); }
.tablecard__hint { font-size: 0.7rem; color: var(--color-muted); margin-top: 0.8rem; }

/* ---------- Planning: editorial rows + the desktop exhibit ---------- */
.plan__head {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: clamp(2rem, 5vw, 5rem);
  align-items: end;
}
.plan__head .display { margin-block: 1.1rem 1.25rem; }
/* The planning photo is the one framed image that IS cropped: it sits beside a
   headline in a two-column grid and its natural 3:2 would tower over the copy.
   The crop lives here, on the modifier, not on the shared .frame__img. */
.plan__frame .frame__img { max-height: 20rem; object-fit: cover; }

/* ---------- The planning photograph bleeds off the right edge ----------
   Only above the 62rem breakpoint, where the row is still two columns. Below it
   the section is one centred column and there is no edge to run off, so the
   photograph stays a faded block with its purple shadow.

   ⚠️ EVERY RULE HERE IS INSIDE A min-width QUERY ON PURPOSE. An unqualified
   `.plan__head { grid-template-columns: ... }` beats the max-width: 62rem rule
   below on specificity, so the section would never collapse on a phone: the
   photograph came out 159px wide instead of 350 and it looked deliberate.

   ⚠️ TWO THINGS TO ESCAPE, NOT ONE. The section's PADDING (the gutter) and,
   above 78rem, the margin that CENTRES .section__inner in the viewport.
   Cancelling only the gutter leaves a 144px band of tint to the right of the
   photograph at 1440, which reads as a bug rather than a bleed.

   ⚠️ AND THE CLIP GOES ON THE SECTION, NOT THE ROW. `overflow: hidden` on
   .plan__head clips at the row's own edge, which is exactly the edge the
   photograph is trying to escape: the bleed silently does nothing while the
   layout box still measures as though it worked. `overflow-x: clip` on the
   section clips at the viewport edge and, unlike hidden, does not make the
   element a scroll container. Prove it by sampling the pixel at the viewport's
   right edge, not by reading getBoundingClientRect. */
@media (min-width: 62.0625rem) {
  /* 6/6 rather than 7/5: the picture gets wider and the headline keeps two
     lines. Measured at 1440 and 1100. 5/7 pushes it to three. */
  .plan__head { grid-template-columns: minmax(0, 6fr) minmax(0, 6fr); }
  .gallery-sec--tint { overflow-x: clip; }
  .plan__frame {
    margin-right: calc(
      -1 * (clamp(1.25rem, 4vw, 3rem) + max(0px, (100vw - var(--content-w)) / 2))
    );
  }
  .plan__frame .frame__img { max-height: 28rem; }
  /* One edge is faded, the inner one. The other three are the page edge or are
     past it, so the four-edge mask above is replaced by a single gradient. */
  .plan__frame .frame__img {
    --f: 90px;
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--f));
    mask-image: linear-gradient(to right, transparent 0, #000 var(--f));
    -webkit-mask-composite: source-over;
    mask-composite: add;
  }
  /* The shadow leans left, away from the edge it has escaped. */
  .plan__frame .frame {
    filter: drop-shadow(-18px 22px 40px rgba(66, 5, 114, 0.42));
  }
}

.deskwrap { margin: clamp(2.5rem, 6vw, 4.5rem) 0 0; }
.browser {
  border-radius: var(--radius-frame);
  overflow: hidden;
  background: var(--color-surface);
  box-shadow: var(--shadow-exhibit), 0 0 0 1px var(--color-border);
}
.browser__bar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 0.85rem;
  background: color-mix(in srgb, var(--color-soft) 60%, var(--color-surface));
  border-bottom: 1px solid var(--color-border);
}
.browser__dots { display: flex; gap: 0.35rem; }
.browser__dots i { width: 0.6rem; height: 0.6rem; border-radius: 50%; background: var(--color-border); }
/* No fake URL. An address on a mockup is a thing to keep true. */
.browser__title {
  font-size: 0.75rem;
  color: var(--color-muted);
  background: var(--color-surface);
  border-radius: 99px;
  padding: 0.2rem 0.9rem;
  border: 1px solid var(--color-border);
}
/* ⚠️ THREE COLUMNS, NOT TWO. The couple portal navigates in two stages: a
   narrow rail of the four groups, then a column showing where you are inside
   the one you picked. A single sidebar of page links is the nav the app had
   before 2026-08-24. */
/* One screenshot, not a three-column drawing. The hand-built mock this
   replaced imitated a nav the app stopped having, and nothing failed when it
   went stale. `aspect-ratio` holds the space before the image decodes so the
   section does not jump. */
.browser__body { background: var(--color-background); }
.browser__shot { display: block; width: 100%; height: auto; aspect-ratio: 16 / 10; }

/* The gradient banner: names and date left, countdown right */

/* the "2 open" / "5 of 8 done" pill on the right of a card heading */

/* ⚠️ NO COUNT BADGES. The app tried them on these tiles and took them off
   again ("No count badges on the Overview quick-row tiles"). */

.vignette__bar {
  height: 5px;
  border-radius: 999px;
  background: var(--color-border);
  overflow: hidden;
}
.vignette__bar i { display: block; height: 100%; width: var(--w, 50%); background: var(--color-primary); border-radius: inherit; }

.deskwrap__cap {
  /* ⚠️ 2rem CLEARS THE EXHIBIT'S DROP SHADOW, AND THAT IS A CONTRAST FIX, NOT
     spacing taste. --shadow-exhibit falls 30px below the browser mock, and at
     1rem this caption sat in the darkest part of it: muted measured 4.19:1
     against the shadowed band instead of 4.92 against the band itself. */
  margin-top: 2rem;
  font-size: 0.8125rem;
  color: var(--color-muted);
  text-align: center;
}

/* The planning list: an index, not cards */
.plan__index {
  margin-top: clamp(2.5rem, 6vw, 4rem);
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0 clamp(1.5rem, 4vw, 3.5rem);
}
.plan__index li {
  border-top: 1px solid var(--color-border);
  padding: 1.1rem 0 1.4rem;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.15rem 0.75rem;
  align-items: start;
}
.plan__index .icon { color: var(--color-primary); grid-row: span 2; margin-top: 0.2rem; }
.plan__index b { font-weight: 700; }
.plan__index span { color: var(--color-muted); font-size: 0.9375rem; grid-column: 2; }

/* ---------- Plans band ---------- */
.plans-band { padding-block: var(--section-y); border-top: 1px solid var(--color-border); }
.plans-band__head { max-width: 54rem; } /* 46rem wrapped the headline after "For" */
.plans-band__head .display { margin-block: 1.1rem 1.25rem; }
.plans-band .plans { grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 1.25rem; margin-top: 2.5rem; }
.plans-band__more {
  margin-top: 2rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 1.25rem;
}
.plans-band__more span { color: var(--color-muted); font-size: 0.9375rem; }

/* ---------- Trust: the vow ----------
   ⚠️ MATCHED BACK TO PRODUCTION on 2026-08-24 (Brad: make the quiet promise
   look more like the original one on wedoevents.app). It had grown a matted
   .frame around the photograph and a hand-drawn underline sweeping under "not
   our inventory", and between them a vow had turned into art direction. One
   centred photograph, one plain italic line. The .uline component went with
   it; it had no other caller. */
.trust { padding-block: var(--section-y); background: var(--color-band); border-top: 1px solid var(--color-border); }
.trust__inner { text-align: center; max-width: 46rem; }
.trust__photo {
  max-width: 32rem;
  width: 100%;
  /* ⚠️ Same trap as .fwpost__img: the height attribute is a presentational
     hint, so without this the photograph laid out 1067px tall inside a 512px
     box and the vow was pushed off the fold. */
  height: auto;
  margin: 0 auto 2.5rem;
  border-radius: var(--radius-photo);
  box-shadow: var(--shadow-frame);
}
.trust .display { margin-block: 1.1rem 1.5rem; }
.trust__lines { display: grid; gap: 0.7rem; color: var(--color-muted); font-size: 1.0625rem; }
.trust__nle { margin-top: 2.25rem; font-size: 0.9375rem; color: var(--color-muted); }

/* ---------- Finale: the reception at dusk ---------- */
.finale {
  position: relative;
  padding-block: calc(var(--section-y) * 1.15);
  overflow: hidden;
  color: var(--night-foreground);
}
.finale__bg { position: absolute; inset: 0; pointer-events: none; }
.finale__bg img { width: 100%; height: 100%; object-fit: cover; }
.finale__bg::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  /* ⚠️ THE SCRIM IS A CONTRAST CONTROL, NOT A MOOD SETTING. Text sits on a
     photograph here, so the number that matters is the ratio at the BRIGHTEST
     patch of the picture, not the average. At the old 55/55/72 the closing
     note measured 4.10:1 against a bright bulb; 66/62/80 puts it over 4.5
     while still reading as a lit room rather than a flat panel. Re-check by
     sampling pixels, not by looking: the eye forgives a bright patch that a
     phone in daylight does not. */
  background:
    radial-gradient(46rem 26rem at 50% 55%, color-mix(in srgb, var(--night-bg) 66%, transparent), transparent 75%),
    linear-gradient(color-mix(in srgb, var(--night-bg) 62%, transparent), color-mix(in srgb, var(--night-bg) 80%, transparent));
}
.finale__bg::after { content: ''; position: absolute; inset: 0; z-index: 1; background-image: var(--grain); }
.finale__inner { position: relative; z-index: 2; text-align: center; }
.finale__lockup {
  display: block;
  width: clamp(14rem, 28vw, 20rem);
  height: auto;
  margin-inline: auto;
  filter: drop-shadow(0 0 28px color-mix(in srgb, var(--night-accent) 45%, transparent));
}
.finale .display em { color: var(--night-accent); }
.finale .display { margin-block: 1rem 2.25rem; }
.finale .btn--primary { box-shadow: 0 14px 36px rgba(0, 0, 0, 0.4); }
.finale__note { margin-top: 1.25rem; font-size: 0.875rem; color: var(--night-muted); }

/* ---------- Footer ---------- */
.footer { border-top: 1px solid var(--color-border); padding-block: 2rem; }
.footer__inner {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2rem;
  align-items: center;
  justify-content: space-between;
  font-size: 0.875rem;
  color: var(--color-muted);
}
.footer a { color: inherit; text-decoration-thickness: 1px; text-underline-offset: 3px; }

/* ---------- Scroll reveals ---------- */
.js .reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.8s var(--ease-out), transform 0.8s var(--ease-out);
}
.js .reveal.is-visible { opacity: 1; transform: none; }

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

/* ⚠️ THE NAV COLLAPSES AT 62rem, NOT 40rem, AND THE GAP IT CLOSES WAS A REAL
   DEFECT. Between 640px and about 960px the full link list came back while the
   bar was still narrow, so the flex row crushed the CTA and Chromium clipped
   its label: "Create your wedding" needed 193px and had 40px on the ten
   generated pages. It never registered as document overflow, so every sweep
   passed while the only button on the page read "Cre". Measured, the long
   label needs ~860px on the home page and ~960px on a content page. Tablets
   now get the same honest nav phones get. */
@media (max-width: 62rem) {
  /*
   * ⚠️ THE WHOLE NAV USED TO VANISH HERE, LOG IN INCLUDED, and nothing replaced
   * it. Found 2026-08-12. The section anchors can go (in-page jumps); anything
   * that LEAVES the page cannot. `nav__keep` marks the survivors: Pricing on
   * the home page, and `.nav__login` stays in the selector because the ten
   * generated pages mark only Log in.
   */
  .nav__links { display: contents; }
  .nav__links a:not(.nav__keep):not(.nav__login) { display: none; }
  .nav__keep,
  .nav__login {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding-inline: 0.4rem;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--color-foreground);
  }
  .nav__keep,
  .nav__login { margin-left: auto; }
  .nav__keep ~ .nav__keep,
  .nav__keep ~ .nav__login { margin-left: 0; }
  .nav.is-night .nav__keep,
  .nav.is-night .nav__login { color: var(--night-foreground); }
  @media (max-width: 22.5rem) {
    .nav__links a.nav__keep:not(.nav__login) { display: none; }
  }
}

@media (max-width: 62rem) {
  /* ⚠️ ONE COLUMN MEANS CENTRED (Brad 2026-08-24: "optimize and center
     elements on mobile"). Everything here was authored for a two-column grid
     and simply collapsed, which left copy, exhibits and photographs all
     hard against the left edge with an arbitrary gutter on the right.
     Numbered steps stay left-aligned inside a centred block: centring a
     numbered list breaks the number column. */
  .scan__grid { grid-template-columns: 1fr; justify-items: center; text-align: center; }
  .scan__grid .sec-head { margin-inline: auto; }
  .scan__grid .lede { margin-inline: auto; }
  .scan__steps { text-align: left; margin-inline: auto; width: 100%; }
  .exhibit { justify-self: center; }
  .plan__head { grid-template-columns: 1fr; gap: 2rem; text-align: center; justify-items: center; }
  .plan__head .lede { margin-inline: auto; }
  .plans-band__head { margin-inline: auto; text-align: center; }
  .plans-band__head .lede { margin-inline: auto; }
  .plans-band__more { justify-content: center; text-align: center; }
  /* The icon column has to go with the centring: leaving the 2-column grid in
     place parks the icon hard left of centred text and reads as a layout bug. */
  .plan__index li { grid-template-columns: 1fr; justify-items: center; text-align: center; gap: 0.3rem; }
  .plan__index .icon { grid-row: auto; margin-top: 0; }
  /* ⚠️ `grid-column: 2` on the description CREATES an implicit second column
     even after the template is reset to 1fr, so the row silently stayed
     two-column and the icon sat hard left of centred text. */
  .plan__index span { grid-column: auto; }
  .deskwrap__cap { text-align: center; }
  .plan__frame .frame__img { max-height: 15rem; }
  .plan__index { grid-template-columns: 1fr; gap: 0; }
  .browser__body { grid-template-columns: 3.75rem 9.5rem 1fr; min-height: 0; }
}
@media (max-width: 40rem) {
  .btn--nav { padding-inline: 1rem; font-size: 0.875rem; }
  .nav__inner { gap: 0.625rem; padding-inline: 1rem; }
  .wordmark { gap: 0.4rem; }
  .wordmark__mark { height: 1.6rem; }
  .wordmark__text { font-size: 0.875rem; letter-spacing: 0.055em; }
}

/* ⚠️ THE NAME LEAVES THE PHONE NAV AT 28rem, AND PRICING IS THE REASON.
   Measured against the bar this replaced: the We Do lockup was 75px wide at
   375px, and mark-plus-name is 131px. The bar had ZERO slack at 375 before the
   rebrand (298px of items plus 36px of gaps inside 335px of room), so there is
   nowhere for 56 more pixels to come from. Padding and gaps give back 26 of
   them and no more.

   Something had to go, and it is not Pricing: `nav__keep` exists precisely
   because the home page shipped with no route to a price until 2026-08-17.
   So below 28rem the bar carries the MARK alone, which is the brand's icon and
   is approved for exactly this use. The name is still in the tab title, the
   hero and the footer.

   28rem, not 40rem, and the crossover is MEASURED: the name needs 110px, and
   the free space in the bar is 104px at 414 and 120px at 430. So it stops
   fitting at about 432px and there is no reason to hide it above that.

   ⚠️ RE-CHECK BY SQUEEZING THE WIDTH, NOT BY LOOKING. A crushed or clipped nav
   item never changes document.scrollWidth, which is how "Create your wedding"
   once shipped reading "Cre" on all ten generated pages. */
@media (max-width: 28rem) {
  .nav .wordmark__text { display: none; }
  .nav .wordmark__mark { height: 1.75rem; }
}

@media (max-width: 40rem) {
  .hero { min-height: 0; }
  .hero__stage { min-width: 0; width: 100%; padding-block: 3rem 4rem; }
  .hero__eyebrow { font-size: 1rem; line-height: 1.5; letter-spacing: .08em; white-space: normal; }
  .hero__actions .btn--primary, .hero__actions .btn--outline { width: 100%; }
  .exhibit { padding: 0 0 3rem 0; max-width: 20rem; margin-inline: auto; }
  .exhibit .tablecard { left: -0.5rem; width: 12.5rem; bottom: 6.5rem; }
  .hero__actions { justify-content: center; }
  .trust__photo { max-width: 100%; }
  .feedsec__copy .display { text-wrap: balance; }
  .plans-band__more .btn--outline { width: 100%; justify-content: center; }
  /* On a phone the mockup's sidebar is 40% of the frame and unreadable. The
     header and stat row still say "laptop" on their own. */
  .browser__body { grid-template-columns: 1fr; }
}

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  .hero { perspective: none; }
  .hero__stage { transform-style: flat; will-change: auto; }
  .hero__bg img { will-change: auto; }
  html { scroll-behavior: auto; }
  .js .reveal-line > span { animation: none; transform: none; }
  .js .reveal { opacity: 1; transform: none; transition: none; }
  .btn, .nav__links a::after { transition: none; }
  .btn--primary:hover { transform: none; }
}

/* ============================================================
   Motion layer (2026-08-17, "make it jaw dropping")
   Everything below is transform/opacity/clip-path only, and every
   loop, autoplay and particle is killed by prefers-reduced-motion
   at the bottom of this file.
   ============================================================ */

/* ---------- Scroll progress hairline (under the nav) ---------- */
.progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  z-index: 60;
  background: var(--color-primary);
  transform-origin: 0 50%;
  transform: scaleX(0);
  pointer-events: none;
}

/* ---------- Hero title ----------
   ⚠️ THE PER-LETTER RISE IS GONE, AND ITS `.ht` MARKUP WITH IT. It existed to
   animate "We do." one glyph at a time, which worked on a five-character
   product name and does not work on the brand's tagline. Both lines now use
   the same masked line-reveal as the rest of the hero, so there is one
   entrance animation on the page instead of two.

   ⚠️ AND "One special day. One complete app." RETIRED WITH THE OLD LOGO. It
   was the We Do lockup's own line; the new lockup carries "Everyone.
   Everything. Together." Do not carry the old line over by habit. */
.hero__title em { font-style: italic; color: var(--color-primary-lift); }

/* The gold flourish: a hairline fading in from nothing, a rotated gold
   diamond, a hairline fading back out. This is the brand's one ornament and
   it is the main thing stopping the rebrand reading as the lilac site
   recoloured. Gold is legal here because it is a MARK, not text. */
.flourish--hero { gap: 0.7rem; margin-top: 1.4rem; }
.flourish--hero i {
  width: clamp(2.5rem, 7vw, 5rem);
  background: linear-gradient(90deg, color-mix(in srgb, var(--color-accent) 0%, transparent), var(--color-accent));
}
.flourish--hero i + b + i {
  background: linear-gradient(90deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 0%, transparent));
}
.flourish--hero b { width: 0.46rem; height: 0.46rem; }

/* ---------- Candlelight bokeh (canvas, JS-populated) ---------- */
.bokeh { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
.bokeh canvas { width: 100%; height: 100%; display: block; }
.hero { position: relative; overflow: hidden; }
.hero .section__inner { position: relative; z-index: 1; }
.finale .bokeh { z-index: 1; }

/* ---------- Buttons: shine sweep + magnetic (JS adds transform) ---------- */
.btn--primary { position: relative; overflow: hidden; }
.btn--primary::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: -60%;
  width: 40%;
  background: linear-gradient(105deg, transparent, rgba(255, 255, 255, 0.35), transparent);
  transform: skewX(-18deg);
  transition: left 0.6s var(--ease-out);
}
.btn--primary:hover::after { left: 120%; }
.btn[data-magnetic] { will-change: transform; }
/* The nav is a flex row and the CTA is the one item in it that cannot be
   allowed to shrink: a squeezed button clips its label instead of wrapping. */
.btn--nav { flex: none; }
/* Responsive button labels. See the comment on .btn--nav in index.html.
   ⚠️ THE BREAKPOINT IS 62rem, NOT 40rem, AND THAT IS MEASURED. Between 640px
   and about 960px the full link list is back in the nav while the bar is still
   narrow, which is the WORST case for the CTA, not a safe middle: the long
   label needed 193px and had 40px on the generated pages. Measured fit is
   ~860px on the home page and ~960px on the ten content pages, so the shared
   62rem breakpoint is the first one with headroom on both. */
.btn__narrow { display: none; }
@media (max-width: 40rem) {
  .btn__wide { display: none; }
  .btn__narrow { display: inline; }
}

/* ---------- The Feed: a vertical feed inside a phone, on a night ground ----
   ⚠️ THE HORIZONTAL CAROUSEL IS GONE (2026-08-24). Brad asked for the actual
   feature: "reworked to the actual feed feature that is scrollable vertically
   with those same pictures." Every `.car__*` rule went with it, along with the
   engine in main.js and the text marquee that used to sit above this section.
   The photographs did not change; the shape around them did.

   The band is DARK on purpose. It is the strongest of the section separations
   Brad asked for, and it is also true: the Feed is what the room is looking at
   after dark. The nav flips to its night glass over it, wired through
   `darkSections` in main.js.

   This section was "the photo wall" until 2026-08-17, when Brad asked for
   every reference to it removed and The Feed marketed instead. The APP still
   has a separate Photo Wall screen and route; only the marketing name
   changed, so do not "fix" the app to match without asking first. */
.feedsec {
  min-height: 94svh;
  display: flex;
  align-items: center;
  padding-block: clamp(4rem, 8vw, 6.5rem);
}
.feedsec--night {
  background: var(--night-bg);
  border-block: 1px solid var(--night-border);
  color: var(--night-foreground);
}
/* ⚠️ GOLD ON THE EYEBROW, AND ONLY HERE. Plum is the one ground where gold
   is a text colour (8.6:1, the logo's own pairing); on paper it is 2.2:1 and
   banned. This label is also the clearest signal that the band belongs to
   this brand rather than being the old lilac band with a new hex in it. */
.feedsec--night .eyebrow { color: var(--night-accent); }
.feedsec--night .lede,
.feedsec--night .feedsec__hint,
.feedsec--night .feedsec__scrollhint { color: var(--night-muted); }
.feedsec--night .display em { color: var(--night-accent); }
.feedsec--night :focus-visible { outline-color: var(--night-accent); }
/* The bezel is --color-foreground everywhere else, which is within a few
   percent of --night-bg: on this ground the phone dissolved into the page. */
.feedsec--night .phone { background: var(--night-border); box-shadow: 0 40px 80px -30px #000; }
.feedsec--night .phone::after { background: var(--night-border); }
.feedsec__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: clamp(2.5rem, 7vw, 7rem);
  align-items: center;
  width: 100%;
}
.feedsec__copy .display { margin-block: 1.1rem 1.25rem; }
.feedsec__hint { margin-top: 1.75rem; font-size: 1.0625rem; font-style: italic; font-family: var(--font-display); }
.feedsec__stage { justify-self: center; }
.feedsec__scrollhint { margin-top: 1rem; text-align: center; font-size: 0.8125rem; }
.phone--feed { width: min(21rem, 76vw); }

/* ⚠️ THE SCREEN IS LIGHT AND THE FLOATING "The Feed LIVE" PILL IS GONE. Both
   were inventions: the guest app has no dark theme and no such pill. It draws
   the couple's names, the date, the share row and the filter chips, which is
   what `.appbar__*`, `.compose` and `.chips` are. The band around the phone
   stays dark; the contrast is better this way round anyway.

   The top strip does NOT scroll with the posts. In the app it does, but here
   the strip is what identifies the screen as the Feed, and a marketing loop
   that drifts its own header off the top loses the label three seconds in. */
.phone--feed .appbar {
  position: absolute;
  inset-inline: 0;
  top: 0;
  z-index: 3;
  padding: 2.15rem 0.7rem 0.55rem;
  background: linear-gradient(var(--color-background) 82%, transparent);
}

/* The scroller. A plain overflow box, so touch, wheel and keys are native;
   main.js only nudges scrollTop for the idle drift. */
.fw {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  scrollbar-width: none;
  /* ⚠️ CLEARS THE FIXED TOP STRIP, AND THE NUMBER IS MEASURED, NOT GUESSED.
     The strip ends 153px down; at 8.4rem the first card started at 134 and the
     filter chips sat on top of its "Photo shared" line. Anything added to
     `.appbar` moves this: measure `.appbar` bottom against `.phone__screen`
     top rather than nudging it. The strip now ends at 147px. */
  padding: 9.75rem 0.7rem 3rem;
}
.fw::-webkit-scrollbar { display: none; }
.fw__track { display: grid; gap: 0.5rem; }
/* The clone main.js appends is a sibling of the track and needs the same gap
   above it, or the seam shows as a tight join once per loop. */
.fw__track + .fw__track { margin-top: 0.5rem; }
/* Posts fading out above the tab bar is what makes it read as a feed that
   continues rather than a list that ends. */
.fw__fade {
  position: absolute;
  inset-inline: 0;
  bottom: 2.4rem;
  height: 2.5rem;
  z-index: 2;
  pointer-events: none;
  margin: 0;
  background: linear-gradient(transparent, var(--color-background));
}

@media (max-width: 62rem) {
  .feedsec { min-height: 0; }
  .feedsec__grid { grid-template-columns: 1fr; justify-items: center; text-align: center; }
  .feedsec__copy .lede { margin-inline: auto; }
  .feedsec__stage { justify-self: center; }
}

/* ---------- The screen switcher above the phone ----------
   ⚠️ These were defined inside the block that got replaced when the mocks were
   re-traced on 2026-08-24, and losing them is silent: the buttons still work,
   they just render as bare bordered boxes, which reads as "the CSS did not
   load" rather than as a missing rule. */
.ptabs {
  display: flex;
  justify-content: center;
  /* nowrap, not wrap: four tabs that fall onto a second row are exactly the
     "not 2 or 3 lines" complaint, and they fit at 320px once .ptab shrinks. */
  flex-wrap: nowrap;
  gap: 0.4rem;
  /* ABOVE the phone, not below it: the exhibit's bottom-left corner belongs
     to the absolutely-positioned table card, which covered half the strip and
     made two tabs unclickable when the tabs sat under the device. */
  margin-bottom: 0.9rem;
}
.ptab {
  white-space: nowrap;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-muted);
  border-radius: 999px;
  min-height: 2.4rem;
  padding: 0 0.95rem;
  font-family: var(--font-sans);
  font-size: 0.8125rem;
  font-weight: 500;
  cursor: pointer;
  transition: background-color 0.2s, color 0.2s, border-color 0.2s;
}
.ptab:hover { color: var(--color-foreground); }
.ptab.is-active {
  background: var(--color-foreground);
  border-color: var(--color-foreground);
  color: var(--color-surface);
}

/* ---------- The interactive phone ---------- */
.pscreen {
  position: absolute;
  inset: 0;
  opacity: 0;
  transform: translateY(10px) scale(0.985);
  transition: opacity 0.45s var(--ease-out), transform 0.45s var(--ease-out);
  pointer-events: none;
  /* ⚠️ NO PADDING HERE. `.app` inside owns it, and when both had it the two
     stacked: 2.6rem + 2.5rem opened every screen except Home 5.1rem down the
     phone, which read as a rendering fault rather than as a layout choice.
     Home looked right only because it carried a `padding: 0` override, which
     is the tell that the padding belonged one level down all along. */
  padding: 0;
  display: flex;
  flex-direction: column;
}
.pscreen.is-active { opacity: 1; transform: none; pointer-events: auto; }
/* ============================================================
   THE APP MOCKS
   ⚠️ TRACED FROM THE RUNNING APP, 2026-08-24 (Brad: "make sure all the app
   previews on the site are semi accurate to the real ui's"). Captured against
   the Gary & Lisa (Test) fixture on staging. Component names here mirror what
   the screens are, not what the marketing section is called, because the next
   person to check accuracy will be looking at the app, not at this file.

   ⚠️ THE PHONE SCREEN IS LIGHT, INCLUDING INSIDE THE DARK FEED SECTION. It was
   dark there for one day and that was a straight invention: the guest app has
   no dark screen. The section ground stays dark, which is better anyway, since
   a light phone on a dark band is the strongest thing on the page.

   The shared vocabulary, so a new screen costs markup and not CSS:
     .appbar__names / .appbar__date   the header every guest screen carries
     .ps__title + .flourish           the italic page title under it
     .compose / .chips                the Feed's share row and its filters
     .app__body                       the scrolling middle
     .tabbar                          Home, Feed, Games, Invite
     .fab                             the Feed's upload button
   ============================================================ */

/* The header: couple names, then the date, on every guest screen */
.appbar__names {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 500;
  font-size: 0.92rem;
  line-height: 1.1;
  text-align: center;
  color: var(--color-primary);
}
.appbar__names em { font-style: italic; }
.appbar__date {
  margin-top: 0.15rem;
  text-align: center;
  font-size: 0.44rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--color-muted);
}
.ps__title {
  margin-top: 0.55rem;
  text-align: center;
  font-family: var(--font-display);
  font-style: italic;
  font-size: 1.28rem;
  color: var(--color-primary);
}
.ps__title + .flourish { margin-block: 0.3rem 0.55rem; }

/* The Feed's share row: Photos, A note, A song */
.compose { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.3rem; margin-top: 0.5rem; }
.compose span {
  background: var(--color-surface);
  border-radius: 0.6rem;
  padding: 0.4rem 0.2rem;
  display: grid;
  justify-items: center;
  gap: 0.15rem;
  font-size: 0.5rem;
  font-weight: 700;
  box-shadow: 0 4px 12px rgba(42, 22, 54, 0.06);
}
.compose .icon { width: 0.8rem; height: 0.8rem; color: var(--color-primary); }

/* Everything / Photos / Songs */
.chips { display: flex; gap: 0.28rem; margin-top: 0.5rem; }
.chips b, .chips span {
  font-size: 0.48rem;
  font-weight: 600;
  padding: 0.22rem 0.55rem;
  border-radius: 999px;
  background: var(--color-surface);
  color: var(--color-muted);
}
.chips b { background: var(--color-primary); color: var(--color-primary-foreground); }

/* The scrolling middle of a screen. The tab bar is 2.4rem and overlaps. */
.app__body {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  display: grid;
  gap: 0.42rem;
  align-content: start;
  margin-top: 0.5rem;
  padding-bottom: 2.6rem;
}

/* "You're Maya  Change" */
.app__you { text-align: center; font-size: 0.52rem; color: var(--color-muted); }
.app__you b { color: var(--color-foreground); }
.app__you i { font-style: normal; color: var(--color-primary); text-decoration: underline; margin-left: 0.25rem; }

/* The countdown card: a white card wrapping a filled pill */
.app__card {
  background: var(--color-surface);
  border-radius: 1rem;
  box-shadow: 0 8px 20px rgba(42, 22, 54, 0.07);
  padding: 0.5rem;
}
.app__cdlabel {
  text-align: center;
  font-size: 0.42rem;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  color: var(--color-muted);
  margin-bottom: 0.4rem;
}
.app__cdpill {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  border-radius: 0.7rem;
  background: var(--color-primary);
  color: var(--color-primary-foreground);
  padding: 0.42rem 0.2rem;
  text-align: center;
}
.app__cdpill b {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.05rem;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.app__cdpill span {
  display: block;
  margin-top: 0.12rem;
  font-size: 0.4rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  opacity: 0.85;
}

/* ⚠️ ROWS, NOT TILES. See the note in index.html: the two-across tile grid is
   the layout guest-home.tsx deleted on 2026-08-18. */
.app__rows { display: grid; gap: 0.4rem; }
.app__rows li {
  background: var(--color-surface);
  border-radius: 0.8rem;
  padding: 0.5rem 0.55rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  box-shadow: 0 6px 16px rgba(42, 22, 54, 0.06);
}
.app__rowtext { display: grid; min-width: 0; flex: 1; }
.app__rowtext b { font-size: 0.6rem; font-weight: 700; }
.app__rowtext span { font-size: 0.5rem; color: var(--color-muted); }
.app__rows i { font-style: normal; color: var(--color-muted); font-size: 0.7rem; }
.app__rows .chip,
.app__tilechip {
  width: 1.5rem;
  height: 1.5rem;
  flex: none;
  border-radius: 50%;
  background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface));
  color: var(--color-primary);
  display: grid;
  place-items: center;
}
.app__rows .chip .icon { width: 0.85rem; height: 0.85rem; }

/* Home, Feed, Games, Invite — `guest-tabs.ts`. Fixed to the phone's bottom. */
.tabbar {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  z-index: 4;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  padding: 0.35rem 0.3rem 0.5rem;
  background: color-mix(in srgb, var(--color-surface) 92%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border-top: 1px solid var(--color-border);
}
.tabbar span {
  display: grid;
  justify-items: center;
  gap: 0.1rem;
  font-size: 0.44rem;
  font-weight: 600;
  color: var(--color-muted);
}
.tabbar .icon { width: 0.9rem; height: 0.9rem; }
.tabbar .is-on { color: var(--color-primary); }

/* The Feed's upload button, `position: fixed` in the app */
.fab {
  position: absolute;
  right: 0.6rem;
  bottom: 3rem;
  z-index: 4;
  width: 1.9rem;
  height: 1.9rem;
  border-radius: 50%;
  background: var(--color-primary);
  color: var(--color-primary-foreground);
  display: grid;
  place-items: center;
  font-size: 1rem;
  line-height: 1;
  box-shadow: 0 8px 20px rgba(42, 22, 54, 0.25);
}

/* ---------- Feed posts ---------- */
.fwpost {
  background: var(--color-surface);
  border-radius: 0.85rem;
  padding: 0.5rem;
  box-shadow: 0 6px 16px rgba(42, 22, 54, 0.06);
}
/* ⚠️ Cards are labelled by KIND, not by who posted. The app writes "Played" or
   "Photo shared" with a relative time on the right; it does not put a name and
   an avatar at the top, which is what this mock invented. */
.fwpost__kind {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.5rem;
  font-weight: 700;
  color: var(--color-primary);
  white-space: nowrap;
}
.fwpost__kind i { font-style: normal; margin-left: auto; font-weight: 500; color: var(--color-muted); }
.fwpost__kind .icon { width: 0.7rem; height: 0.7rem; }
.fwpost__img {
  width: 100%;
  /* ⚠️ `height: auto` is load-bearing: the width/height attributes are
     presentational hints and beat aspect-ratio without it. See README. */
  height: auto;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: 0.55rem;
  margin-top: 0.4rem;
}
.fwpost__cap { font-size: 0.52rem; margin-top: 0.35rem; }
.fwpost__note {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 500;
  font-size: 0.74rem;
  line-height: 1.35;
  margin-top: 0.35rem;
}
.fwpost__comment {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  margin-top: 0.4rem;
}
.fwpost__comment span {
  flex: 1;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  padding: 0.22rem 0.5rem;
  font-size: 0.46rem;
  color: var(--color-muted);
}
.fwpost__comment b {
  background: color-mix(in srgb, var(--color-primary) 55%, var(--color-surface));
  color: var(--color-primary-foreground);
  border-radius: 999px;
  padding: 0.22rem 0.55rem;
  font-size: 0.46rem;
}
/* Three reaction buttons, emoji only. The app shows no counts on them. */
.fwpost__reacts { display: flex; gap: 0.3rem; margin-top: 0.4rem; }
.fwpost__reacts span {
  width: 1.15rem;
  height: 1.15rem;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  font-size: 0.55rem;
}
.fwpost__reacts .is-mine { border-color: var(--color-primary); }
.fwpost__song { display: flex; align-items: center; gap: 0.45rem; margin-top: 0.4rem; }
.fwpost__art {
  width: 1.7rem;
  height: 1.7rem;
  flex: none;
  border-radius: 0.35rem;
  background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface));
  color: var(--color-primary);
  display: grid;
  place-items: center;
}
.fwpost__art .icon { width: 0.8rem; height: 0.8rem; }
.fwpost__meta { display: grid; min-width: 0; }
.fwpost__meta b { font-size: 0.58rem; }
.fwpost__meta span { font-size: 0.48rem; color: var(--color-muted); }

/* ---------- Request a Song ---------- */
.ps__search {
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  border-radius: 0.55rem;
  padding: 0.35rem 0.5rem;
  font-size: 0.52rem;
  color: var(--color-muted);
}
.ps__hint { font-size: 0.46rem; color: var(--color-muted); }
.ps__sub { font-size: 0.5rem; color: var(--color-muted); }
.ps__pills { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.ps__pills span {
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  border-radius: 999px;
  padding: 0.2rem 0.5rem;
  font-size: 0.46rem;
  white-space: nowrap;
}
.ps__pills .is-done { border-color: color-mix(in srgb, var(--color-primary) 45%, transparent); color: var(--color-primary); }
.ps__sechead {
  margin-top: 0.3rem;
  font-size: 0.44rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--color-muted);
}
.ps__req {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  border: 1px solid var(--color-border);
  border-radius: 0.55rem;
  padding: 0.35rem;
  background: var(--color-surface);
}
/* The status line under the artist: "Playing now! 🎶", "The DJ saw this" —
   STATUS_COPY in features/requests/request-card.tsx. */
.ps__req i { font-style: normal; font-size: 0.44rem; font-weight: 700; color: var(--color-muted); }
.ps__req .is-good { color: var(--color-primary); }

/* ---------- Live trivia ---------- */
.ps__answernow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 0.5rem;
  font-weight: 700;
  color: var(--color-muted);
}
.ps__answernow b {
  font-size: 1rem;
  font-variant-numeric: tabular-nums;
  color: var(--color-primary);
}
.ps__timer {
  display: block;
  height: 3px;
  border-radius: 999px;
  background: var(--color-border);
  overflow: hidden;
}
.ps__timer i { display: block; height: 100%; width: 100%; background: var(--color-primary); border-radius: inherit; transform-origin: 0 50%; }
.is-active .ps__timer i { animation: ps-timer 5s linear 0.4s both; }
@keyframes ps-timer { to { transform: scaleX(0); } }
.ps__q { font-size: 0.72rem; font-weight: 700; line-height: 1.25; }
/* ⚠️ STACKED AND LEFT-ALIGNED, not a 2x2 grid. trivia-live.tsx renders one
   full-width button per option; the grid was this mock's own idea. */
.ps__opts { display: grid; gap: 0.28rem; }
.ps__opts span {
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  border-radius: 0.5rem;
  padding: 0.32rem 0.5rem;
  font-size: 0.52rem;
  text-align: left;
}
.ps__opts .is-correct {
  border-color: var(--color-primary);
  background: color-mix(in srgb, var(--color-primary) 14%, var(--color-surface));
  font-weight: 700;
}
.is-active .ps__opts .is-correct { animation: ps-correct 0.5s var(--ease-out) 3.6s both; }
@keyframes ps-correct { from { transform: scale(0.96); } to { transform: none; } }
.ps__verdict {
  border-radius: 0.5rem;
  background: var(--color-soft);
  padding: 0.3rem;
  text-align: center;
  font-size: 0.5rem;
  font-weight: 700;
}
.ps__board { display: grid; gap: 0.25rem; }
.ps__board li {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.5rem;
  background: var(--color-surface);
  border-radius: 0.45rem;
  padding: 0.25rem 0.4rem;
}
.ps__board b { font-family: var(--font-display); font-style: italic; font-weight: 500; color: var(--color-primary); }
.ps__board span { flex: 1; text-align: left; }
.ps__board i { font-style: normal; font-variant-numeric: tabular-nums; }

/* ---------- Section headings settle from a blur ---------- */
.js .reveal .display {
  opacity: 0;
  transform: translateY(18px);
  filter: blur(6px);
  transition: opacity 0.7s var(--ease-out) 0.08s, transform 0.7s var(--ease-out) 0.08s, filter 0.7s var(--ease-out) 0.08s;
}
.js .reveal.is-visible .display { opacity: 1; transform: none; filter: none; }

/* ---------- Photo wipe (planning head photo) ----------
   ⚠️ The `round` keyword is load-bearing. `inset(0 100% 0 0)` clips to a hard
   rectangle, which squares off the photo's rounded corners for the whole
   transition and leaves them square wherever the clip stays applied. */
.js .wipe {
  clip-path: inset(0 100% 0 0 round var(--radius-photo));
  transition: clip-path 1s var(--ease-out) 0.1s;
}
.js .reveal.is-visible .wipe,
.js .wipe.is-visible { clip-path: inset(0 round var(--radius-photo)); }

/* ---------- Counter numbers ---------- */
[data-count] { font-variant-numeric: tabular-nums; }

/* ---------- Motion layer responsive ---------- */
@media (max-width: 40rem) {
  .ptabs { gap: 0.3rem; }
  .ptab { padding: 0 0.7rem; font-size: 0.72rem; }
}

/* ---------- Motion layer: reduced motion kills all of it ---------- */
@media (prefers-reduced-motion: reduce) {
  .progress { display: none; }
  .bokeh { display: none; }
  .btn--primary::after { display: none; }
  .fw { scroll-behavior: auto; }
  .js .reveal .display { opacity: 1; transform: none; filter: none; transition: none; }
  .js .wipe { clip-path: none; transition: none; }
  .is-active .ps__timer i, .is-active .ps__opts .is-correct { animation: none; }
  .ps__opts .is-correct { border-color: var(--color-primary); color: var(--color-foreground); }
  .pscreen { transition: opacity 0.3s; transform: none; }
}

/* ---------------------------------------------------------------------------
   Returning-guest bridge (injected by the inline script in index.html).
   A guest who typed the bare domain instead of using their home-screen icon
   gets one obvious way back into their wedding, above the sales pitch.
   --------------------------------------------------------------------------- */
.returning {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.75rem 1.25rem;
  min-height: 44px;
  background: var(--color-primary);
  color: var(--color-primary-foreground);
  font-family: var(--font-sans);
  font-size: 0.9375rem;
  text-align: center;
  text-decoration: none;
}
.returning strong { font-weight: 600; }
.returning:hover strong { text-decoration: underline; }
.returning:focus-visible {
  outline: 3px solid var(--color-accent);
  outline-offset: -3px;
}
