/* Author: Claude (Anthropic AI), written 2026-10-06. */
/* Save-the-date reveal landing. Brick-dominant, Fraunces, candlelit.
   Scoped under body.landing so it never touches the Dash/DMC pages. */

/* Self-hosted fonts (assets/fonts/README.md): Google's latin subset, variable
   weights. Fraunces is preloaded by the template; the wordmark overlays additionally
   wait for it (.fonts-ready), so swap never shows there. */
@font-face {
  font-family: "Fraunces"; font-style: normal; font-weight: 400 600; font-display: swap;
  src: url("/assets/fonts/fraunces-latin.woff2") format("woff2");
}
@font-face {
  font-family: "Fraunces"; font-style: italic; font-weight: 400 600; font-display: swap;
  src: url("/assets/fonts/fraunces-italic-latin.woff2") format("woff2");
}
@font-face {
  font-family: "Inter"; font-style: normal; font-weight: 400 500; font-display: swap;
  src: url("/assets/fonts/inter-latin.woff2") format("woff2");
}
/* the calendar hint's scrawl; fetched on the reveal tap (landing.js), not with
   the page, so it never competes with the spree photos */
@font-face {
  font-family: "Caveat"; font-style: normal; font-weight: 400 700; font-display: swap;
  src: url("/assets/fonts/caveat-latin.woff2") format("woff2");
}

/* border-box everywhere on the landing so padding is included in sizes (the
   viewport-sized boxes below must not grow by their padding). Scoped to
   body.landing (this sheet is auto-loaded on Dash pages too, which must not be
   touched). */
body.landing,
body.landing *,
body.landing *::before,
body.landing *::after { box-sizing: border-box; }

/* ---------- viewport model: a bleed box and a visible box ----------
   iOS 26 Safari floats translucent toolbars over the page; ordinary content
   renders behind them, but it clips position:fixed layers at the toolbar and
   tints the bars from fixed elements' backgrounds (even at opacity 0). So:
   - nothing here is position:fixed (the page never scrolls, so absolute at the
     document origin is equivalent);
   - the BLEED box (everything visual: stage + shader, spree photos, vignette)
     starts at the document top and spans the --runway, the large viewport and a
     margin, so it reaches under any toolbar;
   - the VISIBLE box (everything legible: wordmarks, greeting, footer, frame, tap
     prompt) is the small viewport, starting --runway down the document.
   The top runway is for the status bar: at scrollY 0 Safari paints that strip
   with the root background, but once scrolled it shows the content beneath. So
   landing.js keeps the document scrolled to exactly --runway (programmatic
   scrolling still works under overflow: hidden), which puts the visible box at
   the viewport top and the runway's slice of the bleed box under the status bar.
   Each height is declared twice: vh first for browsers without svh/lvh. Users
   can't scroll the extra document height (body overflow: hidden). */
html:has(> body.landing) {
  background: #1c1917; /* = --stone-black. Safari also blends the root colour
                          into its blur under the status bar, so keep it dark:
                          a mid-tone reads as a grey seam over the colour field */
  overscroll-behavior: none;
}
.stage, .spree, .vignette {
  height: calc(var(--runway) + 100vh + 5rem);
  height: calc(var(--runway) + 100lvh + 5rem);
}
.content, .tap-prompt {
  top: var(--runway);
  height: 100vh;
  height: 100svh;
}

body.landing {
  /* --- palette: raw stone field, deep draped-cloth colour, candlelit gilt. ---
     Stone is the cold neutral ground; colour enters only as garment/velvet
     accents (wine, sapphire); gold reads as gilt on stone; light is candle. */
  --stone-pale: #8f8578;   /* lit limestone */
  --stone: #5f584e;        /* mid stone */
  --stone-deep: #332f2a;   /* shadowed stone */
  --stone-black: #1c1917;  /* deepest */
  --wine: #6d1f2b;         /* bordeaux velvet (primary garment/drape) */
  --sapphire: #1c3a63;     /* deep blue cloth / dusk */
  --gold: #c9a24b;         /* gilt */
  --candle: #ffcf9e;       /* warm candle highlight */
  --amber: #ffb169;        /* candle glow */
  --parchment: #efe7d6;    /* bone / parchment text */

  /* Hidden document space above the visible box, scrolled under the status bar
     (viewport model, above). Px, read by landing.js; must exceed the tallest
     status bar (~62px with the Dynamic Island). */
  --runway: 120px;

  margin: 0;
  background: var(--stone-black);
  color: var(--parchment);
  font-family: "Fraunces", Georgia, serif;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* Single-screen landing: never scroll. The bleed box is taller than the
     visible area on purpose (see the viewport model above); locking overflow on
     both axes keeps that from becoming scrollable on phone and desktop alike.
     svh/lvh (not dvh) keep it stable as mobile browser chrome shows/hides. */
  overflow: hidden;
}

/* ---------- the rich ground ---------- */
.stage {
  position: relative; /* height: the bleed box, above */
  /* raw stone, candle pool rising from below; grain adds the stone's tooth */
  background:
    radial-gradient(130% 100% at 50% 120%, var(--stone) 0%, var(--stone-deep) 52%, var(--stone-black) 100%);
  isolation: isolate;
  overflow: hidden; /* clip the drifting glow/grain so it can't inflate scroll */
}

/* animated mesh-gradient background (WebGL2, shader-bg.js). Sits above the
   stage's stone gradient (the no-WebGL fallback) and below the
   candle pools, grain, frame and content. Fades in on reveal via .on. */
.bg-shader {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%;
  display: block;
  opacity: 0;
  transition: opacity 1.1s ease .1s;
}
.bg-shader.on { opacity: 1; }

/* candle pools — emission, not fill. Drift + flicker. */
.stage::before {
  content: "";
  position: absolute; inset: -12%;
  pointer-events: none; z-index: 1;
  background:
    radial-gradient(260px 320px at 50% 96%, rgba(255,177,105,.34), transparent 70%),
    radial-gradient(200px 260px at 26% 90%, rgba(255,207,158,.22), transparent 72%),
    radial-gradient(220px 280px at 76% 94%, rgba(228,98,31,.20), transparent 72%);
  filter: blur(8px);
  animation: drift 15s ease-in-out infinite alternate, flicker 3.4s ease-in-out infinite;
}
/* film grain — cheapest "this cost money" cue; also kills gradient banding */
.stage::after {
  content: "";
  position: absolute; inset: 0;
  pointer-events: none; z-index: 2; opacity: .45;
  mix-blend-mode: overlay;
  background-image: 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='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

@keyframes drift {
  from { transform: translate3d(-1.5%, 1%, 0) scale(1.02); }
  to   { transform: translate3d(2%, -1.5%, 0) scale(1.05); }
}
@keyframes flicker {
  0%,100% { opacity: .9; } 25% { opacity: 1; } 42% { opacity: .84; }
  63% { opacity: .97; } 80% { opacity: .9; }
}

/* ---------- thin gold hairline frame (engraved-invitation nod) ---------- */
/* In the visible box, clear of notches / rounded corners / the home indicator
   (landscape) via the safe-area insets. */
.frame {
  --ft: max(14px, env(safe-area-inset-top, 0px));
  --fb: max(14px, env(safe-area-inset-bottom, 0px));
  position: absolute; z-index: 3;
  top: calc(var(--runway) + var(--ft));
  left: max(14px, env(safe-area-inset-left, 0px));
  right: max(14px, env(safe-area-inset-right, 0px));
  height: calc(100vh - var(--ft) - var(--fb));
  height: calc(100svh - var(--ft) - var(--fb));
  border: 1px solid rgba(217,164,65,.55);
  border-radius: 3px;
  pointer-events: none;
}
.frame::before {
  content: ""; position: absolute; inset: 4px;
  border: 1px solid rgba(217,164,65,.22); border-radius: 2px;
}

/* ---------- the composition: one column ----------
   Greeting, wordmark, footer, top to bottom inside the frame. The wordmark
   (.names) takes whatever height is left; landing.js fits its font size to
   that space (--names-fs) and places the spree's wordmark over exactly the same
   box (--names-top/--names-h), so the handoff morphs rather than jumps and
   nothing can overlap, whatever the screen height or greeting length. */
.content {
  position: absolute; left: 0; right: 0; z-index: 3; text-align: center; /* visible box */
  display: flex; flex-direction: column;
  /* inside the frame (whose inset is max(14px, safe area)), plus breathing room */
  padding:
    calc(max(14px, env(safe-area-inset-top, 0px)) + clamp(1.1rem, 4.5svh, 3rem))
    calc(max(14px, env(safe-area-inset-right, 0px)) + 1.1rem)
    calc(max(14px, env(safe-area-inset-bottom, 0px)) + clamp(1.1rem, 4.5svh, 3rem))
    calc(max(14px, env(safe-area-inset-left, 0px)) + 1.1rem);
  /* starts hidden; settles in once the spree releases (see .revealed .content) */
  opacity: 0; transform: translateY(14px) scale(.985);
}
/* with the calendar icon: the wordmark starts below the strip that the icon
   and its hint occupy, reserved from the start so nothing moves when the hint
   appears */
.content.has-cal {
  padding-top: calc(max(14px, env(safe-area-inset-top, 0px)) + 4.25rem);
}

.footer { margin-top: clamp(.6rem, 2.5svh, 1.6rem); }

/* The wordmark. .names (in the column) is its layout box and the accessible
   heading, but is never drawn: the visible wordmark is the pair of .wordmark
   overlays laid over it (further down), above the vignette so it stays on
   screen from the spree to the reveal. Identical metrics in all three — the
   shared rule keeps them in lockstep. The fallback size applies until
   landing.js has fitted --names-fs. */
.names, .wordmark {
  margin: 0;
  display: grid; place-items: center;
  font-family: "Fraunces", Georgia, serif;
  font-weight: 500;
  line-height: .82;
  letter-spacing: -.01em;
  font-size: var(--names-fs, clamp(3rem, 16vw, 6.5rem));
}
.names .amp, .wordmark .amp { font-style: italic; font-weight: 400; }

.names {
  flex: 1 1 0; min-height: 0; /* exactly the leftover height, never its content's */
  opacity: 0;
}

/* ---------- the wordmark overlays ----------
   Over .names' box (landing.js sets --names-top/-h; until then the whole
   visible box), above the vignette and tap gate; pointer-events: none so taps
   pass through to the gate. Knockout white over the spree, the close and the
   press screen; gilt from the tap on. Both wait for Fraunces (.fonts-ready, see
   landing.js) so the face never visibly swaps. */
.wordmark {
  position: absolute; left: 0; right: 0; z-index: 25; /* visible box */
  top: calc(var(--runway) + var(--names-top, 0px));
  height: var(--names-h, 100vh);
  height: var(--names-h, 100svh);
  text-align: center;
  pointer-events: none;
  opacity: 0;
}
/* inverts against whatever lies beneath (photo, vignette, stone-black) */
.wordmark-knockout { color: #fff; mix-blend-mode: difference; }
.wordmark-gilt {
  /* gilt-leaf foil swept across the glyphs (gilded inscription on stone) */
  background: linear-gradient(100deg,
    #7a5e22 0%, var(--gold) 20%, #efd79a 42%, #fff7e4 50%,
    #efd79a 58%, var(--gold) 80%, #7a5e22 100%);
  background-size: 220% 100%;
  -webkit-background-clip: text; background-clip: text; color: transparent;
  animation: sweep 7s ease-in-out infinite;
}
@keyframes sweep {
  0%,100% { background-position: 0% 0; } 50% { background-position: 100% 0; }
}
body.landing.fonts-ready .wordmark-knockout { opacity: 1; transition: opacity .25s ease; }
/* the tap: knockout crossfades to gilt while the vignette opens beneath */
body.landing.revealed .wordmark-knockout { opacity: 0; transition: opacity 1.2s ease; }
body.landing.fonts-ready.revealed .wordmark-gilt { opacity: 1; transition: opacity 1.2s ease; }

/* the salutation under the wordmark, opening the letter that the announcement
   continues (named groups only); long member lists wrap into balanced lines */
.greeting {
  font-style: italic;
  font-size: clamp(1.15rem, 5.4vw, 1.6rem);
  line-height: 1.2;
  color: var(--candle);
  margin: 0 0 clamp(.5rem, 2svh, 1.25rem);
  text-wrap: balance;
}

/* the announcement, running on into the date line below it */
.announce {
  font-style: italic;
  font-size: clamp(.95rem, 4.2vw, 1.25rem);
  line-height: 1.35;
  color: var(--parchment);
  opacity: .88;
  max-width: 24em;
  margin: 0 auto .7rem;
  text-wrap: balance;
}

.date {
  font-family: "Inter", system-ui, sans-serif;
  text-transform: uppercase;
  letter-spacing: .3em;
  font-size: clamp(.78rem, 3.4vw, .95rem);
  color: var(--candle);
  margin: 0 0 .3rem;
}
.location {
  font-family: "Inter", system-ui, sans-serif;
  text-transform: uppercase;
  letter-spacing: .24em;
  font-size: clamp(.58rem, 2.6vw, .68rem);
  color: rgba(242,228,207,.6);
  margin: 0;
}

/* ---------- countdown ---------- */
.countdown {
  display: flex; justify-content: center; gap: clamp(.9rem, 5vw, 1.8rem);
  margin: clamp(.8rem, 3svh, 1.4rem) 0 0;
}
.cd-unit { display: flex; flex-direction: column; align-items: center; }
.cd-num {
  font-weight: 500; font-variant-numeric: tabular-nums;
  font-size: clamp(1.7rem, 8vw, 2.6rem); line-height: 1;
  color: var(--gold);
}
.cd-lab {
  font-family: "Inter", system-ui, sans-serif;
  text-transform: uppercase; letter-spacing: .22em;
  font-size: clamp(.5rem, 2.2vw, .6rem);
  color: rgba(242,228,207,.55); margin-top: .5rem;
}

/* ---------- add to calendar ----------
   A gilt line-icon tucked into the frame's top-right corner (positioned in
   .content, the visible box; the frame's inset is max(14px, safe area)). The
   glyph is 28px, the tap target 48px, its edge 4px inside the frame lines.
   The label lives in aria-label/title. */
.add-cal {
  position: absolute;
  top: calc(max(14px, env(safe-area-inset-top, 0px)) + 4px);
  right: calc(max(14px, env(safe-area-inset-right, 0px)) + 4px);
  width: 48px; height: 48px;
  display: grid; place-items: center;
  color: var(--gold);
  border-radius: 50%;
  transition: color .25s ease, background .25s ease;
}
.add-cal svg {
  fill: none; stroke: currentColor;
  stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round;
}
.add-cal:hover, .add-cal:active { color: var(--candle); background: rgba(28,25,23,.3); }
.add-cal:focus-visible {
  outline: 1px solid rgba(217,164,65,.7);
  outline-offset: -4px;
}

/* The hint: "add to calendar" scrawled in off-white, then an arrow drawn from
   it up into the icon. landing.js adds .cal-hint-on a while after the reveal
   (unless the icon was tapped) and .cal-hint-off when the icon is tapped.
   display: none until then, so Caveat isn't fetched with the page. Placed so
   the arrow's tip (SVG 42,10) lands just left of the icon's glyph, a little
   below its centre (the icon box is 4px inside the frame; glyph 28px in 48). */
.cal-hint {
  /* parchment, the page's off-white: the shader's plum corner (~#3d122b) is
     too dark for any red to read on */
  --hint-ink: var(--parchment);
  position: absolute;
  top: calc(max(14px, env(safe-area-inset-top, 0px)) + 22px);
  right: calc(max(14px, env(safe-area-inset-right, 0px)) + 40px);
  display: none; align-items: flex-end;
  pointer-events: none;
  transform: rotate(-3deg); transform-origin: 100% 0;
  transition: opacity .4s ease;
}
body.landing.cal-hint-on .cal-hint { display: flex; }
body.landing.cal-hint-off .cal-hint { opacity: 0; }
.cal-hint-text {
  font-family: "Caveat", "Fraunces", cursive;
  font-weight: 700;
  font-size: clamp(1.3rem, 6vw, 1.6rem);
  line-height: 1;
  color: var(--hint-ink);
  white-space: nowrap;
  animation: hint-text .5s ease both;
}
.cal-hint svg { flex: none; overflow: visible; }
.cal-hint path {
  fill: none; stroke: var(--hint-ink);
  stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 1; stroke-dashoffset: 1; /* pathLength="1": fully undrawn */
}
.cal-hint-shaft { animation: hint-draw .6s cubic-bezier(.3,.1,.3,1) .35s forwards; }
.cal-hint-head  { animation: hint-draw .25s ease-out .95s forwards; }
@keyframes hint-text { from { opacity: 0; transform: translateY(4px); } }
@keyframes hint-draw { to { stroke-dashoffset: 0; } }

/* ---------- flash-spree overlay ---------- */
.spree {
  position: absolute; top: 0; left: 0; right: 0; z-index: 10; /* bleed box */
  background: var(--stone-black);
}
/* The frames' box. Phones and portrait tablets: the whole screen, photos
   cover-cropped (modern phones are taller than 9:16, so only the sides trim). */
.spree-reel { position: absolute; inset: 0; }
/* Landscape / wide screens: cover-cropping a portrait photo to 16:10 would keep
   about a third of its height, so the photos sit in a centred 9:16 column on the
   stone-black ground instead, its sides feathered out rather than hard-edged. */
@media (min-aspect-ratio: 4/5) {
  .spree-reel {
    left: 50%; right: auto;
    width: calc(100vh * 9 / 16);
    width: calc(100dvh * 9 / 16);
    transform: translateX(-50%);
    -webkit-mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
            mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
  }
}
/* Photos are <img> (decoded before the spree runs, see landing.js); the colour
   fallbacks are <div>s with a gradient — each rule pair covers one of them. */
.spree-frame {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: center;
  background-size: cover; background-position: center;
  opacity: 0;
}
.spree-frame.on { opacity: 1; }

/* Spree exit, leaving the spree's stone-black ground for the tap gate. Normally
   the vignette closes over the held frame and landing.js cuts it away beneath
   it, unseen (.spree-closed). Without the vignette (no WebGL2) it fades out
   instead (.spree-fading). The wordmark is above both and stays. */
body.landing.spree-closed .spree-frame { visibility: hidden; }
body.landing.spree-fading .spree-frame.on {
  opacity: 0;
  transition: opacity .8s ease;
}
/* Its (by now empty, stone-black) ground fades on the reveal. Never scale or
   move it: anything poking past the viewport's sides widens the page on mobile,
   which zooms it out, so the svh boxes (frame, column) stop at the bottom edge. */
body.landing.revealed .spree {
  opacity: 0;
  transition: opacity .7s ease;
  pointer-events: none;
}

/* ---------- "tap to reveal" prompt ----------
   A full-screen button (above the spree) so a tap/click anywhere reveals; the
   label floats in the lower third. Keeps keyboard focus + a screen-reader label. */
.tap-prompt {
  position: absolute; left: 0; width: 100%; z-index: 15; /* visible box */
  display: flex; align-items: flex-end; justify-content: center;
  padding-bottom: 22vh;
  padding-bottom: 22svh;
  margin: 0; border: 0; background: transparent; cursor: pointer;
  font-family: "Inter", system-ui, sans-serif;
  font-weight: 500; text-transform: uppercase;
  letter-spacing: .3em;
  font-size: clamp(.64rem, 2.8vw, .78rem);
  color: var(--candle);
  opacity: 0; pointer-events: none;
  transition: opacity .6s ease;
}
/* Fade in (tap-in), then pulse, which starts where the fade ends (opacity 1).
   No transition on the way in: a running transition outranks animations in the
   cascade, so the .6s fade above would play first and tap-in would then restart
   it from 0 (a double fade). Leaving this state restores the transition, which
   fades the label out on the tap. */
body.landing.awaiting-tap .tap-prompt {
  opacity: 1; pointer-events: auto;
  transition: none;
  animation: tap-in 1.2s ease .3s both, tap-pulse 2.2s ease-in-out 1.5s infinite;
}
.tap-prompt:focus-visible {
  outline: 1px solid rgba(217,164,65,.7);
  outline-offset: -8px;
  border-radius: 2px;
}
@keyframes tap-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes tap-pulse { 0%,100% { opacity: 1; } 50% { opacity: .55; } }

/* ---------- spotlight vignette (top canvas of the sandwich) ----------
   A WebGL canvas (vignette.js) darkening everything below, with a transparent hole
   at the tap point that grows until the scene is revealed. Above everything (incl.
   the fading spree) so the spotlight appears right at the tap; pointer-events: none
   so clicks/mousemove pass through to the tap gate and the colour shader. JS toggles
   display while the one-shot animation runs. */
.vignette {
  position: absolute; top: 0; left: 0; z-index: 20; /* bleed box */
  width: 100%;
  display: none; pointer-events: none;
}

/* content settles in once the spree releases */
body.landing.revealed .content {
  opacity: 1; transform: none;
  transition: opacity 1s ease .15s, transform 1.2s cubic-bezier(.16,.84,.3,1) .15s;
}
