/* ============================================================
   The Sacred Garden — wedding invitation
   Phase 0: reset, design tokens, responsive shell, scaffold bands.
   Section styles arrive in their own phases.
   ============================================================ */

/* ---------- 1 · Tokens ------------------------------------ */

:root {
  /* Palette — REFERENCE.md §2. Nothing here is pure black or white. */
  --cream:        #F7EFDF;
  --paper:        #EFE7D6;
  --gold:         #B08D3E;
  --gold-light:   #D4B36A;
  --maroon:       #6B1220;
  --maroon-deep:  #4A0C16;
  --ink:          #4A3F35;
  --sage:         #8A9A7B;
  --blush:        #D9A9A0;

  /* Derived tints — for rules, shadows, washes. */
  --ink-soft:     color-mix(in srgb, var(--ink) 68%, transparent);
  --gold-soft:    color-mix(in srgb, var(--gold) 32%, transparent);
  --shadow-warm:  color-mix(in srgb, var(--maroon-deep) 14%, transparent);

  /* Type families */
  --font-script:  "Pinyon Script", cursive;   /* candidate — see _fonts.html */
  --font-display: "Cinzel", Georgia, serif;   /* caps, numerals */
  --font-body:    "Ovo", Georgia, serif;

  /* Type scale — fluid, mobile floor to desktop ceiling.
     Script runs deliberately large; it is the personality of the design. */
  --t-script-xl:  clamp(4.25rem, 17vw, 7.5rem);   /* couple's names */
  /* Section headings must clear their flanking flourishes on one line.
     The ceiling is set by the longest heading, "Confirm Your Attendance":
     at 2.6rem it measures ~392px, which fits a 556px desktop column with
     two 56px flourishes and their gaps. Raise this and that heading wraps
     and squeezes its ornaments to nothing. */
  --t-script-lg:  clamp(1.75rem, 8vw, 2.6rem);    /* section headings */
  --t-script-md:  clamp(1.75rem, 6.5vw, 2.5rem);  /* "Scroll down" */
  --t-display-lg: clamp(2.25rem, 9vw, 3.5rem);    /* countdown numerals */
  --t-display-md: clamp(1rem, 3.6vw, 1.25rem);    /* venue name */
  --t-caps:       clamp(0.7rem, 2.9vw, 0.8rem);   /* tracked gold caps */
  --t-body:       clamp(0.95rem, 3.7vw, 1.0625rem);
  --t-small:      clamp(0.75rem, 3vw, 0.8125rem);

  --track-caps:   0.28em;   /* wide tracking on all-caps gold labels */
  --leading-body: 1.75;

  /* Rhythm */
  --space-1: 0.5rem;
  --space-2: 1rem;
  --space-3: 1.5rem;
  --space-4: 2.5rem;
  --space-5: 4rem;
  --space-6: 6rem;

  /* Layout */
  --col:        480px;   /* the intimate column, phone-width by intent */
  --col-wide:   620px;   /* desktop breathes a little, never sprawls */
  --gutter:     clamp(1.25rem, 5vw, 2rem);

  /* Motion — Phase 4 uses these for every reveal. */
  --ease-out:   cubic-bezier(0.22, 0.61, 0.36, 1);
  --dur-reveal: 0.7s;
  --stagger:    120ms;
}

/* ---------- 2 · Reset ------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* `clip`, not `hidden`, and on the root as well as the body.

     The invite card's florals deliberately overhang the screen edges. With
     only `overflow-x: hidden` on <body> that overhang still widened the
     initial containing block — innerWidth read 449 on a 390px phone — and
     anything `position: fixed` then anchored to 449 instead of 390. The
     music button's right edge landed at 429, i.e. two thirds of it off the
     side of the screen.

     `clip` does not create a scroll container, so the overflow cannot
     extend the scrollable area. Measured after: innerWidth 390,
     scrollWidth 390, button right edge 370 — and the floral still
     overhangs to -59 exactly as before. */
  overflow-x: clip;
}

body,
h1, h2, h3, h4, p, figure, blockquote, dl, dd, ul, ol {
  margin: 0;
  padding: 0;
}

ul[role="list"], ol[role="list"] { list-style: none; }

img, picture, svg, video {
  display: block;
  max-width: 100%;
  height: auto;
}

input, button, textarea, select {
  font: inherit;
  color: inherit;
}

button {
  background: none;
  border: 0;
  cursor: pointer;
}

a { color: inherit; }

:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 3px;
}

/* ---------- 3 · Ground ------------------------------------ */

body {
  background: var(--cream);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--t-body);
  line-height: var(--leading-body);
  /* Paired with the same on <html> — see the note there for why `clip`. */
  overflow-x: clip;
  /* Phase 2 adds .is-locked here while the envelope is closed. */
}

/* The single content column. Mobile-first: full width minus gutters,
   capped so it stays intimate. Desktop widens modestly instead of
   leaving a phone-shaped strip stranded in empty cream. */
.col {
  width: 100%;
  max-width: var(--col);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

@media (min-width: 768px) {
  .col { max-width: var(--col-wide); }
}

/* ---------- 4 · Utilities --------------------------------- */

.script  { font-family: var(--font-script); color: var(--gold); line-height: 1.15; }
.display { font-family: var(--font-display); }

.caps {
  font-family: var(--font-display);
  font-size: var(--t-caps);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--gold);
  /* tracking adds a trailing gap; pull it back so text stays optically centered */
  text-indent: var(--track-caps);
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ============================================================
   5 · Design primitives (Phase 1)

   Every ornament on this site is built here, once. Nothing is
   copied from the reference — the wax, the torn edge, and the
   filigree are all CSS gradients and inline SVG.
   ============================================================ */

/* ---------- 5.1 · Headings -------------------------------- */

.heading {
  font-family: var(--font-script);
  color: var(--gold);
  font-size: var(--t-script-lg);
  font-weight: 400;
  line-height: 1.15;
  text-align: center;
}

.heading--names {
  font-size: var(--t-script-xl);
  line-height: 1.02;
}

/* The `&` between the names sits smaller and tucked in. */
.heading--names .amp {
  display: block;
  font-size: 0.42em;
  margin-block: -0.08em;
}

/* Gold filigree flanking a script heading. Requires the heading text to be
   wrapped in a <span> — a bare text node becomes an anonymous flex item,
   which cannot be told to hold its ground, so it wraps to two lines and
   leaves the ornaments straddling both.

   The ornaments take only the space the text leaves over (basis 0, grow 1),
   so a long heading like "Schedule of Events" stays on one line and the
   flourishes quietly get shorter instead. */
.heading--flourished {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(0.4rem, 2.5vw, 1rem);
}

.heading--flourished > span {
  flex: 0 1 auto;
  min-width: 0;
}

.heading--flourished::before,
.heading--flourished::after {
  content: "";
  flex: 1 1 0;
  /* A floor, not zero. Taking only leftover space means a long heading can
     starve these down to 0px and the ornaments vanish with no sign that
     anything is wrong — which is exactly what happened at desktop width.
     The floor scales with the viewport: a fixed 40px is generous on a
     laptop but wide enough on a phone to push the heading onto two lines,
     which is the opposite problem. 5.5vw rather than 8vw: a heading inside
     a card (padding on both the card and its section) has noticeably less
     room than one sitting straight in .section__inner, and 8vw was enough
     to wrap "Schedule of Events" back onto two lines the moment it had a
     card's padding to contend with — the exact failure this floor exists
     to prevent, just from the other side. This still saturates at the
     40px cap well before desktop width, so the original fix is unaffected. */
  min-width: clamp(26px, 5.5vw, 40px);
  max-width: 92px;
  height: 20px;
  background: var(--filigree) center / contain no-repeat;
}

.heading--flourished::before { transform: scaleX(-1); }

/* Below this a flourish is a stub, not an ornament — drop it entirely. */
@media (max-width: 360px) {
  .heading--flourished::before,
  .heading--flourished::after { display: none; }
}

/* The Schedule heading uses the reference's own photographed gold
   scrollwork instead of the shared hand-drawn --filigree. Left and right
   are two distinct images, each already drawn facing the right way — not
   a mirror of one another — so the generic scaleX(-1) mirror is cancelled. */
.heading--schedule::before {
  background-image: url("../assets/img/schedule-flourish-left.png");
  transform: none;
}

.heading--schedule::after {
  background-image: url("../assets/img/schedule-flourish-right.png");
}

/* ---------- 5.2 · Wax seal --------------------------------
   One component, two dresses: a script monogram (R&Z) or
   tracked Cinzel caps (RSVP). Set --seal-size to scale it.

   The lobed edge is a clip-path polygon, so an inset box-shadow
   would trace the element's *rectangle* and get sliced off by the
   clip. All the depth here is radial gradients instead, which
   follow the wax shape whatever it is.
   ---------------------------------------------------------- */

.seal {
  --seal-size: 118px;

  position: relative;
  display: grid;
  place-items: center;
  width: var(--seal-size);
  aspect-ratio: 1;
  isolation: isolate;

  clip-path: polygon(
    99.13% 50.00%, 97.10% 52.85%, 94.83% 55.44%, 93.33% 57.94%, 90.89% 60.08%,
    91.46% 62.92%, 92.57% 66.14%, 93.89% 69.75%, 93.05% 72.59%, 91.35% 75.00%,
    87.08% 75.60%, 84.44% 76.98%, 82.16% 78.49%, 81.14% 81.14%, 80.52% 84.45%,
    79.51% 87.66%, 77.80% 90.28%, 74.71% 90.87%, 71.26% 90.50%, 67.64% 89.19%,
    65.20% 90.07%, 63.11% 92.07%, 60.90% 94.23%, 58.58% 96.83%, 55.84% 98.14%,
    52.87% 97.43%, 50.00% 95.33%, 47.37% 93.53%, 44.90% 91.96%, 42.24% 92.34%,
    38.95% 94.83%, 35.80% 95.56%, 32.59% 95.92%, 30.53% 93.26%, 29.05% 89.92%,
    27.71% 86.87%, 26.06% 84.68%, 23.20% 84.21%, 19.89% 83.98%, 15.87% 84.13%,
    13.95% 81.94%, 12.21% 79.61%, 12.05% 76.19%, 12.68% 72.56%, 12.60% 69.63%,
    10.72% 67.68%,  7.56% 66.10%,  4.63% 64.14%,  2.44% 61.72%,  3.18% 58.58%,
     4.91% 55.48%,  6.61% 52.62%,  7.61% 50.00%,  6.30% 47.36%,  4.18% 44.44%,
     3.62% 41.50%,  3.00% 38.42%,  4.97% 35.97%,  7.83% 34.01%, 10.05% 32.02%,
    12.25% 30.19%, 13.09% 27.69%, 12.42% 24.06%, 12.79% 20.85%, 13.21% 17.41%,
    16.11% 16.11%, 19.37% 15.43%, 22.90% 15.41%, 25.92% 15.12%, 27.63% 12.99%,
    28.75%  9.52%, 30.49%  6.64%, 32.88%  4.86%, 35.72%  4.17%, 39.18%  6.11%,
    42.24%  7.68%, 44.86%  7.67%, 47.40%  6.94%, 50.00%  4.67%, 52.85%  2.88%,
    55.81%  2.17%, 58.60%  3.08%, 61.03%  5.24%, 62.84%  8.79%, 65.22%  9.86%,
    67.71% 10.64%, 71.15%  9.71%, 74.62%  9.28%, 77.38% 10.33%, 79.19% 12.74%,
    79.89% 16.26%, 80.46% 19.54%, 82.07% 21.59%, 84.56% 22.92%, 88.01% 23.76%,
    90.64% 25.43%, 92.94% 27.47%, 93.41% 30.46%, 92.82% 33.76%, 91.06% 37.21%,
    90.81% 39.94%, 93.26% 42.07%, 94.93% 44.54%, 97.80% 47.11%
  );

  background:
    /* top-left highlight, as if lit from a window */
    radial-gradient(circle at 33% 27%,
      color-mix(in srgb, var(--maroon) 78%, #fff) 0%,
      transparent 46%),
    /* the dome: lighter in the middle, deepening to the rim */
    radial-gradient(circle at 50% 46%,
      color-mix(in srgb, var(--maroon) 92%, #fff) 0%,
      var(--maroon) 40%,
      var(--maroon-deep) 88%);

  filter: drop-shadow(0 3px 5px var(--shadow-warm));
}

/* The stamped recess — a true circle, so a real inset shadow works. */
.seal::before {
  content: "";
  position: absolute;
  inset: 11%;
  border-radius: 50%;
  box-shadow:
    inset 0 2px 3px color-mix(in srgb, var(--maroon-deep) 85%, transparent),
    inset 0 -1px 1px color-mix(in srgb, #fff 22%, transparent);
}

.seal__mark {
  position: relative;
  z-index: 1;
  color: var(--gold-light);
  text-shadow: 0 1px 1px color-mix(in srgb, var(--maroon-deep) 70%, transparent);
  line-height: 1;
}

/* Dress 1 — script monogram, e.g. R&Z */
.seal--monogram .seal__mark {
  font-family: var(--font-script);
  font-size: calc(var(--seal-size) * 0.38);
  /* script glyphs sit high in the em box; nudge them onto the optical centre */
  margin-top: calc(var(--seal-size) * 0.04);
}

/* Dress 2 — tracked caps, e.g. RSVP */
.seal--label .seal__mark {
  font-family: var(--font-display);
  font-size: calc(var(--seal-size) * 0.15);
  letter-spacing: 0.18em;
  text-indent: 0.18em;
  text-transform: uppercase;
}

/* A hairline flourish under a label seal. */
.seal--label .seal__mark::after {
  content: "";
  display: block;
  width: 46%;
  height: 3px;
  margin: 0.35em auto 0;
  background: var(--seal-rule) center / contain no-repeat;
  opacity: 0.85;
}

button.seal { padding: 0; transition: transform 0.25s var(--ease-out); }
button.seal:hover { transform: translateY(-2px) scale(1.02); }
button.seal:active { transform: translateY(0) scale(0.99); }

/* ---------- 5.3 · Torn-paper card -------------------------
   Deckled top and bottom edges, cut with a tiling SVG mask so
   the card is genuinely transparent at the tear — it reads
   correctly over any ground, not just cream.
   ---------------------------------------------------------- */

.card {
  --tear: 14px;

  position: relative;
  padding: calc(var(--tear) + var(--space-4)) var(--space-3);
  background: var(--paper);

  -webkit-mask-image: var(--torn-top), var(--torn-bottom), linear-gradient(#000, #000);
          mask-image: var(--torn-top), var(--torn-bottom), linear-gradient(#000, #000);
  -webkit-mask-repeat: repeat-x, repeat-x, no-repeat;
          mask-repeat: repeat-x, repeat-x, no-repeat;
  -webkit-mask-position: top left, bottom left, center;
          mask-position: top left, bottom left, center;
  -webkit-mask-size: auto var(--tear), auto var(--tear), 100% calc(100% - (2 * var(--tear)));
          mask-size: auto var(--tear), auto var(--tear), 100% calc(100% - (2 * var(--tear)));
}

/* Paper grain. Sits under the content, above the fill. */
.card::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--paper-grain);
  opacity: 0.055;
  pointer-events: none;
}

.card > * { position: relative; }

/* ---------- 5.4 · Ornate frame ----------------------------
   Double gold rule with a filigree crest breaking the line at
   top and bottom centre. Wraps the map in Phase 8.
   ---------------------------------------------------------- */

.frame {
  position: relative;
  padding: 7px;
  border: 1px solid var(--gold);
  border-radius: 3px;
  background: var(--cream);
  box-shadow: inset 0 0 0 3px var(--cream), inset 0 0 0 4px var(--gold-soft);
}

.frame__inner {
  overflow: hidden;
  border-radius: 1px;
}

.frame::before,
.frame::after {
  content: "";
  position: absolute;
  left: 50%;
  width: 56px;
  height: 16px;
  translate: -50% 0;
  background: var(--cream) var(--crest) center / contain no-repeat;
}

.frame::before { top: -8px; }
.frame::after  { bottom: -8px; rotate: 180deg; }

/* ---------- 5.5 · Chevron + label cue ---------------------
   The `^` + "Tap to open" / "Scroll down" unit.
   ---------------------------------------------------------- */

.cue {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
}

.cue__chevron {
  width: 15px;
  height: 15px;
  border-top: 1px solid var(--gold);
  border-left: 1px solid var(--gold);
  rotate: 45deg;
  /* the rotated square hangs low; lift it back onto the baseline */
  margin-bottom: -3px;
}

.cue--down { flex-direction: column-reverse; }
.cue--down .cue__chevron { rotate: 225deg; margin-bottom: 0; margin-top: -3px; }

.cue__label { line-height: 1.2; }

/* ---------- 5.6 · Art slots -------------------------------
   Where the real florals and photos drop in. The dashed box is
   visible only while a slot is still empty.
   ---------------------------------------------------------- */

.slot {
  display: grid;
  place-items: center;
  min-height: 60px;
  padding: var(--space-1);
  border: 1px dashed var(--gold-soft);
  border-radius: 2px;
  font-family: var(--font-body);
  font-size: var(--t-small);
  line-height: 1.3;
  color: var(--ink-soft);
  text-align: center;
}

.slot:has(img) { border: 0; padding: 0; min-height: 0; }

/* ---------- 5.7 · Ornament sources ------------------------
   Inline SVG, kept together so the artwork is in one place.
   ---------------------------------------------------------- */

:root {
  /* Deckled tear, generated to tile seamlessly under repeat-x. */
  --torn-top: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 16'%3E%3Cpath fill='%23000' d='M0,16 L0,11.3 Q0,11.3 4,6.5 Q8,1.6 12,6.6 Q16,11.6 20,8 Q24,4.4 28,6.9 Q32,9.4 36,6.9 Q40,4.3 44,7.7 Q48,11 52,7.2 Q56,3.5 60,7 Q64,10.6 68,7 Q72,3.4 76,6.3 Q80,9.3 84,6.5 Q88,3.6 92,7.7 Q96,11.7 100,7.3 Q104,3 108,7.5 Q112,12.1 116,8.1 Q120,4.2 124,7.8 Q128,11.5 132,8.2 Q136,5 140,8.5 Q144,12 148,7.7 Q152,3.5 156,7.4 L160,11.3 L160,16 Z'/%3E%3C/svg%3E");

  --torn-bottom: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 16'%3E%3Cg transform='translate(0,16) scale(1,-1)'%3E%3Cpath fill='%23000' d='M0,16 L0,11.3 Q0,11.3 4,6.5 Q8,1.6 12,6.6 Q16,11.6 20,8 Q24,4.4 28,6.9 Q32,9.4 36,6.9 Q40,4.3 44,7.7 Q48,11 52,7.2 Q56,3.5 60,7 Q64,10.6 68,7 Q72,3.4 76,6.3 Q80,9.3 84,6.5 Q88,3.6 92,7.7 Q96,11.7 100,7.3 Q104,3 108,7.5 Q112,12.1 116,8.1 Q120,4.2 124,7.8 Q128,11.5 132,8.2 Q136,5 140,8.5 Q144,12 148,7.7 Q152,3.5 156,7.4 L160,11.3 L160,16 Z'/%3E%3C/g%3E%3C/svg%3E");

  /* Paper fibre. Turbulence is cheap here — one 140px tile, reused. */
  --paper-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 140 140'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23g)'/%3E%3C/svg%3E");

  /* Filigree: a tapering vine, a curl above and below, one leaf. */
  --filigree: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 52 20'%3E%3Cg fill='none' stroke='%23B08D3E' stroke-width='0.9' stroke-linecap='round'%3E%3Cpath d='M1,10 C14,10 22,4 33,4 C44,4 47.5,10 42,12 C37.5,13.6 35,9.5 39,7.8'/%3E%3Cpath d='M1,10 C14,10 22,16 33,16 C44,16 47.5,10 42,8 C37.5,6.4 35,10.5 39,12.2'/%3E%3C/g%3E%3Cpath fill='%23B08D3E' d='M7,10 C11,6.6 17,6.6 20.5,9.2 C16.5,12.2 10.5,12.2 7,10 Z'/%3E%3Ccircle cx='1.6' cy='10' r='1.5' fill='%23B08D3E'/%3E%3C/svg%3E");

  /* Frame crest: diamond with scrolled wings. */
  --crest: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 56 16'%3E%3Cg fill='none' stroke='%23B08D3E' stroke-width='1' stroke-linecap='round'%3E%3Cpath d='M2,8 C10,8 14,3 20,5'/%3E%3Cpath d='M54,8 C46,8 42,3 36,5'/%3E%3C/g%3E%3Cpath fill='%23B08D3E' d='M28,1.5 L32.5,8 L28,14.5 L23.5,8 Z'/%3E%3Ccircle cx='20.5' cy='8' r='1.4' fill='%23B08D3E'/%3E%3Ccircle cx='35.5' cy='8' r='1.4' fill='%23B08D3E'/%3E%3C/svg%3E");

  /* Envelope vine, drawn twice for the emboss. Tiles: the stem enters and
     leaves each edge at the midpoint, so neighbours join up. */
  --vine-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cg fill='%23ffffff' stroke='none' opacity='0.32'%3E%3Cpath fill='none' stroke='%23ffffff' stroke-width='1.1' d='M0,60 C22,44 38,44 60,60 C82,76 98,76 120,60'/%3E%3Cpath fill='none' stroke='%23ffffff' stroke-width='1.1' d='M60,0 C44,22 44,38 60,60 C76,82 76,98 60,120'/%3E%3Cpath d='M24,50 c4,-5 11,-5 15,0 c-4,5 -11,5 -15,0z' transform='rotate(-32 24 50)'/%3E%3Cpath d='M96,70 c4,-5 11,-5 15,0 c-4,5 -11,5 -15,0z' transform='rotate(148 96 70)'/%3E%3Cpath d='M50,24 c4,-5 11,-5 15,0 c-4,5 -11,5 -15,0z' transform='rotate(58 50 24)'/%3E%3Cpath d='M70,96 c4,-5 11,-5 15,0 c-4,5 -11,5 -15,0z' transform='rotate(238 70 96)'/%3E%3Ccircle cx='60' cy='60' r='3.2'/%3E%3Ccircle cx='0' cy='60' r='2'/%3E%3Ccircle cx='120' cy='60' r='2'/%3E%3Ccircle cx='60' cy='0' r='2'/%3E%3Ccircle cx='60' cy='120' r='2'/%3E%3C/g%3E%3C/svg%3E");

  --vine-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cg fill='%234A3F35' stroke='none' opacity='0.05'%3E%3Cpath fill='none' stroke='%234A3F35' stroke-width='1.1' d='M0,60 C22,44 38,44 60,60 C82,76 98,76 120,60'/%3E%3Cpath fill='none' stroke='%234A3F35' stroke-width='1.1' d='M60,0 C44,22 44,38 60,60 C76,82 76,98 60,120'/%3E%3Cpath d='M24,50 c4,-5 11,-5 15,0 c-4,5 -11,5 -15,0z' transform='rotate(-32 24 50)'/%3E%3Cpath d='M96,70 c4,-5 11,-5 15,0 c-4,5 -11,5 -15,0z' transform='rotate(148 96 70)'/%3E%3Cpath d='M50,24 c4,-5 11,-5 15,0 c-4,5 -11,5 -15,0z' transform='rotate(58 50 24)'/%3E%3Cpath d='M70,96 c4,-5 11,-5 15,0 c-4,5 -11,5 -15,0z' transform='rotate(238 70 96)'/%3E%3Ccircle cx='60' cy='60' r='3.2'/%3E%3Ccircle cx='0' cy='60' r='2'/%3E%3Ccircle cx='120' cy='60' r='2'/%3E%3Ccircle cx='60' cy='0' r='2'/%3E%3Ccircle cx='60' cy='120' r='2'/%3E%3C/g%3E%3C/svg%3E");

  /* Envelope fold creases, in envelope coordinates: the bottom flap folded
     up to the centre. The top flap's own edges are NOT drawn here — they
     belong to the flap, which rotates away, and would be left behind as a
     phantom V across the open envelope. preserveAspectRatio='none' stretches this to any envelope shape;
     vector-effect keeps the stroke a true hairline while it does. */
  --env-crease: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cg fill='none' stroke='%234A3F35' stroke-opacity='0.11' stroke-width='1' vector-effect='non-scaling-stroke'%3E%3Cpath d='M0,100 L50,69 L100,100'/%3E%3C/g%3E%3C/svg%3E");

  /* Rose bud crowning the schedule timeline. */
  --rosebud: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23B08D3E' stroke-width='1.1' stroke-linecap='round'%3E%3Cpath d='M12 22 L12 15'/%3E%3Cpath d='M12 17 C9 17 7.5 15.5 7.5 13.5 C10 13.5 12 15 12 17 Z' fill='%238A9A7B' stroke='%238A9A7B'/%3E%3Cpath d='M12 17 C15 17 16.5 15.5 16.5 13.5 C14 13.5 12 15 12 17 Z' fill='%238A9A7B' stroke='%238A9A7B'/%3E%3C/g%3E%3Cg fill='%23D9A9A0' stroke='%23B08D3E' stroke-width='0.7'%3E%3Cpath d='M12 2 C15.6 2 17.4 5 17.4 8.2 C17.4 11.4 15 13.6 12 13.6 C9 13.6 6.6 11.4 6.6 8.2 C6.6 5 8.4 2 12 2 Z'/%3E%3Cpath d='M12 3.4 C13.9 4.4 14.6 6.6 14.4 8.8 C14.2 11 13.2 12.4 12 13.2' fill='none'/%3E%3Cpath d='M12 3.4 C10.1 4.4 9.4 6.6 9.6 8.8 C9.8 11 10.8 12.4 12 13.2' fill='none'/%3E%3C/g%3E%3C/svg%3E");

  /* Music button icons. */
  /* The reference's own two shapes, to its exact geometry on a 24x24 box:
     a solid triangle 5,3 -> 19,12 -> 5,21, and two 4x14 bars at x=6 and
     x=14 with a 1px round. */
  --icon-play:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpolygon fill='%23000' points='5,3 19,12 5,21'/%3E%3C/svg%3E");
  --icon-pause: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='%23000'%3E%3Crect x='6' y='5' width='4' height='14' rx='1'/%3E%3Crect x='14' y='5' width='4' height='14' rx='1'/%3E%3C/g%3E%3C/svg%3E");

  /* Hairline rule under a seal label. */
  --seal-rule: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 4'%3E%3Cg fill='none' stroke='%23D4B36A' stroke-width='0.8' stroke-linecap='round'%3E%3Cpath d='M2,2 C8,2 10,0.6 14,2'/%3E%3Cpath d='M38,2 C32,2 30,0.6 26,2'/%3E%3C/g%3E%3Ccircle cx='20' cy='2' r='1.3' fill='%23D4B36A'/%3E%3C/svg%3E");
}

/* ============================================================
   6 · Envelope gate (Phase 2)

   Stage one is a still of the sealed envelope. Stage two is a
   short film of it opening, which fades out as it finishes.
   ============================================================ */

.gate {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: var(--cream);
  cursor: pointer;
  transition: opacity 0.8s var(--ease-out), visibility 0s linear 0.8s;
}

.gate[hidden] { display: none; }

/* ---------- 6.1 · Stage one: the sealed envelope ---------- */

.gate__still,
.gate__film {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
}

.gate__still {
  transition: opacity 0.9s var(--ease-out);
}

.gate__img,
.gate__video {
  width: 100%;
  height: 100%;
  /* contain, not cover: the envelope is the whole subject and cropping its
     edges off on a wide screen makes it read as wallpaper. */
  object-fit: contain;
}

/* The tap target is the whole screen; this button carries the label and
   gives keyboard users something real to focus. */
.gate__open {
  position: absolute;
  left: 50%;
  bottom: clamp(2rem, 9vh, 5rem);
  translate: -50% 0;
  padding: var(--space-2);
}

/* ---------- 6.2 · Stage two: the film --------------------- */

.gate__film {
  opacity: 0;
  transition: opacity 0.7s var(--ease-out);
  pointer-events: none;
}

.gate.is-playing .gate__still { opacity: 0; }
.gate.is-playing .gate__film  { opacity: 1; }

/* As the film ends it dissolves, revealing the invitation beneath. */
.gate.is-closing .gate__film { opacity: 0; }

.gate.is-open {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

/* Scroll stays locked until the envelope is gone. position:fixed rather
   than overflow:hidden — iOS Safari scrolls the body regardless of the
   latter. The gate opens at scroll 0, so nothing needs restoring. */
body.is-gated {
  position: fixed;
  inset: 0;
  width: 100%;
  overflow: hidden;
}

/* Dignified instead of dramatic: no film at all, just a fade. */
@media (prefers-reduced-motion: reduce) {
  .gate,
  .gate__still,
  .gate__film { transition-duration: 0.35s; }
}

/* ============================================================
   7 · Hero (Phase 3)

   One painted scene — arch, columns, vines, lake, swans — with
   the couple's words as live text on top. Type is sized in cqw
   so the words keep their proportions against the painting at
   every panel width.
   ============================================================ */

.hero {
  display: grid;
  place-items: center;
  min-height: 100vh;
  min-height: 100svh;
  /* No padding: the panel below is sized to fill this box exactly, so any
     padding here would leave a cream sliver top and bottom that the panel
     could never close. */
  padding: 0;
}

.hero:focus { outline: none; }   /* focus lands here from the gate, silently */

.hero__panel {
  position: relative;
  width: 100%;
  /* On a phone this is already ~100vw, so max-width has no effect there;
     it only caps the panel on wide screens, where .hero centers it and
     the cream either side is deliberate framing, not a gap to close.
     The cap is 78.3% of the invitation card's desktop width (556px), the
     ratio the reference holds between its hero video and its card at
     every breakpoint: the torn card is meant to be visibly wider than the
     photo and jut out past it either side. Below ~436px wide the cap
     stops applying and the photo goes full-bleed, which is how the
     reference reads on a phone too (there both its photo and its card are
     fixed widths that simply overflow the screen). All the hero type is
     sized in cqw against this panel, so narrowing it scales the whole
     composition rather than breaking it. */
  max-width: 436px;
  /* Height is pinned to the full viewport, not the video's own aspect
     ratio — an aspect-ratio box only matches the screen by coincidence,
     and everywhere else it leaves cream showing above and below. Filling
     the height outright and letting object-fit: cover crop the sides
     instead is what keeps the top and bottom flush on every screen. */
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  container-type: inline-size;
}

.hero__bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* Positions mirror where the original artwork had its text, measured off
   the source: eyebrow 17.7%, names 36.5-61.5%, cue 69.8% of the height. */
.hero__words,
.hero__names,
.hero__cue {
  position: absolute;
  left: 50%;
  translate: -50% 0;
  z-index: 2;
  width: 84%;
  text-align: center;
}

.hero__words { top: 16.4%; }

.hero__eyebrow {
  font-size: 6.2cqw;
  line-height: 1;
}

.hero__date {
  font-size: 4.6cqw;
  letter-spacing: 0.04em;
  color: var(--gold);
  margin-top: 0.35em;
}

.hero__names {
  top: 33%;
  font-size: 15.5cqw;
  line-height: 1.06;
  margin: 0;
}

.hero__names .amp {
  font-size: 0.42em;
  margin-block: 0.02em;
}

.hero__cue { top: 67.5%; }

.hero__cue .cue__label { font-size: 4.4cqw; }

.hero__cue .cue__chevron {
  width: 3.4cqw;
  height: 3.4cqw;
  border-width: 1.5px;
}

/* ============================================================
   8 · Scroll reveals (Phase 4)

   Built once, used by every section after it. Mark an element
   `data-reveal`, or a container `data-reveal-stagger` to cascade
   its children one after another.
   ============================================================ */

/* Everything here hangs off `.js`, set by an inline script in <head>.
   Keyed the other way round, a script that fails to load would leave the
   whole invitation invisible — the content must never depend on it. */

.js [data-reveal],
.js [data-reveal-stagger] > * {
  opacity: 0;
  transform: translateY(20px);
  transition:
    opacity   var(--dur-reveal) var(--ease-out),
    transform var(--dur-reveal) var(--ease-out);
}

.js [data-reveal].is-revealed,
.js [data-reveal-stagger].is-revealed > * {
  opacity: 1;
  transform: none;
}

/* --reveal-i is set per child by the observer. */
.js [data-reveal-stagger] > * {
  transition-delay: calc(var(--reveal-i, 0) * var(--stagger));
}

@media (prefers-reduced-motion: reduce) {
  .js [data-reveal],
  .js [data-reveal-stagger] > * {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ============================================================
   9 · Content sections (Phases 5–7)
   ============================================================ */

/* Two adjacent sections each contribute their padding to the gap between
   them, so this is half the space the eye actually sees. The reference
   runs ~44–72px between one section's last line and the next one's first;
   at var(--space-6) each side we were putting 192px there, which is what
   made every band look adrift from the one above it. 36px a side lands on
   the reference's 72px at desktop and tightens to 56px on a phone. */
.section { padding-block: clamp(1.75rem, 4vw, 2.25rem); }

.section__inner {
  width: 100%;
  max-width: var(--col);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

@media (min-width: 768px) {
  .section__inner { max-width: var(--col-wide); }
}

/* ---------- 9.1 · Bismillah / invitation (Phase 5) --------
   Matched to the live reference: its own torn-paper photograph as the
   card ground (assets/img/invite-card-bg.png, replacing the hand-drawn
   mask below), its own Bismillah calligraphy graphic rather than live
   Naskh text, and its own floral cutouts flanking the card. Colours and
   the script font for "Two Souls..." are taken from the reference's own
   CSS. Its serif for the greeting/body ("GT Super Display Light") is a
   paid commercial font we won't redistribute; Ovo (the sitewide body
   font already) stands in — see assets/img/README.md.
   ---------------------------------------------------------- */

/* The card rides up over the bottom of the hero rather than sitting below
   it in its own band. In the reference the two overlap by ~7% of the
   card's width — the photo runs on behind the torn edge and shows through
   its notches — and that seam is the whole reason the florals exist: they
   straddle it, tying the narrower photo to the wider card. Left in its own
   band (the section's default 96px of top padding) they have nothing to
   sit on and read as floating in empty cream.
   Both .hero__panel and .card are already positioned, and this comes later
   in the document, so it paints over the hero without needing a z-index. */
#invitation-card {
  padding-top: 0;
  margin-top: calc(-1 * clamp(24px, 7vw, 45px));
}

/* Every card in the reference is a fixed 574px that simply overflows a
   phone screen, so its straight side edges are never in view and the art
   runs edge to edge. Below 484px we do the same by dropping the gutters.
   On the invitation card this is load-bearing rather than cosmetic: the
   card must never be narrower than the hero photo it overlaps, or the
   photo's bottom corners poke out past the torn edge at the seam. */
@media (max-width: 484px) {
  #invitation-card .section__inner,
  #schedule .section__inner,
  #venue .section__inner {
    max-width: none;
    padding-inline: 0;
  }

  /* Let the Schedule heading span the card's full width rather than its
     padded content box. The reference's scrollwork sits hard against the
     screen edges on a phone (measured: 1px and 390px on a 390 screen),
     which it can only do because its card is wider than the screen. Ours
     fits the screen, so the heading has to reclaim the padding or the
     flourishes get starved down to ~28px against the 41px title. */
  .heading--schedule { margin-inline: calc(-1 * var(--space-3)); }

  /* #venue drops its gutters so the Location card's torn paper reaches the
     screen edges like every other card. The map frame and its link sit
     outside that card, though, and a bordered box hard against the bezel
     reads as a mistake — so they take the gutter back individually. */
  #venue .venue__frame,
  #venue .venue__actions { margin-inline: var(--gutter); }
}

.invite { text-align: center; }

/* The real photographed card replaces the CSS torn-paper mask: no more
   --tear reveal, no repeating grain overlay — both are baked into the
   photo. Left/right stay straight, same as the mask version, so the art
   still reads as a strip rather than a bounded rectangle. */
.card.invite {
  /* Top inset is padding, not a margin on the Bismillah: with no
     padding-top a child's margin-top collapses through the card and
     shifts the whole thing down instead of insetting the content.
     10% ≈ the reference's 60px-below-the-torn-edge on its own card. */
  padding: 10% var(--space-3) var(--space-4);
  background: url("../assets/img/invite-card-bg.png") center / 100% 100% no-repeat;
  -webkit-mask-image: none;
          mask-image: none;
}

.card.invite::before { display: none; }

/* 49% of the card's width, measured off the reference; its distance from
   the torn edge comes from .card.invite's padding-top. The florals' vines
   hang past it either side, which is why it reads small against the card. */
.bismillah {
  display: block;
  /* 49% of the card's full width. As an in-flow child a bare % would
     resolve against the content box, which the card's inline padding has
     already narrowed — so add that padding back before taking the share.
     (The florals above are absolutely positioned and resolve against the
     padding box, so their percentages need no such correction.) */
  width: calc(0.493 * (100% + 2 * var(--space-3)));
  height: auto;
  margin: 0 auto var(--space-3);
}

/* The reference sets these at a flat 41px / 21px (line-height 43 / 26) at
   every viewport — its layout is a fixed 1200px canvas, so nothing about
   it is fluid. Matched as a ceiling that only small phones scale back
   from, rather than a hard px value that would overflow a 320px screen:
   both hit the reference's exact size from ~390px up. The site's own
   --t-script-md / --t-body are deliberately not used here; they are a
   whole step smaller (25px / 14px on a phone) and left this section
   looking half the weight of the reference. */
.invite {
  --invite-script: clamp(1.9rem, 10.5vw, 2.5625rem);  /* → 41px */
  --invite-body:   clamp(1rem, 5.4vw, 1.3125rem);     /* → 21px */
}

.invite__line {
  font-family: "Imperial Script", var(--font-script);
  color: #A67D2B;
  font-size: var(--invite-script);
  line-height: 1.05;
}

.invite__line:last-of-type { margin-bottom: var(--space-3); }

.invite__greeting {
  color: #6A5140;
  font-size: var(--invite-body);
  line-height: 1.24;
  letter-spacing: 0.02em;
  margin-bottom: var(--space-1);
}

.invite__body {
  color: #6A5140;
  font-size: var(--invite-body);
  line-height: 1.24;
  text-wrap: pretty;
}

/* Two halves of one floral swag across the card's top: flush to each
   outer edge, hanging ~56% of their own height above the torn edge so the
   trailing vines drape down either side of the Bismillah.

   The sizing came from the live reference's rendered geometry, normalised
   against its *visible* paper (the source PNG floats the card inside a
   wider transparent canvas, so its own element box is 30% wider than the
   paper and can't be compared to ours directly). Its two halves are
   slightly mismatched — 39%/41% wide, 54.5%/57.5% up, because the art
   isn't mirrored — so both use the midpoint of each pair instead, giving
   a matching size and rise on either side.

   Sized and inset to the scale the reference reads at on a phone rather
   than on desktop: there its card is a fixed 574px overflowing a 390px
   screen, so its florals come out ~56% of the visible width and hang
   ~11–14% off each edge. That's the bigger, bolder look — on desktop the
   same art sits at only ~39% and well inside the card. Hanging off the
   edge is why the card clips them (overflow is hidden on the body), which
   is intended: they read as cut off by the frame, not as free-floating
   bouquets. */
/* The <picture> wrappers must establish no containing block, or the
   absolutely positioned image inside resolves its percentages against the
   wrapper instead of the card. Two separate rules would each cause that:
     · .card > * sets position: relative (for the ::before grain, which
       .card.invite doesn't have) — so force it back to static;
     · the stagger reveal sets transform: translateY(20px) on every direct
       child, and a transformed element is a containing block for
       absolutely positioned descendants. That one also *moves* as the
       transition runs, so the florals would slide into place a beat after
       the card rather than sitting still. Opacity alone carries the
       fade-in; it creates a stacking context but no containing block. */
/* .js …> * scores (0,2,0), so this must too — hence .js, not .invite alone. */
.js .invite > picture,
.invite > picture {
  position: static;
  transform: none;
}

.invite__ivy {
  position: absolute;
  top: 0;
  width: 56%;
  height: auto;
  transform: translateY(-56%);
}

.invite__ivy--start { left: -15%; }
.invite__ivy--end   { right: -15%; }

/* ---------- 9.2 · Countdown (Phase 6) ---------------------
   Matches the reference's own custom widget: Ovo (not Cinzel)
   digits in a shimmering gold gradient, revealed with an "ink"
   wipe the first time they scroll into view, then flipped in
   and out — not just overwritten — whenever a unit changes.
   ---------------------------------------------------------- */

.countdown__clock {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: clamp(0.15rem, 1.2vw, 0.5rem);
  margin-top: var(--space-4);
}

.count {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Fixed width per unit. Ticking digits must not shove their neighbours
     around once a second — the seconds column changes constantly. */
  min-width: clamp(3.1rem, 15vw, 4.4rem);
}

/* Clips the flip transform so a digit leaving upward does not briefly
   overlap the label sitting underneath it.
   The font-size is what makes the height correct, not decoration: `em`
   resolves against this element's own font-size, and without it the wrap
   inherited the body's ~15px and clipped a 36px digit down to 18px —
   cutting off more than half of every number. It must track whatever
   .count__value is set to. */
.count__wrap {
  overflow: hidden;
  font-size: var(--t-display-lg);
  height: 1.22em;
  display: grid;
  place-items: center;
}

.count__value {
  font-family: var(--font-body);
  font-size: var(--t-display-lg);
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: "tnum" 1;
  line-height: 1.1;

  background: linear-gradient(105deg,
    var(--gold) 0%, var(--gold) 25%,
    var(--gold-light) 46%, color-mix(in srgb, var(--gold-light) 70%, #fff) 52%,
    var(--gold-light) 58%, var(--gold) 75%, var(--gold) 100%);
  background-size: 300% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;

  /* Hidden until .js-countdown-in scrolls it into view (see main.js), then
     wiped open left-to-right. Motion-safe fallback keeps this at 0% so a
     reduced-motion guest is never shown a permanently blank digit. */
  clip-path: inset(0 100% 0 0);
  transition: transform 0.55s cubic-bezier(0.25, 0.1, 0.25, 1), opacity 0.55s ease;
}

.count__value.is-revealed {
  animation:
    countInkReveal 1.1s cubic-bezier(0.4, 0, 0.2, 1) forwards,
    countShimmer 4.5s ease-in-out 1.1s infinite;
}

/* Staggered so the four units wipe in left to right rather than at once. */
[data-count="days"].is-revealed    { animation-delay: 0s,    1.1s; }
[data-count="hours"].is-revealed   { animation-delay: 0.18s, 1.28s; }
[data-count="minutes"].is-revealed { animation-delay: 0.36s, 1.46s; }
[data-count="seconds"].is-revealed { animation-delay: 0.54s, 1.64s; }

@keyframes countInkReveal {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0% 0 0); }
}

@keyframes countShimmer {
  0%, 100% { background-position: 100% 0; }
  50%      { background-position: 0% 0; }
}

/* One unit ticking over: out on the way up, back in from below. Applied
   only when the value actually changes — see main.js — not every second. */
.count__value.is-leaving  { transform: translateY(-60%); opacity: 0; }
.count__value.is-entering {
  transform: translateY(60%);
  opacity: 0;
  transition: none;   /* snap to the entry position before animating in */
}

.count__label {
  font-family: var(--font-body);
  font-style: italic;
  font-size: var(--t-caps);
  letter-spacing: 0.1em;
  text-indent: 0.1em;
  color: var(--gold);
  opacity: 0;
  margin-top: var(--space-1);
  transition: opacity 0.8s ease;
}

.count__label.is-revealed { opacity: 0.85; }

.count__label[data-count-label="days"].is-revealed    { transition-delay: 0.9s; }
.count__label[data-count-label="hours"].is-revealed   { transition-delay: 1.08s; }
.count__label[data-count-label="minutes"].is-revealed { transition-delay: 1.26s; }
.count__label[data-count-label="seconds"].is-revealed { transition-delay: 1.44s; }

/* Given the same box as .count__wrap and centred in it, so it lines up
   with the digits on its own. This previously used a -0.7em nudge, which
   was tuned to cancel the clipped wrap height above — with that fixed the
   nudge threw the colons ~43px clear of the digits. */
.count__sep {
  font-family: var(--font-body);
  font-size: var(--t-display-lg);
  height: 1.22em;
  display: grid;
  place-items: center;
  line-height: 1;
  color: var(--gold);
  opacity: 0;
  transition: opacity 0.6s ease;
}

.count__sep.is-revealed { opacity: 0.9; }
.count__sep:nth-of-type(1).is-revealed { transition-delay: 0.25s; }
.count__sep:nth-of-type(2).is-revealed { transition-delay: 0.43s; }
.count__sep:nth-of-type(3).is-revealed { transition-delay: 0.61s; }

@media (prefers-reduced-motion: reduce) {
  .count__value {
    clip-path: none;
    animation: none;
    background-position: 0 0;
  }
  .count__label,
  .count__sep { opacity: 0.85; transition: none; }
}

.countdown__after {
  font-size: var(--t-script-lg);
  color: var(--gold);
  text-align: center;
  margin-top: var(--space-3);
}

.countdown__after[hidden] { display: none; }

/* ---------- 9.3 · Schedule timeline (Phase 7) -------------
   Cloned from the reference's own Schedule record: its two torn-paper
   photographs for the card, its gold scrollwork and peony, and its type
   (Imperial Script 41/64 heading, Ovo 30/47 times, 20/22 labels) and
   colours, all measured off the rendered page. Sizes are expressed as a
   ceiling that only narrow phones scale back from, the same approach the
   invitation card uses — the reference itself is a fixed 1200px canvas
   with nothing fluid in it, so there are no responsive values to copy.
   ---------------------------------------------------------- */

/* Reusable: torn top and bottom edges from the reference's own paper
   photographs, with a fill between them colour-matched to the texture.
   The paper is essentially flat — every row samples within 1-2/255 of
   the next — so the fill is set to the exact tone of the rows the strips
   join on, rgb(241,229,212), and the seams don't show. Beats the
   --torn-* CSS mask for a card of unknown height: the strips keep their
   own aspect and the middle simply stretches. */
.card--paper {
  padding: clamp(2rem, 7vw, 3.25rem) var(--space-3);
  background:
    url("../assets/img/schedule-card-top.png") top center / 100% auto no-repeat,
    url("../assets/img/schedule-card-bottom.png") bottom center / 100% auto no-repeat,
    #F1E5D4;
  -webkit-mask-image: none;
          mask-image: none;
}

.card--paper::before { display: none; }   /* grain is in the photograph */

.heading--schedule {
  font-size: clamp(1.9rem, 10.5vw, 2.5625rem);   /* → 41px */
  line-height: 1.56;                             /* → 64px */
  color: #A67D2B;
}

/* The reference's scrollwork is 79x41 against its 574px card and sits at
   the very screen edges on a phone. Keeping flex: 1 1 0 from the base
   rule lets it take only the room the heading leaves, so it reaches that
   79px on a wide card and shrinks rather than wrapping the text on a
   narrow one; aspect-ratio keeps the art from squashing as it does. */
.heading--schedule::before,
.heading--schedule::after {
  height: auto;
  aspect-ratio: 79 / 41;
  min-width: clamp(22px, 6vw, 34px);
  max-width: 79px;
  transform: none;
}

.heading--schedule::before {
  background-image: url("../assets/img/schedule-flourish-left.png");
}

.heading--schedule::after {
  background-image: url("../assets/img/schedule-flourish-right.png");
}

.timeline {
  /* One stop, centre to centre, in the reference: 78px. The rule and the
     peony both hang off this, so they stay pinned to the first and last
     node without either being nudged by hand. */
  --row: clamp(3.25rem, 20vw, 4.875rem);

  position: relative;
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: 0;
}

/* The rule runs between the first and last node, not the full box — one
   overshooting past the end stops reading as a timeline. Flat #9C8575 at
   1px, as the reference draws it, not the gold gradient we had. */
.timeline::before {
  content: "";
  position: absolute;
  left: 50%;
  top: calc(var(--row) / 2);
  bottom: calc(var(--row) / 2);
  width: 1px;
  translate: -0.5px 0;
  background: #9C8575;
}

/* The reference centres its peony exactly on the first node rather than
   floating it above the rule — measured, its centre and the first
   diamond's centre are the same point, so it crowns the rule by covering
   that node. */
/* --rose-shift is set by initScheduleRose() in js/main.js as the guest
   scrolls, migrating the peony down through every node rather than
   sitting fixed on the first — matching the reference's own scroll-
   scrubbed animation. Defaults to 0px, which is the first node, so a
   guest with JS disabled or motion reduced still sees it correctly
   placed rather than missing or stuck off-position. `translate` centres
   it on the node; `transform` carries the scroll travel — the two
   compose instead of colliding because they are separate properties. */
.timeline::after {
  content: "";
  position: absolute;
  left: 50%;
  top: calc(var(--row) / 2);
  width: clamp(38px, 14.1vw, 55px);
  aspect-ratio: 55 / 51;
  translate: -50% -50%;
  transform: translateY(var(--rose-shift, 0px));
  background: url("../assets/img/schedule-rose.png") center / contain no-repeat;
}

@media (prefers-reduced-motion: reduce) {
  .timeline::after { transform: none; }
}

/* The gap has to clear the peony, not just the node: the flower is 55px
   wide over an 11px node, so it reaches ~22px past the column on each
   side and will sit on top of "5 PM" and the first label if the gap is
   any tighter. The reference leaves ~17px of daylight beyond that. */
.timeline__stop {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  column-gap: clamp(1.75rem, 8vw, 2rem);
  min-height: var(--row);
}

.timeline__time {
  font-family: var(--font-body);            /* the reference's weight 600 is Ovo */
  /* Sized for a short reference time like "5 PM" (4 characters). Repurposed
     as a full date — "01.05.2025", 10 characters — the old clamp(1.375rem,
     7.7vw, 1.875rem) hit its 30px ceiling on every phone (its 7.7vw term
     passes 30px before 390px width), so the date dominated the row and
     crowded the divider. Brought down to sit close to .timeline__event
     instead of a third again as large. */
  font-size: clamp(1.05rem, 5.6vw, 1.375rem);    /* → 22.4px */
  line-height: 1.3;
  letter-spacing: 0.01em;
  color: #846F61;
  text-align: right;
}

.timeline__event {
  font-size: clamp(0.95rem, 5.13vw, 1.25rem);    /* → 20px */
  line-height: 1.1;                              /* → 22px */
  color: #6C513F;
  text-align: left;
  text-wrap: balance;
}

/* Diamond node, sitting on the rule. Stays 8px rather than scaling: the
   reference fixes it, and a node that shrinks with the viewport stops
   reading as a marker. */
.timeline__node {
  width: 8px;
  height: 8px;
  rotate: 45deg;
  background: #8C7666;
}

/* ---------- 9.4 · Venue + map (Phase 8) ------------------- */

/* Matched to the supplied Location artwork (assets/img/_source/
   location-design-original.png). Every size and gap below is that
   design's own measurement, expressed as a fraction of its card width
   (1402px) so it scales to whatever width our column happens to be.

   Two things the design does that the rest of the site does not, and
   which are deliberate here rather than oversights:
     · no flanking filigree — a single centred rule sits *under* the
       heading instead, so this heading is a plain .heading;
     · the venue name is title-case body serif, not tracked Cinzel caps.

   Colours are sampled from the artwork's own stroke interiors (eroded,
   so antialiasing against the paper can't lighten them):
     heading #A78031 · name #513B27 · address #412F1D                 */

.venue { text-align: center; }

.venue__card {
  text-align: center;
  /* Our torn strip is the reference's photograph, which is proportionally
     about twice as deep as the one in this design (10% of the card width
     against 4.7%). Left at the shared card padding the heading tucks up
     under the tear; 13% puts its cap where the design has it, 12.5% of the
     card width down. Percentage padding resolves against the containing
     block's width, so this tracks the card at every size. */
  padding-top: 13%;

  /* Percentage rather than the shared 24px gutter, so the content box stays
     a fixed 91.4% of the card. With a fixed gutter it does not: 24px is
     4.3% of the 556px desktop card but 6.2% of a 390px phone one, and the
     drawing — sized against the content box — quietly loses 4% of the card
     on the way down. */
  padding-inline: 4.3%;
  /* One shared unit for the whole block: every size and gap below is a
     multiple of it, the way the design's are. It has to be a custom
     property rather than `em`, because an `em` on the divider or the
     drawing resolves against *their* inherited font-size, not the
     address's — the trap that clipped the countdown digits earlier.

     Held at the site's body size. Scaled strictly by the design's own
     ratio it would read about 13px in a phone column, which is too small
     to print an address at. Everything here is therefore ~13% larger
     against the card than the design is; the positions still match. */
  --venue-addr: var(--t-body);
}

/* The design's gold is a touch deeper and browner than the site's --gold
   (#B08D3E). Scoped, so no other heading shifts. */
.venue__card .heading {
  color: #A78031;
  /* One line, and a script face whose ink already fills its em box. The
     shared 1.15 leading only adds dead space under the swash, which then
     has to be subtracted back out of the divider's margin. */
  line-height: 1;
}

/* The multipliers below are not the design's raw ink gaps (which run
   29/30/32/18px against a 41.4px address on a 1402px card). They are what
   reproduces those gaps here, solved by measuring the rendered result:
   a CSS margin stacks on top of the line box's half-leading, and how much
   half-leading there is depends on the font. Every band now lands within
   0.8% of the card width of the design at both 390px and 1470px. */

.venue__divider {
  display: block;
  /* 14.5% of the card in the design. Our content box is the card less its
     padding, so the same ribbon is 15.9% of it. */
  width: max(46px, 15.9%);
  height: auto;
  /* Pulls up through the slack left under the heading by the swash on the
     script L, which the line box reserves whether or not the swash is
     there. Tied to the heading's own size so it scales with it. */
  margin: calc(-0.15 * var(--t-script-lg)) auto 0;
}

.venue__name {
  font-size: calc(1.345 * var(--venue-addr));   /* design: cap 39px vs 29px */
  line-height: 1.25;
  color: #513B27;
  margin-top: calc(0.39 * var(--venue-addr));
}

.venue__address {
  font-size: var(--venue-addr);
  /* The design sets this far tighter than the site's 1.75 body leading —
     two address lines read as one block there, not as a paragraph. */
  line-height: 1.32;
  color: #412F1D;
  margin-top: calc(0.235 * var(--venue-addr));
}

/* "Address:" is a label the design prints, not part of the address itself,
   so it lives in the markup and stays out of WEDDING.venue — the map query
   and the directions link must not inherit it. */
.venue__address-label { white-space: nowrap; }

/* display: contents dissolves the wrapper's box, so the first address line
   sits inline beside that label the way the design has it, and every line
   after it starts a new one. */
.venue__address-lines { display: contents; }
.venue__address-line { display: block; }
.venue__address-line:first-child { display: inline; }

/* The drawing is the section's centrepiece, and 78.9% of the card wide in
   the design — 86.4% of our content box once the card's own padding is
   taken off. (The other reading available, 28.4x the venue name's cap
   height, lands at 90%; the card fraction wins because our type runs a
   little larger against the card than the design's does.) */
.venue__art {
  display: block;
  width: 86.4%;
  height: auto;
  margin: calc(0.69 * var(--venue-addr)) auto 0;
}

.venue__frame { margin-top: var(--space-4); }

.venue__map {
  display: block;
  width: 100%;
  height: clamp(200px, 52vw, 300px);
  border: 0;
  /* The embed paints white before it loads; cream keeps the gap warm. */
  background: var(--paper);
}

.venue__actions { margin-top: var(--space-3); }

/* A gold caps link with a rule that draws itself under on hover. */
.link-caps {
  display: inline-block;
  font-family: var(--font-display);
  font-size: var(--t-caps);
  letter-spacing: var(--track-caps);
  text-indent: var(--track-caps);
  text-transform: uppercase;
  color: var(--gold);
  text-decoration: none;
  padding-bottom: 3px;
  background: linear-gradient(var(--gold), var(--gold)) 0 100% / 0 1px no-repeat;
  transition: background-size 0.35s var(--ease-out);
}

.link-caps:hover,
.link-caps:focus-visible { background-size: 100% 1px; }

/* ============================================================
   11 · Music button (Phase 11)
   ============================================================ */

/* Matched to the reference's own floating control: a 60px maroon disc
   20px off the bottom-right corner, #5A0F1B, a low soft shadow, and a
   30px white glyph. It fades in over 0.3s only once the opening film has
   finished — the reference keeps it hidden behind the envelope, and a
   music button floating over a sealed envelope would be an odd first
   thing to see. */
.music {
  position: fixed;
  right: 20px;
  /* The reference sits at a flat 20px, which on a notched iPhone puts a
     60px disc partly under the home indicator. max() keeps the
     reference's spacing everywhere it is safe and yields where it isn't. */
  bottom: max(20px, env(safe-area-inset-bottom));
  z-index: 60;

  display: grid;
  place-items: center;
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background: #5A0F1B;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);

  /* Hidden until the film ends. visibility, not display, so the opacity
     transition has something to animate. */
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.3s ease-in-out,
              transform 0.2s var(--ease-out);
}

.music.is-ready {
  visibility: visible;
  opacity: 1;
}

.music[hidden] { display: none; }

.music:hover { transform: scale(1.06); }
.music:active { transform: scale(0.97); }

.music__icon {
  width: 30px;
  height: 30px;
  background: #FFF;
  /* Masked, so one shape swap changes the icon and the colour stays ours. */
  -webkit-mask: var(--icon-play) center / contain no-repeat;
          mask: var(--icon-play) center / contain no-repeat;
}

.music.is-playing .music__icon {
  -webkit-mask-image: var(--icon-pause);
          mask-image: var(--icon-pause);
}

/* The fade is decoration; the button must still arrive for everyone. */
@media (prefers-reduced-motion: reduce) {
  .music { transition: none; }
}

/* ============================================================
   12 · Closing (Phase 12)
   ============================================================ */

.closing { padding-top: var(--space-6); }

.closing__words { text-align: center; }

.closing__line { font-size: var(--t-script-lg); }

.closing__names {
  font-size: var(--t-display-md);
  letter-spacing: 0.2em;
  text-indent: 0.2em;
  text-transform: uppercase;
  color: var(--ink);
  margin-top: var(--space-2);
}

/* The photo at the original file's own dimensions, 818x1280, uncropped.

   Capped at 818px, which is a real constraint rather than a preference:
   the photo is portrait, so width and height trade off directly. Running
   it edge to edge on a 1470px window would make it 2300px tall — two and a
   half screenfuls of one photograph — and enlarge it 1.8x while doing it.
   At its native width it is full-bleed on every phone (390px wide gives a
   tall 390x610 band) and never enlarged anywhere, which also retires the
   softness the landscape version had on big monitors. */
.closing__figure {
  position: relative;
  margin: var(--space-5) auto 0;
  max-width: 818px;
}

/* No aspect-ratio and no object-fit: the box takes the photo's own shape,
   so nothing is ever cropped off any edge. The width/height attributes in
   the markup supply the intrinsic ratio, so the space is reserved before
   the file arrives and nothing below it jumps on load. */
.closing__photo {
  display: block;
  width: 100%;
  height: auto;
}

/* The arch overlaps the photo's bottom corners. */
.closing__arch {
  position: absolute;
  bottom: 0;
  width: clamp(110px, 34vw, 240px);
  height: clamp(80px, 24vw, 170px);
}

.closing__arch--start { left: 0; }
.closing__arch--end   { right: 0; }

/* ---------- 12.1 · Scaffold bands (Phase 0 only) ----------
   Placeholder styling so the skeleton is legible and scrollable.
   Every rule in this block gets deleted as its phase lands.
   ---------------------------------------------------------- */

.band {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 55vh;
  padding: var(--space-5) var(--gutter);
  border-top: 1px dashed var(--gold-soft);
  text-align: center;
}

.band--full {
  min-height: 100vh;
  min-height: 100svh;
}

.band:first-child { border-top: 0; }

.band__label {
  font-family: var(--font-display);
  font-size: var(--t-display-md);
  font-weight: 400;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold);
}

.band[data-phase]::after {
  content: attr(data-phase);
  position: absolute;
  top: var(--space-2);
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--font-body);
  font-size: var(--t-small);
  letter-spacing: 0.1em;
  color: var(--ink-soft);
}

/* ---------- 10 · Motion preferences ----------------------- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
