/* 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 render box and a visible box ----------
   Two boxes, both measured by viewport.js (no svh/lvh/dvh: those are guesses
   about toolbar state, and go stale):
   - the RENDER box (everything visual: stage + shader, spree photos, vignette)
     is the whole document: the visible box plus a bleed above and below,
     bounded by the physical screen, so it reaches under any browser chrome
     (--render-h);
   - the VISIBLE box (everything legible: wordmarks, column, frame, tap prompt)
     is the current unobscured layout viewport, at --vis-top in the document,
     --vis-h tall; device intrusions within it (notch, home indicator, Android
     gesture bar) are the env(safe-area-inset-*)s, applied below.
   No API reports browser toolbars, but the layout viewport already excludes
   them (iOS: above the floating bar, collapsed or expanded). Portrait iOS gives
   no top/bottom insets and paints its status bar with the root background at
   scrollY 0, yet shows scrolled content under it, so viewport.js keeps the
   document scrolled past the top bleed (a workaround, not a supported
   technique; if Safari refuses it, only that strip falls back to the root
   colour). Nothing here is position:fixed: iOS Safari paints a solid band under
   its toolbars sampled from viewport-sized fixed layers. Users can't scroll the
   extra 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: var(--render-h); }
.content, .tap-prompt {
  top: var(--vis-top);
  height: var(--vis-h);
}

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 */

  /* The measured boxes (viewport model, above); viewport.js overrides these
     defaults inline before the first frame. --vu is 1% of the visible height. */
  --render-h: 100vh;
  --vis-top: 0px;
  --vis-h: 100vh;
  --vu: 1vh;
  --photo-above: 0px;
  --photo-h: 100vh;

  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 render 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. */
  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(--vis-top) + var(--ft));
  left: max(14px, env(safe-area-inset-left, 0px));
  right: max(14px, env(safe-area-inset-right, 0px));
  height: calc(var(--vis-h) - 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.5 * var(--vu), 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.5 * var(--vu), 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.5 * var(--vu), 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(--vis-top) + var(--names-top, 0px));
  height: var(--names-h, var(--vis-h));
  text-align: center;
  pointer-events: none;
  opacity: 0;
  /* During the spree its centre sits --wm-start down the visible box, below
     the faces in most photos; .wordmark-rise (landing.js, as the vignette
     closes) slides it up to its place. */
  --wm-start: .67;
  transform: translateY(calc(var(--vis-h) * var(--wm-start)
    - var(--names-top, 0px) - var(--names-h, var(--vis-h)) / 2));
}
/* 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 rise, from a beat into the vignette's close: eases in and out, the
   settle longer than the start, and ends at 2.4s, its tail overlapping the tap
   prompt's fade-in (from CLOSE_MS 1.6s in vignette.js + .3s). Keeps the
   fade-in above in case Fraunces is late. */
body.landing.wordmark-rise .wordmark {
  transform: none;
  transition: transform 2s cubic-bezier(.5,0,.2,1) .4s, 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);
  /* Pinned optical size: Fraunces italic swaps in its plain "&" below opsz
     ~21.6 and its decorative one above, and auto opsz follows the font size,
     which crosses that line between iPhone widths. 20 keeps the plain one on
     every screen, the decorative "&" being the wordmark's alone. */
  font-variation-settings: "opsz" 20;
  line-height: 1.2;
  color: var(--candle);
  margin: 0 0 clamp(.5rem, 2 * var(--vu), 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, 3 * var(--vu), 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%;
  -webkit-tap-highlight-color: transparent; /* no grey flash on tap (iOS) */
  transition: color .25s ease;
}
.add-cal svg {
  fill: none; stroke: currentColor;
  stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round;
}
/* a warmer gilt while pressed, and on hover only where hover is real: iOS
   keeps :hover on a tapped element */
.add-cal:active { color: var(--candle); }
@media (hover: hover) { .add-cal:hover { color: var(--candle); } }
.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 by then); once shown it stays.
   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;
}
body.landing.cal-hint-on .cal-hint { display: flex; }
.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;
}
/* Drawn by dash offset, in the paths' real lengths (shaft 49.4, head 18.2 SVG
   units; no pathLength, whose rescaling the dashes would otherwise depend on).
   Undrawn, the one dash lies wholly before the path's start (offset > its
   length, gap > the path): at offset = length exactly, a zero-length dash would
   sit at the start and the round cap would draw it as a dot. `both` holds the
   undrawn base value through the delay; the keyframe draws to offset 0. */
.cal-hint-shaft {
  stroke-dasharray: 50 100; stroke-dashoffset: 51;
  animation: hint-draw .6s cubic-bezier(.3,.1,.3,1) .35s both;
}
.cal-hint-head {
  stroke-dasharray: 19 40; stroke-dashoffset: 19.5;
  animation: hint-draw .25s ease-out .95s both;
}
@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 estimated physical screen
   (--photo-h tall, starting --photo-above over the visible box; viewport.js),
   not the render box, whose double bleed would zoom the photos in; photos
   cover-cropped (modern phones are taller than 9:16, so only the sides trim). */
.spree-reel {
  position: absolute; left: 0; right: 0;
  top: calc(var(--vis-top) - var(--photo-above));
  height: var(--photo-h);
}
/* 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, the
   visible box's height, 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;
    top: var(--vis-top); height: var(--vis-h);
    width: calc(var(--vis-h) * 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 visible 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: calc(22 * var(--vu));
  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;
}
