/* Wedding Road — a drive down a painted ribbon.
   The ribbon carries the road and both verges as one image, so there is no
   layer boundary anywhere in the scene and nothing to blend at runtime. */

:root {
  --leg-h: 150vh;
  --asphalt: #2B2C2E;
  --leaf-deep: #25401F;
  --ground: #1b2416;
  --gold: #E8C66A;
  --blossom: #F6DDE3;
  --text: #FFFFFF;
  /* Shared by every fixed text overlay (.title, .details) — a tight dark
     layer for edge contrast against busy painted art, plus a soft wide one
     so the whole glyph reads even over a light patch of the scene. A single
     soft blur alone (the original spec) wasn't enough contrast once the
     type got large and content varied — the fixed word "and" or a event
     name can land over sky, water or a light hillside depending on scroll
     position, not just the same day-lit road every time. */
  --overlay-shadow:
    0 1px 3px rgba(0, 0, 0, .75),
    0 2px 10px rgba(0, 0, 0, .6),
    0 4px 30px rgba(10, 18, 8, .55);
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; background: var(--ground); }
body {
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overscroll-behavior-y: none;
  font-family: "EB Garamond", Georgia, serif;
  color: var(--text);
}

/* ---------------------------------------------------------------- the world */

.world {
  position: fixed; inset: 0; overflow: hidden; z-index: 0;
  background: var(--ground);
  /* contrast(1.113)/saturate(1.08) used to live here as a runtime filter.
     Moved into tools/build-ribbon.py's own pixel pipeline instead (baked
     into the shipped .webp) — filter on the element that also gets
     transform-animated every scroll frame forces the browser to
     re-rasterize the filtered output continuously, a real cause of scroll
     jank on mobile. Same visual result, zero per-frame cost this way. */
}

.ribbon {
  position: absolute; top: 0; left: 50%;
  will-change: transform;
  transform: translate3d(0, 0, 0);
}
/* chunks are cuts from one strip — integer tops and heights keep them hairline-free */
.ribbon img {
  position: absolute; left: 0; width: 100%;
  display: block; pointer-events: none;
}

/* A tappable box painted into the art (e.g. a "take me here" callout) — the
 * one interactive island inside an otherwise look-don't-touch scrolling
 * scene, so pointer-events is explicitly re-enabled here rather than
 * inherited (nothing else in .ribbon should ever intercept the scroll drag).
 * Invisible: the affordance is already painted into the image itself: this
 * is only the tap target sized and positioned to match it (see measure() in
 * road.js and build-ribbon.py's link export). */
.map-link {
  position: absolute;
  pointer-events: auto;
  cursor: pointer;
}
/* The same action as a real button, above the venue lines, for as long as a
   callout is on screen — a pill reads as tappable where painted art doesn't. */
.stop-btn {
  position: fixed; z-index: 23; left: 50%;
  bottom: calc(3vh + env(safe-area-inset-bottom, 0px) + 84px);
  display: inline-flex; align-items: center; gap: 9px;
  padding: 0 20px 0 16px; height: 48px; border-radius: 999px;
  font: inherit; font-family: "EB Garamond", Georgia, serif; font-size: 1.1rem; letter-spacing: .03em;
  color: #FFF6E8; white-space: nowrap; cursor: pointer;
  background: linear-gradient(180deg, rgba(107, 44, 22, .8), rgba(75, 27, 12, .86));
  border: 1px solid rgba(232, 198, 106, .6);
  backdrop-filter: blur(10px) saturate(130%); -webkit-backdrop-filter: blur(10px) saturate(130%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .14), 0 8px 26px rgba(0, 0, 0, .35);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .35);
  opacity: 0; visibility: hidden; pointer-events: none;
  transform: translate(-50%, 14px) scale(.96);
  transition: opacity .4s ease, transform .5s cubic-bezier(.2, .9, .25, 1.1), visibility 0s linear .4s;
  -webkit-tap-highlight-color: transparent;
}
.stop-btn svg { width: 14px; height: 18px; flex: none; }
.stop-btn svg path { fill: var(--gold); }
.stop-btn svg circle { fill: #4B1B0C; }
.stop-btn.on {
  opacity: 1; visibility: visible; pointer-events: auto;
  transform: translate(-50%, 0);
  transition: opacity .4s ease, transform .5s cubic-bezier(.2, .9, .25, 1.1), visibility 0s;
}
.stop-btn:active { transform: translate(-50%, 0) scale(.97); }
html.sheet-open .stop-btn { opacity: 0; visibility: hidden; pointer-events: none; }

/* tarmac motion smear, gated on measured speed */
.streaks {
  position: absolute; top: 0; left: 50%;
  height: 300vh; opacity: 0;
  /* hidden, not just transparent, whenever it isn't moving — road.js flips
     this on only while measured speed is above zero (see tick()) */
  visibility: hidden;
  pointer-events: none;
  will-change: transform, opacity;
  /* normal blend on purpose: the background tile set from road.js is
     pre-rendered so plain alpha blending matches what overlay used to do,
     without overlay's extra per-frame render passes (see start()) */
}

.vignette {
  position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(120% 62% at 50% 8%,  rgba(255,248,226,.16), rgba(255,248,226,0) 58%),
    radial-gradient(155% 96% at 50% 104%, rgba(10,18,8,.42),   rgba(10,18,8,0)   56%);
}

/* ----------------------------------------------------------- atmosphere */

.clouds {
  position: fixed; left: -6%; right: -6%; top: 0; height: 100%;
  z-index: 6; pointer-events: none; opacity: .12;
  background-repeat: repeat-y; will-change: transform;
  background-image:
    radial-gradient(58% 24% at 22% 11%, rgba(255,252,244,.62), rgba(255,252,244,0) 70%),
    radial-gradient(43% 19% at 79% 37%, rgba(255,250,238,.48), rgba(255,250,238,0) 72%),
    radial-gradient(64% 21% at 44% 62%, rgba(255,247,234,.42), rgba(255,247,234,0) 74%),
    radial-gradient(36% 15% at 11% 85%, rgba(255,252,246,.34), rgba(255,252,246,0) 76%);
}

/* falling petals (petals.js) — above the car, under the time-of-day tint so
   they take the same light as the road */
.petals { position: fixed; inset: 0; width: 100vw; height: 100vh; z-index: 14; pointer-events: none;
          opacity: 0; transition: opacity 1.2s ease; }
.petals.on { opacity: 1; }

/* normal blending on purpose — see the DAY table in road.js for why, and
 * for how its tint colours were fitted to match the old soft-light look */
.tint  { position: fixed; inset: 0; z-index: 18; pointer-events: none; background-color: rgba(255,226,70,.046); }
.dusk  { position: fixed; inset: 0; z-index: 19; pointer-events: none; background-color: rgba(20,26,40,0); }
/* Film grain. This used to be an feTurbulence SVG at opacity .10 with
 * mix-blend-mode: overlay — and because the ribbon underneath moves every
 * scroll frame, an overlay layer has to be re-blended against a fresh
 * backdrop every frame (a separate render surface plus a blend pass over the
 * whole viewport), for an effect that measured at ~1 level out of 255.
 * Overlay against grey noise really does just two things here: a faint
 * lift (the noise averages above 50% grey) and sub-level speckle. So the
 * tile is now pre-rendered offline (art/grain*.png) from the very same
 * noise field Chromium rasterised for the old SVG, least-squares
 * fitted to what overlay produced over the scene's real colours, and
 * reduced to a 4-entry palette (2-bit PNG) of warm near-white speckles at
 * four strengths — drawn with plain normal blending, which the compositor
 * can do without reading the backdrop at all.
 *
 * Why the tiny opacity rather than baking alpha into the PNG: the fitted
 * alphas are ~.004-.011, i.e. 1-3 steps of an 8-bit alpha channel, which
 * would quantise the four strengths into two. Storing them at near-full
 * range in the palette and scaling with opacity keeps them distinct.
 *
 * Why three resolutions: feTurbulence was rendered at device resolution,
 * so on a 3x phone the old grain had 3x the detail per CSS px. image-set
 * picks the matching tile (5 / 18 / 38 KB, one download); the plain url()
 * line is the fallback for browsers without image-set. */
.grain {
  position: fixed; inset: 0; z-index: 20; pointer-events: none; opacity: .011;
  background-image: url("art/grain@2x.png");
  background-image: -webkit-image-set(url("art/grain.png") 1x, url("art/grain@2x.png") 2x, url("art/grain@3x.png") 3x);
  background-image: image-set(url("art/grain.png") 1x, url("art/grain@2x.png") 2x, url("art/grain@3x.png") 3x);
  background-size: 140px 140px;
}

/* ------------------------------------------------------------------ car */

.car-anchor {
  /* 56% of the *stable* (address-bar-hidden) viewport height, matching
     S.carY = 0.56 * S.vh in road.js, where S.vh is read from a 100lvh probe.
     A plain % would follow the visible height as a phone's toolbar moves while
     the maths doesn't, putting the car off the road. road.js also sets this in
     px on every measure(); these lines only cover the first paint before it
     runs, and engines without lvh keep the 56vh line. */
  position: fixed; top: 56vh; top: 56lvh; left: 0; right: 0;
  z-index: 12; display: flex; justify-content: center;
  transform: translateY(-50%); pointer-events: none;
}
.car-drop  { animation: carIn 2.5s cubic-bezier(.28,1.12,.6,1) both; }
.car-track { will-change: transform; }
/* The car's shadow used to be `filter: drop-shadow()` on .car-idle. That filter
   wraps a WebGL canvas inside #car, which tick() translates AND rotates every
   frame while moving - a filter the compositor keeps as its own render
   surface and re-applies (a GPU blur pass) as the car moves. .car-shadow
   instead is a plain box-shadow on a static element inside the same moving
   layer: it's
   rasterised once, then just carried along by #car's transform as a bitmap -
   it still rotates/bobs with the car exactly as the drop-shadow did.
   isolation gives .car-idle its own stacking context so z-index:-1 can put
   the shadow behind the canvas/img without dropping below the road. */
.car-idle  { position: relative; isolation: isolate; }
/* Sized off .car-idle itself (insets in %), so it follows the car's width set
   in measure() and the height the canvas (Car3D.resize) or the fallback <img>
   gives it - no JS needed, and it survives fallback() swapping the canvas.
   A rounded rect with the car's rough top-down outline stands in for its
   real silhouette; offset/blur/alpha were tuned against screenshots of the
   old drop-shadow rather than copied (a box-shadow of a rect reads darker
   than the drop-shadow of a car outline with glass/mirror gaps, hence .35
   not .45, and 14px for the softer edge the outline gave).
   A box-shadow isn't painted under its own element's box, which would leave
   a hole under the car's glass and rounded ends - so the element is parked
   1000px above where the shadow belongs and the shadow is offset back down
   by the same amount, leaving the whole shadow drawn and nothing else. */
.car-shadow {
  position: absolute; z-index: -1; pointer-events: none;
  left: 2%; right: 2%; top: -1000px; bottom: 1000px;
  border-radius: 45% / 20%;
  box-shadow: 0 calc(1000px + 8px) 14px rgba(10,16,8,.35);
}
/* no car drawn yet (3D model still loading) -> no shadow either; road.js
   adds .has-car once the model or the fallback image is in */
.car-idle:not(.has-car) .car-shadow { display: none; }
.car-idle img { display: block; width: 100%; height: auto; transform: rotate(180deg); }
/* the 3D car's canvas is sized in JS (measure()) to match the painted
   image's old width/height contract - width comes from CAR_ROAD same as
   before, height from the model's own aspect ratio once it loads, since a
   canvas has no natural aspect ratio to derive from the way an <img> does */
.car-idle canvas { display: block; }

@keyframes carIn { from { transform: translate3d(0,-125vh,0); } to { transform: translate3d(0,0,0); } }

/* --------------------------------------------------------------- content */

.legs { position: relative; z-index: 10; pointer-events: none; }
.leg  { position: relative; height: var(--leg-h); }

/* ------------------------------------------------------------------ rail */

.rail {
  /* hidden for now - v3's stops/copy aren't finalised, so the dots don't
     track anything meaningful yet (wrong count, no visible active state) */
  display: none;
}
.rail i {
  display: block; width: 6px; height: 6px; border-radius: 50%;
  background: rgba(255,255,255,.5);
  /* the ribbon runs bright turquoise and pale sand under this rail, so the dots
     need their own contrast rather than borrowing the scene's */
  box-shadow: 0 0 0 1px rgba(10,18,8,.42), 0 1px 3px rgba(10,18,8,.55);
  transition: background .35s ease, transform .35s ease, box-shadow .35s ease;
}
.rail i.on {
  background: var(--gold); transform: scale(1.5);
  box-shadow: 0 0 0 1px rgba(10,18,8,.5), 0 0 9px rgba(232,198,106,.7);
}

/* The opening hint. Guests missed the old 1px line, so it's a glass pill with
   a gesture that shows the motion: a finger swiping up on touch screens, a
   wheel rolling on desktop. Tapping it nudges the drive forward (road.js). */
.cue {
  position: fixed; z-index: 22; left: 50%;
  bottom: calc(3vh + env(safe-area-inset-bottom, 0px) + 72px);
  display: inline-flex; align-items: center; gap: 12px;
  padding: 0 20px 0 16px; height: 52px; border-radius: 999px;
  font: inherit; font-family: "EB Garamond", Georgia, serif; color: #FFF6E8;
  white-space: nowrap; cursor: pointer;
  background: rgba(27, 36, 22, .5);
  border: 1px solid rgba(232, 198, 106, .45);
  backdrop-filter: blur(10px) saturate(130%); -webkit-backdrop-filter: blur(10px) saturate(130%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12), 0 8px 26px rgba(0, 0, 0, .35);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .45);
  transform: translate(-50%, 0);
  animation: cue-in .7s cubic-bezier(.2, .9, .25, 1.1) .6s backwards, cue-hop 4s ease-in-out 3s infinite;
  transition: opacity .4s ease, visibility 0s;
  -webkit-tap-highlight-color: transparent;
}
.cue.gone {
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .4s ease, visibility 0s linear .4s;
}
.cue:hover { border-color: var(--gold); }
@keyframes cue-in { from { opacity: 0; transform: translate(-50%, 14px); } }
/* a small hop every few seconds while it's still being ignored */
@keyframes cue-hop {
  0%, 84%, 100% { transform: translate(-50%, 0); }
  88% { transform: translate(-50%, -7px); }
  92% { transform: translate(-50%, 0); }
  95% { transform: translate(-50%, -3px); }
}
.cue-text { font-size: .95rem; letter-spacing: .18em; padding-left: .18em; text-transform: uppercase; font-weight: 500; }
.cue-gesture { width: 20px; height: 27px; flex: none; }
.cue-frame { fill: none; stroke: #FFF6E8; stroke-width: 1.6; opacity: .85; }
.cue-finger { fill: var(--gold); animation: cue-swipe 1.8s cubic-bezier(.5, 0, .3, 1) infinite; }
.cue-wheel { fill: var(--gold); animation: cue-wheel 1.6s ease-in-out infinite; }
@keyframes cue-swipe {
  0%   { transform: translateY(4px); opacity: 0; }
  15%  { opacity: 1; }
  70%  { transform: translateY(-12px); opacity: 1; }
  100% { transform: translateY(-12px); opacity: 0; }
}
@keyframes cue-wheel { 0% { transform: translateY(0); opacity: 1; } 70% { transform: translateY(6px); opacity: 0; } 100% { transform: translateY(0); opacity: 0; } }
.cue-chev {
  width: 16px; height: 10px; flex: none; fill: none; stroke: var(--gold); stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
  animation: cue 2.2s ease-in-out infinite;
}
@keyframes cue { 0%, 100% { opacity: .35; transform: translateY(-2px); } 50% { opacity: 1; transform: translateY(4px); } }
/* touch screens swipe, everything else scrolls */
.cue .cue-touch { display: none; }
@media (hover: none) and (pointer: coarse) {
  .cue .cue-touch { display: inline; }
  .cue .cue-mouse { display: none; }
}

/* Music toggle — a small floating control, top-right, in the same gold-on-
   dark language as the rest of the fixed UI. Shipped with no styling at
   all originally (a bare <button>♪</button> in normal document flow), which
   read as broken rather than as a control. safe-area-aware since it sits at
   a screen edge on a phone. */
.music-toggle {
  position: fixed; z-index: 23;
  top: calc(16px + env(safe-area-inset-top, 0px));
  right: calc(16px + env(safe-area-inset-right, 0px));
  width: 44px; height: 44px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgba(232, 198, 106, .45);
  background: rgba(27, 36, 22, .55);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  color: var(--gold);
  font-family: "EB Garamond", Georgia, serif; font-size: 1.1rem; line-height: 1;
  cursor: pointer;
  transition: background .25s ease, border-color .25s ease, transform .15s ease;
}
.music-toggle:hover { border-color: rgba(232, 198, 106, .8); }
.music-toggle:active { transform: scale(.92); }
.music-toggle[aria-pressed="true"] {
  background: rgba(232, 198, 106, .18);
  border-color: var(--gold);
}

@media (max-width: 640px) {
  .music-toggle { width: 40px; height: 40px; font-size: 1rem; }
}

/* Opening title — hidden at scroll 0, revealed by tick() once the very first
   small scroll happens (see the .03 * vh threshold there). Starts at opacity
   0 here so there's nothing to see before the first tick() runs; JS then owns
   it from there. Not part of the ribbon: it names the two people, not a place
   the road passes through, so it doesn't scroll away with the art the way
   section captions would. */
/* align-items: flex-start + this padding, not center, keeps the text clear
   of the car — it sits fixed at 56lvh (.car-anchor), and a
   true vertical center overlaps that line directly. 18% leaves a clear gap
   of open road between the bottom of the text and the car. */
.title {
  position: fixed; inset: 0; z-index: 21;
  display: flex; align-items: flex-start; justify-content: center;
  padding-top: 23vh;
  text-align: center; pointer-events: none;
  opacity: 0; will-change: opacity;
  transition: opacity .6s ease;
}
.title h1 {
  margin: 0; font-family: "Mrs Saint Delafield", "EB Garamond", cursive, serif;
  font-weight: 400; font-size: clamp(4.6rem, 18vw, 10rem);
  line-height: 1.15;
  color: var(--text);
  text-shadow: var(--overlay-shadow);
}

.title .amp-word { display: none; }

/* Below this width, at the larger size above, "Bhavya & Ramcharan" no longer
   fits one line without shrinking the type back down to fight for room — so
   it breaks onto three lines instead: "Bhavya", "and", "Ramcharan" — the
   ampersand swapped for the word, which reads better standing alone than a
   lone "&" does. */
@media (max-width: 640px) {
  .title .first-name, .title .second-name, .title .amp-word { display: block; }
  .title .amp { display: none; }
  .title .amp-word {
    font-size: 0.4em; letter-spacing: .04em; margin: 0.15em 0;
  }
}

/* Event details — same fixed overlay slot as .title, one leg later: the
   title covers the opening two legs (garden-lead + garden) plus the beach
   leg, fades out once that beach leg (garden-beach.png, leg index 2)
   finishes, and this fades in behind it in the same spot — except while the
   dam leg (DAM_LEG in road.js) or the very last leg is active, where
   .dam-caption or #ending take the slot instead and this hides too. See
   tick() for the full onDam / onLast / TITLE_LAST_LEG split. Content is the
   three real events (venue + per-event date/time). */
/* Anchored from the top with padding rather than centered — .details has
   more vertical content than .title (venue line + 3 event rows), and
   centering it would let the bottom rows creep toward the car's fixed 56%
   line on shorter viewports. 12vh clears it with room to spare even at
   this block's tallest (three events, mobile line-wrap). */
.details {
  position: fixed; inset: 0; z-index: 21;
  display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
  padding-top: 16vh;
  gap: 1.1em;
  text-align: center; pointer-events: none;
  opacity: 0; will-change: opacity;
  transition: opacity .6s ease;
}

/* Venue — its own fixed layer, pinned to the bottom of the viewport rather
   than stacked into .details, so it reads as anchored to the device's own
   bottom edge (home indicator / gesture bar territory on a phone) instead of
   scaling with how tall the events list happens to be. The safe-area inset
   is what actually keeps it clear of that system UI — a bare vh value sits
   partly behind it on notched/gesture-nav phones, which is why an earlier
   version of this (bottom: 6vh, no inset) wasn't visible on a real device
   despite working in a plain desktop browser check. Same visibility timing
   as .details (tick() drives both together). */
.venue {
  position: fixed; left: 0; right: 0; z-index: 21;
  bottom: calc(3vh + env(safe-area-inset-bottom, 0px));
  text-align: center; pointer-events: none;
  opacity: 0; will-change: opacity;
  transition: opacity .6s ease;
}
.venue p { margin: 0; }
.details p { margin: 0; }
/* Typography follows the original brief: the venue is small uppercase white
   letterspaced 0.36em, ceremony names EB Garamond 500, times EB Garamond
   italic warm off-white (chats/chat1.md). */
.details-place {
  font-family: "EB Garamond", Georgia, serif;
  font-weight: 600; font-size: clamp(0.975rem, 3.6vw, 1.125rem);
  line-height: 1.7;
  text-transform: uppercase; letter-spacing: 0.36em;
  color: var(--text);
  text-shadow: var(--overlay-shadow);
}
.details-events {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 0.55em;
}
.details-events li {
  display: flex; flex-direction: column; align-items: center; gap: 0.1em;
}
.details-ceremony {
  font-family: "EB Garamond", Georgia, serif;
  font-weight: 500; font-size: clamp(1.8rem, 6.5vw, 2.2rem);
  color: var(--text);
  text-shadow: var(--overlay-shadow);
}
.details-when {
  font-family: "EB Garamond", Georgia, serif;
  font-style: italic; font-weight: 400; font-size: clamp(1.05rem, 3.6vw, 1.2rem);
  color: #FFF6E8;
  text-shadow: var(--overlay-shadow);
}

/* Per-section caption — shown only while the dam-reservoir.png leg (index 5,
   see DAM_LEG in road.js) is on screen, independent of the title/.details
   handoff which is already resolved by then. Same top-anchored, car-clearing
   layout as .details, right down to reusing its .details-events/li markup
   for the one event row — a bare pair of <p> tags here read visibly
   cramped and inconsistent against the Vizag panel's proper row spacing. */
.dam-caption {
  position: fixed; inset: 0; z-index: 21;
  display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
  padding-top: 16vh;
  text-align: center; pointer-events: none;
  opacity: 0; will-change: opacity;
  transition: opacity .6s ease;
}
.dam-caption p { margin: 0; }

/* ------------------------------------------------------- event sheets */

/* The glass pop-ups the painted venue callouts open (see Sheets in
   road.js). Frosted, translucent and light-edged over the scene: a
   bottom sheet on phones, a centred card on wider screens. */
html.sheet-open { overflow: hidden; }

/* The event card: dark olive glass with gold hairlines, a centred title over
   an ornament rule, and a sakura branch that creeps in from the top-left and
   bottom-right corners every time it opens (built in road.js, Sheets.bloom).
   The dialog itself clips (rounded corners crop the branches) while
   .sheet-body scrolls, so the branches stay pinned to the card's corners
   instead of scrolling away with the content. */
.sheet {
  --ivory: #F3EBDD;
  --ivory-soft: rgba(243, 235, 221, .72);
  --hair: rgba(243, 235, 221, .2);
  --gold-hair: rgba(232, 198, 106, .4);
  width: min(420px, calc(100vw - 24px));
  margin: auto;
  padding: 0;
  border: 1px solid var(--gold-hair);
  border-radius: 28px;
  background: linear-gradient(165deg, rgba(78, 86, 42, .66), rgba(36, 42, 19, .78));
  backdrop-filter: blur(22px) saturate(140%);
  -webkit-backdrop-filter: blur(22px) saturate(140%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .14),
    inset 0 0 0 5px rgba(232, 198, 106, .04),
    0 30px 70px rgba(0, 0, 0, .5);
  color: var(--ivory);
  font-family: "EB Garamond", Georgia, serif;
  overflow: hidden;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .3);
  animation: sheet-in .5s cubic-bezier(.2, .9, .25, 1.05);
}
.sheet::backdrop {
  background: rgba(10, 14, 8, .4);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  animation: sheet-fade .3s ease;
}
.sheet.closing { animation: sheet-out .22s ease forwards; }
.sheet.closing::backdrop { animation: sheet-fade .22s ease reverse forwards; }

@keyframes sheet-in  { from { opacity: 0; transform: translateY(24px) scale(.96); } }
@keyframes sheet-out { to   { opacity: 0; transform: translateY(16px) scale(.97); } }
@keyframes sheet-fade { from { opacity: 0; } }

.sheet-body {
  position: relative; z-index: 1;
  max-height: min(86vh, 720px);
  overflow-y: auto; overscroll-behavior: contain;
  padding: 46px 22px 22px;
}

.sheet-head { text-align: center; padding: 0 30px; }
.sheet-title {
  margin: 0; font-weight: 500; line-height: 1.08;
  font-size: clamp(1.9rem, 8vw, 2.3rem);
}
.sheet-sub {
  margin: 8px 0 0; color: var(--ivory-soft);
  font-size: .78rem; letter-spacing: .34em; text-indent: .34em; text-transform: uppercase;
}
.sheet-rule {
  display: flex; align-items: center; gap: 10px;
  width: 78%; margin: 12px auto 0; color: var(--gold);
}
.sheet-rule::before, .sheet-rule::after {
  content: ""; flex: 1; height: 1px;
  background: linear-gradient(90deg, transparent, var(--gold-hair));
}
.sheet-rule::after { background: linear-gradient(270deg, transparent, var(--gold-hair)); }
.sheet-rule svg { flex: none; width: 22px; height: 17px; }

.sheet-close {
  position: absolute; z-index: 3; top: 14px; right: 14px;
  width: 38px; height: 38px; border-radius: 50%;
  border: 1px solid var(--hair); background: rgba(0, 0, 0, .18);
  color: var(--ivory); font: inherit; font-size: 1.5rem; line-height: 1; cursor: pointer;
}
.sheet-close:hover { background: rgba(255, 255, 255, .12); }
.sheet-close:focus { outline: none; }
.sheet-close:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

/* the venue's events, one after another, split by hairlines: line icon on
   the left, then name and time, a single dress-code colour where there is
   one, and the one-line note */
.sheet-events { list-style: none; margin: 20px 0 0; padding: 0; }
.sheet-ev {
  display: grid; grid-template-columns: 44px 1fr; column-gap: 16px;
  padding: 16px 0; border-top: 1px solid var(--hair);
}
.sheet-ev:first-child { border-top: 0; padding-top: 6px; }
.sheet-ev:last-child { padding-bottom: 4px; }
.sheet-ev::before {
  content: ""; grid-row: 1 / span 3; width: 44px; height: 44px; margin-top: 2px;
  background: var(--gold);
  -webkit-mask: var(--art) center / contain no-repeat;
          mask: var(--art) center / contain no-repeat;
}
.sheet-ev > * { grid-column: 2; }
.sheet-ev[data-art="vinyl"] { --art: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48' fill='none' stroke='black' stroke-width='1.3' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='20' cy='27' r='16'/%3E%3Ccircle cx='20' cy='27' r='10' stroke-opacity='.55'/%3E%3Ccircle cx='20' cy='27' r='2.6'/%3E%3Cpath d='M43 5l-7 17-5 3'/%3E%3Cpath d='M40 31v9a2.6 2.6 0 1 1-1.4-2.3M40 31l5-1.6'/%3E%3C/svg%3E"); }
.sheet-ev[data-art="sun"]   { --art: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48' fill='none' stroke='black' stroke-width='1.3' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='24' cy='24' r='7'/%3E%3Ccircle cx='24' cy='24' r='2' stroke-opacity='.6'/%3E%3Cpath d='M24 5v6M24 37v6M5 24h6M37 24h6M10.6 10.6l4.2 4.2M33.2 33.2l4.2 4.2M10.6 37.4l4.2-4.2M33.2 14.8l4.2-4.2'/%3E%3C/svg%3E"); }
.sheet-ev[data-art="rings"] { --art: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48' fill='none' stroke='black' stroke-width='1.3' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='18' cy='29' r='10.5'/%3E%3Ccircle cx='30' cy='29' r='10.5'/%3E%3Cpath d='M26.8 14.5L30 10l3.2 4.5L30 18z'/%3E%3C/svg%3E"); }
.sheet-ev[data-art="thali"] { --art: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48' fill='none' stroke='black' stroke-width='1.3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cellipse cx='24' cy='33' rx='20' ry='7'/%3E%3Cellipse cx='24' cy='32' rx='12' ry='3.6' stroke-opacity='.6'/%3E%3Cpath d='M17 24c-2-2.6 2-4.4 0-7.6M24 23c-2-2.6 2-4.4 0-7.6M31 24c-2-2.6 2-4.4 0-7.6'/%3E%3C/svg%3E"); }
.ev-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.ev-name { min-width: 0; margin: 0; font-weight: 500; font-size: 1.35rem; line-height: 1.2; text-wrap: balance; }
.ev-when {
  flex: none; margin: 0; color: var(--gold);
  font-size: .76rem; letter-spacing: .16em; text-transform: uppercase;
}
/* the dress code is the one thing guests act on, so it gets a chip tinted in
   its own colour rather than another line of small text */
.ev-dress {
  justify-self: start;
  display: inline-flex; align-items: center; gap: 10px;
  margin: 10px 0 0; padding: 5px 14px 5px 5px; border-radius: 999px;
  background: rgba(0, 0, 0, .18);
  background: color-mix(in srgb, var(--c) 16%, rgba(0, 0, 0, .18));
  border: 1px solid color-mix(in srgb, var(--c) 55%, transparent);
  color: var(--ivory); font-size: 1.08rem; line-height: 1.2;
}
.ev-dress::before {
  content: ""; flex: none; width: 24px; height: 24px; border-radius: 50%;
  background: var(--c); border: 1px solid rgba(255, 255, 255, .6);
  box-shadow: 0 1px 4px rgba(0, 0, 0, .35);
}
.ev-dress span {
  color: var(--ivory-soft); font-size: .68rem; letter-spacing: .18em; text-transform: uppercase;
}
.ev-note { margin: 8px 0 0; font-style: italic; font-size: 1rem; line-height: 1.3; color: rgba(243, 235, 221, .6); }

.sheet-directions {
  display: flex; align-items: center; justify-content: center; gap: 14px;
  margin-top: 22px; padding: 15px; border-radius: 18px;
  text-decoration: none; color: var(--ivory);
  background: linear-gradient(180deg, #6B2C16, #4B1B0C);
  border: 1px solid rgba(232, 198, 106, .3);
  font-size: 1.2rem;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12), 0 8px 20px rgba(0, 0, 0, .3);
  transition: transform .15s ease;
}
.sheet-directions::after { content: "\2192"; font-size: 1.3rem; transition: transform .25s ease; }
.sheet-directions:hover::after { transform: translateX(4px); }
.sheet-directions:active { transform: scale(.98); }

/* ---- the sakura branches. One drawing (Sheets.bloom in road.js), placed at
   the top-left and turned 180deg for the bottom-right. The stems draw
   themselves in (stroke-dashoffset over pathLength=1), each blossom pops
   open as the stem reaches it (--d, from its distance to the corner), then
   the whole branch sways. A dialog is display:none while closed, so every
   open replays the lot from the start. */
.sheet-bloom {
  position: absolute; z-index: 2; pointer-events: none;
  width: min(54%, 190px); aspect-ratio: 1;
}
.sheet-bloom--tl { top: -3px; left: -3px; }
.sheet-bloom--br { bottom: -3px; right: -3px; transform: rotate(180deg); }
.sheet-bloom svg {
  display: block; width: 100%; height: 100%; overflow: visible;
  transform-origin: 0 0;
  animation: bloom-sway 5s ease-in-out 1.9s infinite alternate;
}
.sheet-bloom .stem {
  fill: none; stroke: #4A2C20; stroke-linecap: round;
  stroke-dasharray: 1; stroke-dashoffset: 1;
  animation: bloom-creep 1.1s cubic-bezier(.45, 0, .25, 1) var(--d) forwards;
}
.sheet-bloom .bl {
  transform-box: fill-box; transform-origin: center;
  opacity: 0; transform: scale(0) rotate(-70deg);
  animation: bloom-open .7s cubic-bezier(.3, 1.5, .5, 1) var(--d) forwards;
}
.sheet-bloom .fall {
  opacity: 0;
  animation: bloom-fall 6s linear var(--d) infinite;
}
@keyframes bloom-creep { to { stroke-dashoffset: 0; } }
@keyframes bloom-open  { to { opacity: 1; transform: none; } }
@keyframes bloom-sway  { to { transform: rotate(1.8deg); } }
@keyframes bloom-fall {
  0%   { opacity: 0; transform: translate(0, 0) rotate(0); }
  12%  { opacity: .9; }
  100% { opacity: 0; transform: translate(70px, 150px) rotate(320deg); }
}

/* browsers without backdrop-filter get a solid-enough tint to stay legible */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .sheet { background: rgba(36, 42, 19, .96); }
}

/* phones: the card rises from the bottom edge of the screen into place and
   sinks back down when closed, like a bottom sheet, but stays a floating card */
@media (max-width: 640px) {
  .sheet-body { padding: 46px 18px 20px; }
  .sheet { animation: sheet-rise .55s cubic-bezier(.2, .9, .25, 1); }
  .sheet.closing { animation: sheet-sink .22s cubic-bezier(.5, 0, .75, 0) forwards; }
}
@keyframes sheet-rise { from { transform: translateY(100vh); } }
@keyframes sheet-sink { to   { transform: translateY(100vh); } }

/* ------------------------------------------------------- reduced motion */

@media (prefers-reduced-motion: reduce) {
  .car-drop, .cue, .cue-chev, .cue-finger, .cue-wheel { animation: none !important; }
  .streaks { display: none !important; }
  .title, .details, .dam-caption, .venue, .music-toggle { transition: none; }
  .sheet, .sheet::backdrop { animation: none !important; transition: none !important; }
  .sheet-bloom svg, .sheet-bloom .stem, .sheet-bloom .bl { animation: none !important; }
  .sheet-bloom .stem { stroke-dashoffset: 0; }
  .sheet-bloom .bl { opacity: 1; transform: none; }
  .sheet-bloom .fall { display: none; }
}
