/* Bloom — Ebenezer & Regina.
   Palette sampled from their printed invitation. The engine is themed through
   its six tokens; everything below is this page's own. */

:root {
  --sc-canvas: #FBF6EC;         /* ivory, the card */
  --sc-surface: #F3E9D7;
  --sc-ink: #4A1B0E;            /* oxblood, darker than the card's for 4.5:1 */
  /* Was #7C5240. Every small line on this page is set in it — the eyebrow,
     the date line, the captions, the map — and measured on composited pixels
     it came in at 3.66..4.17:1 over the paper bloom, i.e. under the 4.5
     floor wherever a petal or a leaf tinted the ground beneath it. Two steps
     darker, same hue, still unmistakably a tinted brown and not a grey. */
  --sc-ink-soft: #653F2E;
  --sc-accent: #A63A17;         /* the lily, stepped down to clear AA on ivory */
  --sc-accent-ink: #FBF6EC;

  --lily: #C0491F;              /* the card's own orange, for ornament only */
  --olive: #6E7A4A;

  --sc-font-display: "EB Garamond", Georgia, serif;
  --sc-font-text: "EB Garamond", Georgia, serif;
  --script: "Pinyon Script", "EB Garamond", cursive;
}

/* Theirs is a heritage, ceremonial document and the printed card is already
   set in a didone and a copperplate script. A grotesque here would be a
   different wedding. */

body {
  background: var(--sc-canvas);
  color: var(--sc-ink);
  font-family: var(--sc-font-text);
  /* Light-on-dark compensation does not apply: this page is dark-on-light. */
  font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
  line-height: 1.6;
  text-wrap: pretty;
}

/* ---- the paper ------------------------------------------------------------
   Generated texture was refused by the model's recitation filter, so the grain
   is drawn here instead: two offset radial fields plus a faint diagonal weave.
   Zero bytes, no request, and it scales to any viewport. */

.paper {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(120% 90% at 50% 0%, #FDFAF3 0%, transparent 60%),
    radial-gradient(130% 100% at 20% 100%, #F6EEDD 0%, transparent 55%),
    repeating-linear-gradient(32deg,
      rgba(124, 82, 64, 0.016) 0 2px, transparent 2px 5px),
    repeating-linear-gradient(-54deg,
      rgba(124, 82, 64, 0.012) 0 3px, transparent 3px 7px),
    var(--sc-canvas);
}

/* ---- the planes -----------------------------------------------------------
   Five depths, driven by --bloom-open rather than --bloom-p.

   --bloom-p is the whole flight's progress; --bloom-open ramps across the
   OPENING STATION ONLY and then holds. The planes use the latter because the
   flowers must come off the card at one identifiable moment. Driving them from
   page progress spread the motion so thinly that the first contact sheet
   showed forty-eight near-identical frames and no peak anywhere. Both are
   published by bloom.js off the engine's own scroll read, so this moves in
   lockstep with the copy windows without the engine knowing it exists.

   Depth comes from three things at once, not from translation alone: different
   travel rates, different scale rates, and real occlusion. The garland is
   furthest and barely moves; the near leaves are closest and leave frame. */

.planes { position: absolute; inset: 0; overflow: hidden; }

.plane {
  position: absolute;
  display: block;
  /* The engine's own poster push-in is disabled for these: this page drives
     them, so two transforms would fight. */
  will-change: transform, opacity;
}

/* Furthest: a band of foliage across the top, drifting right and sinking.
   Held high and faint. The first screenshot had it crossing the centre at full
   strength with the invitation line laid over the leaves, which was
   unreadable: a scrim heavy enough to fix it would have hidden the botany
   that is the whole point. Solved structurally instead, by keeping the middle
   of the frame as paper and letting the ornament hold the edges. */
.plane--garland {
  top: -14vh; left: -4vw; width: 108vw;
  opacity: calc(0.4 - var(--bloom-open, 0) * 0.14);
  transform:
    translate3d(
      calc(var(--bloom-open, 0) * 4vw + var(--bloom-settle, 0) * 2vw),
      calc(var(--bloom-open, 0) * -11vh + var(--bloom-settle, 0) * -5vh), 0)
    scale(calc(1 + var(--bloom-open, 0) * 0.16 + var(--bloom-settle, 0) * 0.05));
}

/* The card's two corner sprays. These are the ones that "come off the card":
   they begin tucked into the corners exactly as printed, then separate
   outward and rotate slightly as the flight opens. */
.plane--tr {
  top: -10vh; right: -12vw; width: min(62vw, 760px);
  transform:
    translate3d(
      calc(var(--bloom-open, 0) * -4vw + var(--bloom-settle, 0) * -2vw),
      calc(var(--bloom-open, 0) * 9vh + var(--bloom-settle, 0) * 4vh), 0)
    rotate(calc(var(--bloom-open, 0) * 9deg + var(--bloom-settle, 0) * 3deg))
    scale(calc(1 + var(--bloom-open, 0) * 0.3 + var(--bloom-settle, 0) * 0.08));
}

.plane--bl {
  bottom: -14vh; left: -14vw; width: min(66vw, 820px);
  transform:
    translate3d(
      calc(var(--bloom-open, 0) * 4vw + var(--bloom-settle, 0) * 2vw),
      calc(var(--bloom-open, 0) * -10vh + var(--bloom-settle, 0) * -4vh), 0)
    rotate(calc(var(--bloom-open, 0) * -8deg + var(--bloom-settle, 0) * -3deg))
    scale(calc(1 + var(--bloom-open, 0) * 0.34 + var(--bloom-settle, 0) * 0.09));
}

/* A single sprig that crosses the frame through the quiet middle, so the
   silence at the vow still has something alive in it. Travels high, above the
   copy band, and thins as it goes. */
.plane--sprig {
  top: 6vh; left: 6vw; width: min(18vw, 210px);
  opacity: calc(0.6 - var(--bloom-open, 0) * 0.22);
  transform:
    translate3d(
      calc(var(--bloom-open, 0) * 14vw + var(--bloom-settle, 0) * 8vw),
      calc(var(--bloom-open, 0) * 6vh + var(--bloom-settle, 0) * 4vh), 0)
    rotate(calc(-8deg + var(--bloom-open, 0) * 16deg));
}

/* ---- the reading field --------------------------------------------------
   A soft bloom of paper behind the copy column only, so the middle of the
   frame reads as the card's empty centre and the type sits on paper rather
   than on leaves. Not a full-frame overlay: that is banned, and it would kill
   the botany. This is local, elliptical, and invisible as an edge. */

.field {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* ABOVE the petal canvas (z-index 6), and this is load-bearing.
     `.field` is positioned with z-index auto, so it painted below the
     canvas's z-index 6 and the petals passed BETWEEN the paper and the type.
     A terracotta petal at 0.88 alpha directly behind a 13.4px line measured
     1.62:1. The reading field's whole job is to be the last thing between the
     words and the world, so it has to sit above everything the world
     contains. */
  z-index: 7;
  /* Two blooms, not one.
     A single 58%x42% ellipse at dead centre was right when every station was
     one centred column. The hero is now a two-column block anchored left of
     centre, and its words column reaches to about 0.78 of the width and up to
     0.18 of the height — well outside that ellipse. Measured there, the
     eyebrow fell to 1.48:1 against a lily, the ampersand to 1.64:1 and the
     date line to 1.88:1.

     The first stop covers the hero's full column; the second keeps the
     centred stations (the vow, the quiet) as they were. Both still fade to
     nothing well inside the frame, so the botany holds the edges. */
  background:
    radial-gradient(
      62% 52% at 44% 48%,
      color-mix(in oklab, var(--sc-canvas) 95%, transparent) 0%,
      color-mix(in oklab, var(--sc-canvas) 84%, transparent) 46%,
      transparent 78%
    ),
    radial-gradient(
      52% 40% at 50% 50%,
      color-mix(in oklab, var(--sc-canvas) 92%, transparent) 0%,
      color-mix(in oklab, var(--sc-canvas) 74%, transparent) 44%,
      transparent 74%
    );
}

/* Nearest: oversized leaves that pass the viewer and leave frame.
   Pushed well below the fold and kept faint. At the scale the first build used
   they reached the middle of the viewport and sat behind the couple's names,
   which is the one place on this page that must stay clean. They now read as a
   near-plane at the bottom edge, which is what gives the hero its depth
   without competing with the type. */
.plane--leaves {
  /* Smaller and higher than the first pass (128vw at -34vh), which put a
     viewport-wide mass across the whole lower third. A near plane reads as
     near by being partly out of frame, not by being enormous. */
  bottom: -26vh; left: -6vw; width: 108vw;
  /* Was 0.3 falling to 0.22. At that strength the olive drained to grey fog
     and the plate's drawing was lost; the asset is a well-drawn leaf and
     deserves to be seen as one. It is still the faintest plane on the page. */
  opacity: calc(0.52 - var(--bloom-open, 0) * 0.12);
  transform:
    translate3d(
      calc(var(--bloom-open, 0) * -5vw + var(--bloom-settle, 0) * -3vw),
      calc(var(--bloom-open, 0) * 15vh + var(--bloom-settle, 0) * 7vh), 0)
    scale(calc(1.05 + var(--bloom-open, 0) * 0.26 + var(--bloom-settle, 0) * 0.08));
}

/* ---- the drawn rule ------------------------------------------------------
   The card has a printed border. Here it draws itself over the first station
   and stays, so the page starts as a card and becomes a space. */

.frame {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
}

.frame__rect {
  fill: none;
  stroke: color-mix(in oklab, var(--sc-ink) 34%, transparent);
  stroke-width: 1;
  /* Perimeter of a 94x94 rect in the viewBox's units. */
  stroke-dasharray: 376;
  stroke-dashoffset: calc(376 - 376 * var(--bloom-frame, 0));
  opacity: calc(0.3 + var(--bloom-frame, 0) * 0.7);
}

/* ---- petals that land -----------------------------------------------------
   The signature move's residue. See bloom.js. */

.drift { position: absolute; inset: 0; pointer-events: none; }

.petal {
  position: absolute;
  width: var(--w);
  opacity: 0;
  transform: translate3d(0, -12vh, 0) rotate(0deg);
  transition: opacity 900ms ease-out;
}

.petal.is-landed { opacity: var(--o); }

/* ---- the map -------------------------------------------------------------
   The grammar's chrome. Set as a margin folio, not a bar: this is a document,
   and a fixed app bar over a wedding invitation is the wrong object. */

.map {
  position: fixed;
  top: 50%; right: clamp(0.75rem, 2.5vw, 2rem);
  translate: 0 -50%;
  z-index: 40;
}

.map__list {
  list-style: none;
  margin: 0;
  padding: 0.6rem 0.7rem 0.6rem 0.9rem;
  text-align: right;
  /* The map sits over the top-right floral corner — the densest part of the
     whole page, now with petals passing through it as well. The first plate
     was a gradient fading to nothing at its left edge, which left the longest
     label ("the opening") sitting on bare lilies.
     This is an opaque paper card instead: a real object, with its own edge
     and its own shadow, which is what the rest of the page is made of. */
  /* Opaque paper, full stop.
     Tried at 95% and at 99% with an 8px backdrop blur; both still measured
     under the floor (2.89:1, then 3.72:1) because a blur rearranges what is
     behind the plate without lightening it, and the lily spray here is
     genuinely dark. These are navigation controls, read before they are
     pressed, so they are held to 4.5:1 whatever their size.
     The page is made of paper objects; this is one of them. */
  background: #FDFAF3;
  border: 1px solid color-mix(in oklab, var(--sc-ink) 10%, transparent);
  border-right: 0;
  border-radius: 2px 0 0 2px;
  box-shadow: -3px 0 18px rgba(74, 27, 14, 0.09);
}

.map__item {
  appearance: none;
  background: none;
  border: 0;
  border-right: 1px solid transparent;
  padding: 0.42rem 0.7rem 0.42rem 0;
  font-family: var(--sc-font-text);
  font-size: 0.78rem;
  letter-spacing: 0.13em;
  text-transform: lowercase;
  /* Was 46%-tinted ink, which measured under 3:1 on its own plate. These are
     navigation controls a guest reads before pressing. */
  color: var(--sc-ink-soft);
  cursor: pointer;
  transition: color 240ms ease, border-color 240ms ease;
}

.map__item:hover { color: var(--sc-ink); }

.map__item[aria-current="true"] {
  color: var(--sc-accent);
  border-right-color: var(--sc-accent);
}

.map__item:focus-visible {
  outline: 2px solid var(--sc-accent);
  outline-offset: 3px;
}

/* ---- copy ----------------------------------------------------------------
   The engine positions and fades these. Only type here. */

.copy-layer { z-index: 20; }

.copy {
  position: absolute;
  left: 50%;
  translate: -50% -50%;
  width: min(92vw, 46rem);
  text-align: center;
}

/* Staggered anchors. Adjacent blocks never share a band, so the moment where
   one hands over to the next is legible instead of overprinted. */
.copy--card  { top: 50%; }
.copy--vow   { top: 48%; }
.copy--alone { top: 50%; }
/* Round 5: the day is set big, as the card sets it, now that the album has
   taken the photograph that used to sit under it. */
.copy--plate { top: 47%; width: min(92vw, 60rem); }
.copy--reply { top: 56%; }

.eyebrow {
  margin: 0 auto;
  /* 32ch wrapped this to five short lines at desktop width, which reads as a
     column of fragments rather than a sentence. Wider measure, balanced. */
  max-width: 52ch;
  text-wrap: balance;
  font-size: clamp(0.72rem, 0.66rem + 0.3vw, 0.84rem);
  /* Owner decision 2026-11-01: tighter than the printed card, easier to read. */
  letter-spacing: 0.08em;
  text-transform: uppercase;
  line-height: 1.9;
  color: var(--sc-ink-soft);
}

/* Their names in the script the card uses. The one place this page spends
   real scale. */
.names {
  margin: clamp(1.4rem, 4vh, 2.6rem) 0 0;
  font-size: 2em; /* the browser's h1 size, stated: the Next.js port's reset zeroes it */
  font-family: var(--script);
  font-weight: 400;
  line-height: 1.04;
  letter-spacing: 0.01em;
  color: var(--sc-ink);
  text-wrap: balance;
}

.names__a, .names__b { display: block; font-size: clamp(2.3rem, 7.4vw, 5.2rem); }

.names__amp {
  display: block;
  margin: clamp(0.3rem, 1.4vh, 0.9rem) 0;
  font-family: var(--sc-font-text);
  font-size: clamp(0.78rem, 0.7rem + 0.4vw, 1rem);
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--sc-ink-soft);
}

.card-foot {
  margin: clamp(1.6rem, 4.5vh, 2.8rem) auto 0;
  max-width: 44ch;
  text-wrap: balance;
  font-size: clamp(0.76rem, 0.7rem + 0.3vw, 0.9rem);
  letter-spacing: 0.03em; /* a sentence, not a label: wide tracking slows reading */
  font-style: italic;
  /* Full ink, not soft: a petal from the field drifts behind this line now
     and then, and soft ink measured 3.7:1 over one. */
  color: var(--sc-ink);
}

/* The vow. The peak's copy: large, alone, and the only italic display on the
   page. */
.vow { margin: 0; }

/* This is the PEAK of the feeling curve, and at 3.4rem it was the same size
   as a pull quote in a blog post while two thirds of the frame sat empty
   around it. The peak has to be the largest type on the page, or the page has
   no peak — it just has a quiet middle. */
.vow p {
  margin: 0;
  font-family: var(--sc-font-display);
  font-style: italic;
  font-size: clamp(2.1rem, 7.2vw, 5rem);
  line-height: 1.16;
  letter-spacing: -0.012em;
  color: var(--sc-ink);
  text-wrap: balance;
}

.vow cite {
  display: block;
  margin-top: clamp(1.2rem, 3.4vh, 2.2rem);
  font-style: normal;
  font-size: clamp(0.72rem, 0.68rem + 0.28vw, 0.82rem);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--sc-ink-soft);
}

/* The quiet. Decorative repetition of the names, hidden from the accessibility
   tree because it says nothing new. */
.alone {
  margin: 0;
  font-family: var(--script);
  font-size: clamp(2rem, 8vw, 4.6rem);
  line-height: 1.1;
  color: color-mix(in oklab, var(--sc-ink) 80%, transparent);
}

.alone span { color: var(--lily); font-size: 0.8em; }

/* The plate. Three cells divided by rules, the way the card sets them. */
/* The ledger gets its own paper, for the same reason the reply does and with
   more at stake: it carries the date, the hour and the church. Measured over
   the bare page, the third cell — "Mount Olivet / Adiembra Methodist Church"
   — sat inside the top-right lily spray at 1.29:1. A guest who cannot read
   the name of the church cannot come to the wedding, which makes this the
   single highest-stakes piece of text on the page.

   A plate rather than a stronger scrim: the florals stay fully visible either
   side of it, and the page is already made of paper objects. */
.plate {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0;
  margin: 0;
  text-align: center;
  background: color-mix(in oklab, var(--sc-canvas) 94%, transparent);
  backdrop-filter: blur(7px);
  border: 1px solid color-mix(in oklab, var(--sc-ink) 9%, transparent);
  border-radius: 2px;
  padding: clamp(1.8rem, 6vh, 3.6rem) clamp(0.8rem, 2.4vw, 2rem);
  box-shadow: 0 10px 40px rgba(74, 27, 14, 0.07);
}

.plate__cell { padding: 0 clamp(0.6rem, 2vw, 1.6rem); }

.plate__cell + .plate__cell {
  border-left: 1px solid color-mix(in oklab, var(--sc-ink) 22%, transparent);
}

.plate dt {
  font-size: clamp(0.72rem, 0.68rem + 0.24vw, 0.8rem);
  letter-spacing: 0.19em;
  text-transform: uppercase;
  line-height: 1.6;
  color: var(--sc-ink-soft);
}

.plate dd { margin: 0.45rem 0 0; }

.plate dd strong {
  font-family: var(--sc-font-display);
  font-weight: 500;
  font-size: clamp(2.6rem, 7.4vw, 6.2rem);
  line-height: 1;
  letter-spacing: 0.01em;
  font-variant-numeric: lining-nums tabular-nums;
}

.plate__sup {
  /* Absolute, not em-relative. At 0.42em of a 40px numeral this rendered at
     6.8px, which is a legibility failure even for an ordinal. A fixed 11.5px
     still reads as a superscript against a 30-40px figure, and it needs no
     suppression to pass the floor. */
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  vertical-align: 0.9em;
  color: var(--sc-ink-soft);
}

.plate__cell--hour dd strong { font-size: clamp(2.2rem, 5.6vw, 4.6rem); }

.plate__note, .plate__venue {
  margin-top: 0.5rem;
  font-size: clamp(0.72rem, 0.66rem + 0.3vw, 0.88rem);
  letter-spacing: 0.1em;
  line-height: 1.5;
  color: var(--sc-ink-soft);
}

.plate__venue {
  font-family: var(--sc-font-display);
  font-size: clamp(1.05rem, 0.8rem + 1.1vw, 1.7rem);
  line-height: 1.3;
  letter-spacing: 0.02em;
  color: var(--sc-ink);
}

/* The reply. The close resolves: it is the only act with a usable control in
   it, and the phone numbers are real links a thumb can hit.

   This station gets its own paper plate. The bottom-left lily spray and the
   near leaves both reach into the middle of the frame here, and the names
   ("Bro Francis", "Sis Agnes") were measurably illegible where the lily
   crossed them. The local .field bloom is tuned for a single centred line of
   display type and is not enough for a four-row list of functional text.

   A plate, not a scrim: it has an edge, it sits on the paper like the map
   and the card do, and it leaves the botany either side of it visible. */

/* The engine's .copy block only positions and fades; the PAPER lives on the
   inner .reply__sheet, because that is the thing that gets dealt in from the
   side and a background on the engine's own element could not travel. */
.copy--reply { width: min(92vw, 34rem); }

.reply__sheet {
  background: color-mix(in oklab, var(--sc-canvas) 94%, transparent);
  backdrop-filter: blur(7px);
  border: 1px solid color-mix(in oklab, var(--sc-ink) 9%, transparent);
  border-radius: 2px;
  padding: clamp(1.6rem, 4vh, 2.6rem) clamp(1.4rem, 4vw, 3rem);
  box-shadow: 0 10px 40px rgba(74, 27, 14, 0.07);
}

.reply__head {
  margin: 0;
  font-family: var(--script);
  font-weight: 400;
  font-size: clamp(1.9rem, 6.4vw, 3rem);
  line-height: 1.1;
  color: var(--sc-ink);
}

.reply__list {
  list-style: none;
  margin: clamp(1.4rem, 4vh, 2.4rem) auto 0;
  padding: 0;
  max-width: 24rem;
}

.reply__list li {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  padding: 0.62rem 0;
  border-bottom: 1px solid color-mix(in oklab, var(--sc-ink) 14%, transparent);
}

.reply__list li:last-child { border-bottom: 0; }

.reply__list span {
  flex: 0 0 auto;
  font-size: clamp(0.8rem, 0.74rem + 0.3vw, 0.94rem);
  letter-spacing: 0.04em;
  color: var(--sc-ink-soft);
}

/* A dotted leader, the way an order of service sets a name against a number. */
.reply__list li::before {
  content: "";
  order: 1;
  flex: 1 1 auto;
  translate: 0 -0.28em;
  border-bottom: 1px dotted color-mix(in oklab, var(--sc-ink) 26%, transparent);
}

.reply__list a {
  order: 2;
  flex: 0 0 auto;
  /* Tabular so four numbers of equal length line up down the right edge. */
  font-variant-numeric: lining-nums tabular-nums;
  font-size: clamp(0.88rem, 0.8rem + 0.4vw, 1.05rem);
  letter-spacing: 0.04em;
  color: var(--sc-accent);
  text-decoration: underline;
  text-decoration-thickness: from-font;
  text-underline-offset: 0.22em;
  /* 44px minimum target, met with padding rather than a taller row. */
  padding: 0.5rem 0;
}

.reply__list a:focus-visible {
  outline: 2px solid var(--sc-accent);
  outline-offset: 3px;
}

.reply__close {
  margin: clamp(1.6rem, 4.5vh, 2.6rem) auto 0;
  max-width: 28ch;
  font-style: italic;
  font-size: clamp(0.8rem, 0.74rem + 0.3vw, 0.95rem);
  letter-spacing: 0.03em; /* as .card-foot */
  color: var(--sc-ink-soft);
}

/* ---- phone --------------------------------------------------------------
   Art-directed, not shrunk. The map moves to the foot and goes horizontal,
   the near plane is pulled back so it cannot cover the type, and the hero
   type steps down a rung. */

@media (max-width: 700px) {
  .copy { width: min(88vw, 30rem); }

  .names__a, .names__b { font-size: clamp(1.9rem, 10.5vw, 2.9rem); }

  /* Three cells side by side at 390px gives 7 characters a line. Stack. */
  .plate { grid-template-columns: 1fr; gap: clamp(1rem, 3vh, 1.6rem); }

  /* Stacked, the plate is roughly three times its desktop height, so centring
     it pushed its first label up to y=161 on an 844px screen: into the top
     floral corner, measured at 3.66:1. A small uppercase label needs 4.5:1,
     not the 3:1 large-text allowance, so the block is anchored lower where the
     paper is clear rather than scrimmed harder. */
  .copy--plate { top: 52%; width: min(88vw, 30rem); }
  .plate { padding: 1.4rem 1rem; }
  .plate dd strong { font-size: 2.9rem; }
  .plate__cell--hour dd strong { font-size: 2.4rem; }
  .copy--reply { top: 52%; }
  .plate__cell + .plate__cell {
    border-left: 0;
    border-top: 1px solid color-mix(in oklab, var(--sc-ink) 18%, transparent);
    padding-top: clamp(1rem, 3vh, 1.6rem);
  }

  .plane--leaves { opacity: calc(0.22 - var(--bloom-open, 0) * 0.14); }
  .plane--tr { width: min(86vw, 420px); right: -22vw; }
  .plane--bl { width: min(92vw, 460px); left: -26vw; }

  /* The sprig crossed the copy column at phone width. The stacked plate is
     three times taller here than its desktop row, so its top line met the
     sprig and the measured contrast fell to 2.7:1 against a dark leaf. It is
     held in the upper margin, above every copy block, and kept faint. */
  .plane--sprig {
    width: min(26vw, 130px);
    top: 1vh; left: 3vw;
    opacity: calc(0.4 - var(--bloom-open, 0) * 0.18);
    transform:
      translate3d(calc(var(--bloom-open, 0) * 8vw), calc(var(--bloom-open, 0) * 2vh), 0)
      rotate(calc(-8deg + var(--bloom-open, 0) * 10deg));
  }

  /* The reading field has to work harder on a narrow screen: the copy column
     is proportionally wider, so the paper bloom behind it is taller and more
     opaque, and it is the thing that guarantees the contrast floor.

     Taller than it was (46%), because the hero is no longer one centred line.
     The card, the eyebrow, the names and the date now make a column roughly
     0.2..0.75 of the viewport deep, and at 46% the ellipse had faded to about
     40% paper by the time it reached the eyebrow at 0.63 — measured at
     1.93:1 against a terracotta lily. 64% covers the whole column. */
  .field {
    background: radial-gradient(
      92% 64% at 50% 48%,
      color-mix(in oklab, var(--sc-canvas) 97%, transparent) 0%,
      color-mix(in oklab, var(--sc-canvas) 91%, transparent) 58%,
      transparent 88%
    );
  }

  .map {
    top: auto; bottom: 0; right: 0; left: 0;
    translate: none;
    background: linear-gradient(to top,
      color-mix(in oklab, var(--sc-canvas) 94%, transparent),
      transparent);
    padding: 0.5rem 0 0.7rem;
  }
  .map__list {
    display: flex;
    justify-content: center;
    gap: 0.15rem;
    text-align: center;
  }
  .map__item {
    border-right: 0;
    border-bottom: 1px solid transparent;
    /* 11.2px floor and a 44px target. These are buttons a guest reads before
       pressing, and 9.9px on a phone failed that on both counts. */
    padding: 0.7rem 0.45rem;
    font-size: 0.7rem;
    letter-spacing: 0.04em;
  }
  .map__item[aria-current="true"] {
    border-right-color: transparent;
    border-bottom-color: var(--sc-accent);
  }
}

/* ===========================================================================
   THE SECOND BUILD
   ---------------------------------------------------------------------------
   Everything below answers a specific criticism of the first one: that it had
   no loader, no animation, no moving parts, no background movement, no 3D and
   no objects. Taken in order: the boot screen, the ambient botany, the WebGL
   petal field, the card you can pick up, and the photographic plates.
   ========================================================================= */

/* ---- 1. the gatefold (boot.js) -------------------------------------------
   Obsidian's door, in this page's own material. The invitation is a gatefold
   card: two leaves of paper meeting at a fold, closed by a wax seal, their
   names either side of it. A thread of ink (the ring) is drawn round the seal
   as the real bytes land (--p, 0..1). The moment the ring closes the seal
   breaks on the fold (each leaf carries its own half), the two halves slide
   apart to the edges of the screen like a box being opened, the petals go
   with the gust, and the page behind steps forward into focus.

   The critical-path rules are duplicated inline in index.html so the closed
   card paints on the first frame. Keep the two in step. */

.boot {
  position: fixed;
  inset: 0;
  z-index: 900;
  perspective: 1700px;
  perspective-origin: 50% 50%;
  overflow: hidden;
  --seal: clamp(104px, 22vmin, 168px);
  --tx: 0;
  --ty: 0;
}

.boot__door {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 50%;
  overflow: hidden;
  transition:
    transform 1500ms cubic-bezier(0.62, 0.02, 0.18, 1),
    filter 1500ms ease,
    opacity 500ms ease 1050ms;
  will-change: transform;
}

/* While closed, the leaves stand a hair ajar toward the pointer, so the card
   answers the hand before it is opened (boot.js writes --tx, -1..1). */
.boot__door--l {
  left: 0;
  transform-origin: 0 50%;
  background: linear-gradient(to right, #FBF6EC 0%, #F7EFE1 70%, #EFE3CE 100%);
  transform: rotateY(calc(var(--tx) * 1.4deg));
}

.boot__door--r {
  right: 0;
  transform-origin: 100% 50%;
  background: linear-gradient(to left, #FBF6EC 0%, #F7EFE1 70%, #EFE3CE 100%);
  transform: rotateY(calc(var(--tx) * 1.4deg));
}

/* The fold: a soft valley where the two leaves meet. */
.boot__door--l::after,
.boot__door--r::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  width: 28px;
  pointer-events: none;
}
.boot__door--l::after { right: 0; background: linear-gradient(to left, rgba(74, 27, 14, 0.13), transparent); }
.boot__door--r::after { left: 0; background: linear-gradient(to right, rgba(74, 27, 14, 0.10), transparent); }

/* The printed border, three sides of each leaf, open at the fold. */
.boot__leaf {
  position: absolute;
  inset: clamp(14px, 2.4vmin, 26px);
  border: 1px solid rgba(74, 27, 14, 0.2);
}
.boot__door--l .boot__leaf { right: -1px; border-right: 0; }
.boot__door--r .boot__leaf { left: -1px; border-left: 0; }

.boot__corner {
  position: absolute;
  width: clamp(230px, 52vmin, 560px);
  pointer-events: none;
  translate: calc(var(--tx) * -8px) calc(var(--ty) * -8px);
}
.boot__corner--bl { left: -9%; bottom: -10%; }
.boot__corner--tr { right: -9%; top: -10%; }

.boot__name {
  position: absolute;
  margin: 0;
  font-family: var(--script);
  font-size: clamp(2.2rem, 6.4vmin, 4.6rem);
  line-height: 1;
  color: #4A1B0E;
  white-space: nowrap;
  translate: calc(var(--tx) * 6px) calc(var(--ty) * 6px);
}
.boot__name--l {
  right: calc(var(--seal) * 0.3);
  bottom: calc(50% + var(--seal) * 0.74);
}
.boot__name--r {
  left: calc(var(--seal) * 0.3);
  top: calc(50% + var(--seal) * 0.74);
}

/* The wax. Each leaf holds a whole seal centred on its inner edge, and the
   leaf's own overflow cuts it in half, so when the leaves part the seal
   breaks exactly on the fold with nothing scripted. */
.boot__wax {
  position: absolute;
  top: 50%;
  width: var(--seal);
  height: var(--seal);
  margin-top: calc(var(--seal) / -2);
  border-radius: 49% 51% 47% 53% / 52% 48% 52% 48%;
  background:
    radial-gradient(circle at 38% 32%, rgba(255, 210, 180, 0.35), transparent 34%),
    radial-gradient(circle at 50% 50%, #9A3019 0%, #7A2312 56%, #5A180B 100%);
  box-shadow:
    inset 0 0 0 calc(var(--seal) * 0.07) rgba(70, 18, 8, 0.35),
    inset 0 calc(var(--seal) * 0.03) calc(var(--seal) * 0.06) rgba(255, 190, 160, 0.25),
    0 6px 18px rgba(74, 27, 14, 0.32);
  display: grid;
  place-items: center;
  transition: scale 180ms ease;
}
.boot__door--l .boot__wax { right: calc(var(--seal) / -2); }
.boot__door--r .boot__wax { left: calc(var(--seal) / -2); }

.boot__wax-mark {
  font-family: var(--script);
  font-size: calc(var(--seal) * 0.34);
  line-height: 1;
  color: #E9B79C;
  text-shadow:
    0 1px 0 rgba(60, 12, 4, 0.75),
    0 -1px 0 rgba(255, 220, 200, 0.25);
  translate: 0 0.06em;
}
.boot__wax-mark i { font-style: normal; font-size: 0.74em; padding: 0 0.06em; }

.boot.is-pressed .boot__wax { scale: 0.93; }

/* The thread of ink round the seal: the progress. */
.boot__ring {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--seal) * 1.42);
  height: calc(var(--seal) * 1.42);
  translate: -50% -50%;
  rotate: -90deg;
  overflow: visible;
  pointer-events: none;
  transition: opacity 400ms ease;
}
.boot__track { fill: none; stroke: rgba(74, 27, 14, 0.14); stroke-width: 0.6; }
.boot__prog {
  fill: none;
  stroke: #B8421C;
  stroke-width: 1.1;
  stroke-linecap: round;
  stroke-dasharray: 1;
  stroke-dashoffset: calc(1 - var(--p, 0));
}

/* Ready: the ring has closed, and the seal swells once as it lets go. */
.boot.is-ready .boot__wax { animation: wax-set 380ms ease-out; }
@keyframes wax-set {
  0%, 100% { scale: 1; }
  45% { scale: 1.06; }
}

.boot__rain {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 2;
}

.boot__foot {
  position: absolute;
  z-index: 3;
  bottom: clamp(1.6rem, 7vh, 4rem);
  left: 0;
  right: 0;
  display: grid;
  gap: 0.45rem;
  justify-items: center;
  pointer-events: none;
  transition: opacity 400ms ease;
}

.boot__label {
  margin: 0;
  padding: 0.3rem 0.9rem;
  font-family: var(--sc-font-text);
  font-style: italic;
  font-size: clamp(0.95rem, 0.9rem + 0.3vw, 1.1rem);
  letter-spacing: 0.02em;
  color: #4A1B0E;
  background: rgba(251, 246, 236, 0.86);
  border-radius: 2px;
}

/* The botany on the leaves is alive while the ring fills: each corner sways
   from the corner it grows out of, the two out of step. */
.boot__corner--bl { transform-origin: 0% 100%; animation: boot-sway 5.6s ease-in-out infinite alternate; }
.boot__corner--tr { transform-origin: 100% 0%; animation: boot-sway 6.4s ease-in-out -2.1s infinite alternate; }
@keyframes boot-sway {
  from { rotate: -2.4deg; }
  to { rotate: 2.2deg; }
}

/* Open: a box opened. Owner, round 8: "make it split in half and each side
   goes to the sides of the screen like opening a box." The two halves part
   at the fold and slide out to either edge, each carrying its half of the
   broken seal, and stay solid paper the whole way (no fade: they leave the
   screen). Weighted: pressed together a hair as the seal gives, slow off the
   mark, then carried. The edge that was the fold casts a shadow onto the
   page as the gap opens. */
.boot.is-open { pointer-events: none; }
.boot.is-open .boot__door--l { animation: box-l 1700ms forwards; box-shadow: 14px 0 38px rgba(74, 27, 14, 0.22); }
.boot.is-open .boot__door--r { animation: box-r 1700ms forwards; box-shadow: -14px 0 38px rgba(74, 27, 14, 0.22); }
@keyframes box-l {
  0% { transform: translateX(0); animation-timing-function: cubic-bezier(0.3, 0, 0.6, 1); }
  10% { transform: translateX(0.6%); animation-timing-function: cubic-bezier(0.7, 0, 0.18, 1); }
  100% { transform: translateX(-104%); }
}
@keyframes box-r {
  0% { transform: translateX(0); animation-timing-function: cubic-bezier(0.3, 0, 0.6, 1); }
  10% { transform: translateX(-0.6%); animation-timing-function: cubic-bezier(0.7, 0, 0.18, 1); }
  100% { transform: translateX(104%); }
}
.boot.is-open .boot__ring,
.boot.is-open .boot__foot { opacity: 0; }
.boot[hidden] { display: none; }

/* What waits behind the gates: the page, seen through a little haze and
   shade, which lifts as the gates part. A backdrop filter on the loader's
   own layer, so nothing in the page itself is touched. */
.boot::before {
  content: "";
  position: absolute;
  inset: 0;
  -webkit-backdrop-filter: blur(9px);
  backdrop-filter: blur(9px);
  background: rgba(74, 27, 14, 0.16);
  transition: opacity 1300ms cubic-bezier(0.4, 0, 0.2, 1) 300ms;
}
.boot.is-open::before { opacity: 0; }

/* The words are held back until the gates open, then step forward from just
   behind the threshold rather than simply appearing. */
html:not(.is-booted) .copy-layer { opacity: 0; scale: 0.94; }
html.is-booted .copy-layer {
  transition:
    opacity 900ms ease 120ms,
    scale 1500ms cubic-bezier(0.2, 0.7, 0.2, 1);
  opacity: 1;
  scale: 1;
}

/* ---- 2. the ambient botany ----------------------------------------------
   The planes keep their scroll-driven transforms (those are the depth), and
   gain a slow idle sway on top. This is the "actual background movement":
   with the page untouched, the garland breathes, the corners rock and the
   near leaves drift. Each gets its own period and its own phase, so they
   never pulse together and the composition never reads as a loop.

   The sway is applied on a WRAPPER so it composes with the scroll transform
   instead of overwriting it. One element, one transform. */

.plane-sway {
  position: absolute;
  inset: 0;
  will-change: transform;
}

@keyframes sway-a {
  0%   { transform: translate3d(0, 0, 0) rotate(0deg); }
  50%  { transform: translate3d(1.1%, -0.9%, 0) rotate(0.5deg); }
  100% { transform: translate3d(0, 0, 0) rotate(0deg); }
}
@keyframes sway-b {
  0%   { transform: translate3d(0, 0, 0) rotate(0deg); }
  50%  { transform: translate3d(-1.4%, 1.2%, 0) rotate(-0.7deg); }
  100% { transform: translate3d(0, 0, 0) rotate(0deg); }
}
@keyframes sway-c {
  0%   { transform: translate3d(0, 0, 0) scale(1); }
  50%  { transform: translate3d(0.8%, 1.6%, 0) scale(1.012); }
  100% { transform: translate3d(0, 0, 0) scale(1); }
}

/* Each plane also leans toward the pointer / finger / tilt (bloom.js writes
   --px and --py, -1..1). Nearer planes move further, against the hand. */
.plane-sway--garland { translate: calc(var(--px, 0) * -6px) calc(var(--py, 0) * -4px); }
.plane-sway--tr      { translate: calc(var(--px, 0) * -16px) calc(var(--py, 0) * -12px); }
.plane-sway--bl      { translate: calc(var(--px, 0) * -18px) calc(var(--py, 0) * -14px); }
.plane-sway--sprig   { translate: calc(var(--px, 0) * -26px) calc(var(--py, 0) * -18px); }
.plane-sway--leaves  { translate: calc(var(--px, 0) * -40px) calc(var(--py, 0) * -26px); }

.plane-sway--garland { animation: sway-a 19s ease-in-out infinite; }
.plane-sway--tr      { animation: sway-b 23s ease-in-out infinite -4s; }
.plane-sway--bl      { animation: sway-a 27s ease-in-out infinite -11s; }
.plane-sway--sprig   { animation: sway-c 17s ease-in-out infinite -6s; }
.plane-sway--leaves  { animation: sway-b 31s ease-in-out infinite -17s; }

/* ---- 3. the WebGL petal field -------------------------------------------
   Sits above the flat planes and below the reading field, so petals pass in
   front of the botany and behind the words. Pointer-transparent: it is
   scenery, and it must never eat a scroll or a tap meant for the page.

   The canvas is fixed, not absolute: the world it draws is a space the reader
   is moving through, so it is anchored to the viewport and not to the
   document. See bloom-field.js. */

.field3d {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 6;
  /* Multiply is wrong here (it would make every petal a stain) and screen is
     wrong (it would bleach them). Normal, with the alpha set in the shader. */
  mix-blend-mode: normal;
}

/* ---- 3b. the hero -------------------------------------------------------
   The card as an object, beside the names at full scale. Two columns on a
   desktop; the card sits above the words on a phone.

   The card had to earn its place rather than replace the typography: the
   names set in Pinyon Script at 5.2rem are the one moment this page spends
   real scale on, and shrinking them to 2rem inside a 340px card to make room
   for an interaction would have been a worse page with a better gimmick. */

.hero {
  display: grid;
  grid-template-columns: minmax(0, 0.84fr) minmax(0, 1.16fr);
  align-items: center;
  gap: clamp(1.6rem, 4vw, 3.4rem);
  text-align: left;
}

.hero__words { text-align: left; }
.hero__words .eyebrow { margin-left: 0; max-width: 34ch; }
.hero__words .names { text-align: left; }
.hero__words .card-foot { margin-left: 0; max-width: 32ch; }

/* The copy block is wider than the engine's default measure because it now
   carries two columns rather than one centred one — but NOT as wide as it
   first was (68rem). At 1440px that ran the block to within 70px of the
   viewport edge, which put "Ebenezer Andoh" through the top-right lily spray
   and under the map. The page's right margin belongs to the botany and the
   chrome; the type stops short of it.

   Shifted left of centre for the same reason: the heavy ornament is top-right
   and bottom-left, so the clearest paper on the page is the left-of-middle
   diagonal, and that is where the words go. */
.copy--card {
  width: min(92vw, 58rem);
  left: 46%;
}

@media (max-width: 860px) {
  .hero {
    grid-template-columns: 1fr;
    justify-items: center;
    gap: clamp(1.2rem, 3.4vh, 2rem);
    text-align: center;
  }
  .hero__words, .hero__words .names { text-align: center; }
  .hero__words .eyebrow,
  .hero__words .card-foot { margin-left: auto; margin-right: auto; }
  .copy--card { left: 50%; }
  /* Card and type together must still clear a short viewport, so the card
     gives up size before the names do. */
  .cardobj { width: min(52vw, 208px); transform: none; }
  .cardobj__hint { bottom: -2.2rem; font-size: 0.68rem; }
}

/* The phone hero, art-directed rather than reflowed.
   Stacked at the sizes above it came to 294px of card + a hint + five lines
   of eyebrow + two lines of script names + a date line, which on an 844px
   screen clipped the card off the top, ran the hint into the eyebrow, and
   pushed the date under the map bar at the foot — measured at 190x59px of
   overlap. Every element below gives up something so all of them fit. */
@media (max-width: 700px) {
  .hero { gap: clamp(0.8rem, 2.2vh, 1.4rem); }

  /* Smaller card. It is a second statement of the names, not the primary
     one, so it is the first thing to yield. */
  .cardobj { width: min(43vw, 164px); }

  /* The hint moves INSIDE the card's lower margin instead of hanging below
     it, which is what collided with the eyebrow. */
  .cardobj__hint {
    bottom: 0.5rem;
    font-size: 0.66rem;
    letter-spacing: 0.1em;
    color: var(--sc-ink-soft);
  }

  /* Wider measure, fewer lines: five lines of 22ch became three of 36ch. */
  .hero__words .eyebrow {
    max-width: 36ch;
    font-size: 0.7rem;
    line-height: 1.7;
    letter-spacing: 0.06em;
  }

  .hero__words .names { margin-top: clamp(0.7rem, 2vh, 1.2rem); }
  .names__a, .names__b { font-size: clamp(1.6rem, 8.6vw, 2.3rem); }
  .names__amp { margin: 0.15rem 0; font-size: 0.7rem; }

  .hero__words .card-foot {
    margin-top: clamp(0.8rem, 2.4vh, 1.4rem);
    font-size: 0.75rem;
    max-width: 30ch;
  }

  /* The whole hero sits above the foot bar rather than centred through it. */
  .copy--card { top: 46%; }
}

/* ---- 4. the card as an object -------------------------------------------
   Their printed card, on preserve-3d, with the printed face on the front and
   their photograph on the back. Draggable, throwable, keyboard-operable, and
   breathing when idle. See bloom-loader.js.

   This is the page's one real object, and it is the hero: the first thing the
   reader meets is a thing they can pick up. */

.cardobj {
  position: relative;
  width: min(74vw, 340px);
  aspect-ratio: 0.707;             /* A-series, like the real card */
  margin: 0 auto;
  perspective: 1100px;
  cursor: grab;
  touch-action: pan-y;             /* vertical scroll still belongs to the page */
  border: 0;
  background: none;
  padding: 0;
  /* Pull the card up into the hero's optical centre rather than its
     mathematical one. */
  transform: translateY(-1.5vh);
}

.cardobj:focus-visible {
  outline: 2px solid var(--sc-accent);
  outline-offset: 14px;
  border-radius: 2px;
}

.cardobj.is-held { cursor: grabbing; }

.cardobj__inner {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  will-change: transform;
  /* NO `filter` HERE. A filter on this element creates a containing block
     that FLATTENS the 3D rendering context, which silently cancels both
     preserve-3d and the children's backface-visibility. The symptom is the
     front face showing through the back as mirrored text, which is exactly
     what the first screenshot of this card showed. The shadow belongs on the
     faces, as a box-shadow, where it costs nothing. */
}

.cardobj__face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  overflow: hidden;
  background: #FDFAF3;
  border: 1px solid color-mix(in oklab, var(--sc-ink) 13%, transparent);
  display: grid;
  place-items: center;
  box-shadow:
    0 2px 3px rgba(74, 27, 14, 0.07),
    0 24px 34px rgba(74, 27, 14, 0.16);
}

/* The printed front: her card's own ornament, at card scale. */
.cardobj__face--front {
  background-image:
    radial-gradient(70% 50% at 50% 50%, #FDFAF3 60%, #F5EBD9 100%);
}

.cardobj__corner {
  position: absolute;
  width: 62%;
  opacity: 0.92;
  pointer-events: none;
}
.cardobj__corner--tr { top: -6%; right: -10%; }
.cardobj__corner--bl { bottom: -8%; left: -12%; width: 66%; }

.cardobj__print {
  position: relative;
  z-index: 2;
  text-align: center;
  padding: 0 12%;
  display: grid;
  gap: 0.5rem;
}

/* 0.7rem = 11.2px, the functional-text floor. The card is an interactive
   control, so everything printed on it is functional text however
   decoratively it is set: 0.6rem rendered at 9.6px and failed. */
/* The browser's paragraph margins, stated, so the card keeps its air under
   a CSS reset (the Next.js port runs under Tailwind's). */
.cardobj__eyebrow,
.cardobj__names,
.cardobj__when { margin: 1em 0; }

.cardobj__eyebrow {
  font-size: 0.7rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--sc-ink-soft);
}

.cardobj__names {
  font-family: var(--script);
  font-size: clamp(1.5rem, 7.4vw, 2.1rem);
  line-height: 1.12;
  color: var(--sc-ink);
}

.cardobj__rule {
  width: 42%;
  height: 1px;
  margin: 0.2rem auto;
  background: color-mix(in oklab, var(--sc-accent) 52%, transparent);
}

.cardobj__when {
  font-size: 0.7rem;        /* 11.2px floor, as above */
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--sc-ink-soft);
}

/* The back: their photograph, which is the reward for turning it over. */
.cardobj__face--back {
  transform: rotateY(180deg);
  padding: 5%;
  background: #F5EBD9;
}

.cardobj__photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  border: 1px solid color-mix(in oklab, var(--sc-ink) 10%, transparent);
}

.cardobj__hint {
  position: absolute;
  left: 50%;
  bottom: -2.9rem;
  transform: translateX(-50%);
  white-space: nowrap;
  font-size: 0.7rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--sc-ink-soft);
  opacity: 0.92;
  animation: hint-breathe 3.4s ease-in-out infinite;
  pointer-events: none;
}

@keyframes hint-breathe {
  0%, 100% { opacity: 0.45; }
  50%      { opacity: 0.95; }
}

/* Once the reader has turned it, the instruction has done its job. */
.cardobj.is-touched .cardobj__hint {
  opacity: 0;
  animation: none;
  transition: opacity 500ms ease;
}

/* ---- 5. the album (src/album.js) ----------------------------------------
   The WebGL stage draws all five photographs. It sits above the paper bloom
   (.field, z 7) so nothing fogs them, and below the words (z 20). The
   <figure>s are the album for a screen reader, and the album itself when
   there is no WebGL or motion is reduced (html.album-dom). */

.album-gl-canvas {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 8;
  pointer-events: none;
}

.copy--album {
  top: 50%;
  width: auto;
  pointer-events: none;
}

/* The table. Round 8: the prints are scattered, each in its own format
   (src/album.js dress() sets data-frame and the words). This is the WebGL
   stage's table set in CSS: what a guest without WebGL, or with reduced
   motion, sees, each print fading in at its place, in order. */
.album {
  position: relative;
  width: min(94vw, 1180px);
  height: min(84vh, 820px);
}

.album__print {
  position: absolute;
  margin: 0;
  width: 23%;
  padding: 4.5%;
  box-sizing: content-box;
  background: #FBF6EC;
  box-shadow: 0 2px 2px rgba(74, 27, 14, 0.05), 0 18px 36px rgba(74, 27, 14, 0.18);
  container-type: inline-size;
  opacity: 0;
}
.album__print img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
}

/* Padding is a share of the TABLE's width in CSS, not the print's, so the
   margins are set in the print's own units instead. */
.album__print[data-frame] { padding: 0; }
.album__print[data-frame="album"] { left: 3%; top: 4%; rotate: -7deg; padding: 1.05%; }
.album__print[data-frame="album"]::before {
  /* The four oxblood corners over the photograph. */
  content: "";
  position: absolute;
  inset: 4.5cqi;
  pointer-events: none;
  background:
    linear-gradient(135deg, #7A2312 0 50%, transparent 50%) top left / 9cqi 9cqi no-repeat,
    linear-gradient(225deg, #7A2312 0 50%, transparent 50%) top right / 9cqi 9cqi no-repeat,
    linear-gradient(45deg, #7A2312 0 50%, transparent 50%) bottom left / 9cqi 9cqi no-repeat,
    linear-gradient(315deg, #7A2312 0 50%, transparent 50%) bottom right / 9cqi 9cqi no-repeat;
}
.album__print[data-frame="polaroid"] {
  left: 73%; top: 2%; rotate: 6deg;
  padding: 1.4% 1.4% 5.5%;
  background: #F9F8F4;
}
.album__print[data-frame="polaroid"] img { aspect-ratio: 0.92; }
.album__print[data-frame="keyline"] { left: 37.5%; top: 13%; width: 26%; rotate: -1.5deg; padding: 2.1% 2.1% 4.2%; }
.album__print[data-frame="keyline"] img { outline: 1px solid #7A2312; outline-offset: 2.8cqi; }
.album__print[data-frame]::after {
  /* The words on the bottom margin: their names in pen, or the date. */
  content: attr(data-cap);
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  text-align: center;
  color: #4A1B0E;
}
.album__print[data-frame="polaroid"]::after {
  height: 24cqi;
  line-height: 24cqi;
  font-family: var(--script);
  font-size: 10.5cqi;
  rotate: -2deg;
}
.album__print[data-frame="keyline"]::after {
  height: 16cqi;
  line-height: 16cqi;
  font-family: var(--sc-font-text);
  font-size: 4.2cqi;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.album__print[data-frame="deckle"] {
  left: 8%; top: 51%; rotate: 4deg; padding: 1.75%;
  box-shadow: none;
  filter: drop-shadow(0 12px 18px rgba(74, 27, 14, 0.2));
}
.album__print[data-frame="locket"] { left: 70%; top: 48%; rotate: -5deg; padding: 2.1%; }
.album__print[data-frame="locket"] img {
  border-radius: 50%;
  box-shadow: 0 0 0 1.4cqi #FBF6EC, 0 0 0 1.9cqi #7A2312, 0 0 0 3cqi #FBF6EC, 0 0 0 3.3cqi rgba(122, 35, 18, 0.5);
}

/* With the stage running, the figures stay in the document (and in the
   accessibility tree) but are never painted. */
html.album-gl .album__print { opacity: 0 !important; }

@media (max-width: 760px) {
  .album { width: 94vw; height: 80vh; }
  .album__print { width: 42%; }
  .album__print[data-frame="album"] { left: 2%; top: 3%; padding: 1.9%; }
  .album__print[data-frame="polaroid"] { left: 52%; top: 2%; padding: 2.5% 2.5% 10%; }
  .album__print[data-frame="deckle"] { left: 4%; top: 60%; padding: 3.2%; }
  .album__print[data-frame="locket"] { left: 52%; top: 58%; padding: 3.8%; }
  .album__print[data-frame="keyline"] { left: 22%; top: 27%; width: 54%; padding: 4.3% 4.3% 8.6%; }
}

/* ---- 7. the transitions -------------------------------------------------
   One DIFFERENT move at every station boundary. They all read the variables
   bloom.js publishes on each copy block (--e/--x entering and exiting, the
   eased and staggered variants, --h for the held drift) and each decides for
   itself what to do with them. The engine keeps the opacity fade; nothing
   here touches opacity except where a mover needs to be invisible before it
   has "arrived".

   Rest state: --e = 1 and --x = 0, so every expression below resolves to
   translate(0) / scale(1) / rotate(0). That is also exactly what reduced
   motion gets. */

.mv { will-change: transform; }

/* 1. THE CARD IS TOSSED AWAY.
   While the flowers open (--h, 0..1 across the first 20%) the whole hero
   leans away from you; when the vow takes over (--xe) it is thrown off to the
   left like a page turned, shrinking as it goes. */
.mv--toss {
  transform-origin: 0% 55%;
  transform:
    perspective(1500px)
    translate3d(calc(var(--xe, 0) * -40vw), calc(var(--xe, 0) * -8vh), 0)
    rotateY(calc(var(--h, 0) * 9deg + var(--xe, 0) * 62deg))
    rotateZ(calc(var(--xe, 0) * -7deg))
    scale(calc(1 - var(--h, 0) * 0.05 - var(--xe, 0) * 0.22));
}

/* 2. THE VOW RISES FROM MASKS, AND LEAVES TO THE RIGHT.
   Each line is cut out of its own mask and rises into place, the second a
   beat behind the first. On the way out they go the OPPOSITE way, sideways,
   so the exit is not the entrance played backwards. */
.vow__line {
  display: block;
  overflow: hidden;
  /* Room for ascenders and descenders: a mask clipped to the line box shears
     the tails off g, y and p, the most common way this effect looks broken. */
  padding: 0.14em 0 0.2em;
  margin: -0.14em 0 -0.2em;
}

.vow__in {
  display: block;
  will-change: transform;
}

.vow__in--1 {
  transform: translate3d(calc(var(--xe, 0) * 48vw), calc((1 - var(--ee, 1)) * 118%), 0);
}
.vow__in--2 {
  transform: translate3d(calc(var(--xe2, 0) * 48vw), calc((1 - var(--ee2, 1)) * 118%), 0);
}

/* 4. THE DAY DROPS IN, AND LEAVES TO THE LEFT.
   Three cells fall from above one after another with overshoot, so each lands
   with a small bounce, and on the way out they slide off sideways in the same
   order. */
.plate__cell { will-change: transform; }

.plate__cell--1 {
  transform:
    translate3d(calc(var(--xe, 0) * -62vw), calc((1 - var(--eb, 1)) * -17vh), 0)
    rotate(calc((1 - var(--eb, 1)) * -8deg));
}
.plate__cell--2 {
  transform:
    translate3d(calc(var(--xe2, 0) * -62vw), calc((1 - var(--eb2, 1)) * -17vh), 0)
    rotate(calc((1 - var(--eb2, 1)) * 6deg));
}
.plate__cell--3 {
  transform:
    translate3d(calc(var(--xe3, 0) * -62vw), calc((1 - var(--eb3, 1)) * -17vh), 0)
    rotate(calc((1 - var(--eb3, 1)) * -5deg));
}

/* 5. THE REPLY IS DEALT IN FROM THE RIGHT.
   The sheet slides in from beyond the right edge with a tilt, like a card
   dealt across a table; its four rows then follow one at a time. */
.mv--deal {
  transform:
    translate3d(calc((1 - var(--ee, 1)) * 82vw), calc((1 - var(--ee, 1)) * 4vh), 0)
    rotate(calc((1 - var(--ee, 1)) * 8deg));
}

.reply__list li:nth-child(1) {
  transform: translate3d(calc((1 - var(--ee2, 1)) * 52px), 0, 0);
  opacity: clamp(0, calc(var(--ee2, 1) * 1.8 - 0.3), 1);
}
.reply__list li:nth-child(2) {
  transform: translate3d(calc((1 - var(--ee3, 1)) * 52px), 0, 0);
  opacity: clamp(0, calc(var(--ee3, 1) * 1.8 - 0.3), 1);
}
.reply__list li:nth-child(3) {
  transform: translate3d(calc((1 - var(--ee4, 1)) * 52px), 0, 0);
  opacity: clamp(0, calc(var(--ee4, 1) * 1.8 - 0.3), 1);
}
.reply__list li:nth-child(4) {
  transform: translate3d(calc((1 - var(--ee5, 1)) * 52px), 0, 0);
  opacity: clamp(0, calc(var(--ee5, 1) * 1.8 - 0.3), 1);
}

@media (max-width: 700px) {
  .mv--toss { transform-origin: 50% 40%; }
}

@media (prefers-reduced-motion: reduce) {
  /* The variables are pinned to their resting values in bloom.js, so these
     already resolve to identity; this makes it explicit and survives anyone
     later adding a mover that does not default cleanly. */
  .mv, .vow__in, .plate__cell, .reply__list li { transform: none !important; }
  .reply__list li { opacity: 1 !important; }
}

/* ---- 5b. the place (owner 2026-11-02) ------------------------------------
   The church on a live Google map, mounted in the album's own ivory mat and
   oxblood corners so it reads as one more page of the same book. */
.copy--place { top: 50%; width: min(92vw, 42rem); }

.place__sheet {
  background: color-mix(in oklab, var(--sc-canvas) 95%, transparent);
  backdrop-filter: blur(7px);
  border: 1px solid color-mix(in oklab, var(--sc-ink) 9%, transparent);
  border-radius: 2px;
  padding: clamp(1.3rem, 3.4vh, 2.3rem) clamp(1.1rem, 3.4vw, 2.6rem);
  box-shadow: 0 10px 40px rgba(74, 27, 14, 0.08);
  text-align: center;
}

.place__head {
  margin: 0;
  font-family: var(--script);
  font-weight: 400;
  font-size: clamp(1.9rem, 6.2vw, 2.9rem);
  line-height: 1.1;
  color: var(--sc-ink);
}

.place__name {
  margin: 0.4rem auto 0;
  max-width: 28ch;
  font-size: clamp(1rem, 0.95rem + 0.3vw, 1.2rem);
  line-height: 1.35;
  color: var(--sc-ink-soft);
  text-wrap: balance;
}

/* The mat: an ivory border round the map, like a print in the album. */
.place__map {
  position: relative;
  margin: clamp(1rem, 2.6vh, 1.6rem) 0 0;
  padding: clamp(8px, 1.4vmin, 14px);
  background: #FBF7EF;
  box-shadow:
    0 1px 0 rgba(74, 27, 14, 0.06),
    0 14px 30px -12px rgba(74, 27, 14, 0.35);
  aspect-ratio: 16 / 10;
  max-height: 42vh;
  width: 100%;
  box-sizing: border-box;
}

.place__frame {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
  background: #EFE6D6;
  /* Warmed toward the card's paper, so Google's cool greys sit in the page. */
  filter: sepia(0.2) saturate(0.85) contrast(0.97);
  /* Inert until the guest asks: a map that takes the scroll gesture is the
     classic trap on a scrolling page, worst of all under a thumb. */
  pointer-events: none;
}
.place__map.is-live .place__frame { pointer-events: auto; filter: none; }

.place__explore {
  position: absolute;
  left: 50%;
  bottom: clamp(16px, 3vmin, 26px);
  translate: -50% 0;
  margin: 0;
  padding: 0.5rem 1.1rem;
  border: 1px solid color-mix(in oklab, var(--sc-ink) 22%, transparent);
  border-radius: 999px;
  background: rgba(251, 246, 236, 0.94);
  font: italic 0.95rem/1.2 var(--sc-font-text);
  color: var(--sc-ink);
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(74, 27, 14, 0.16);
}
.place__explore:hover { background: #FBF6EC; }
.place__explore:focus-visible { outline: 2px solid var(--sc-accent); outline-offset: 3px; }
.place__map.is-live .place__explore { display: none; }

/* While the map is live, a way out that works with a thumb. Escape alone is
   not enough: focus is inside Google's frame then, and its keys never reach
   this page. */
.place__done {
  display: none;
  position: absolute;
  top: clamp(16px, 3vmin, 24px);
  right: clamp(16px, 3vmin, 24px);
  margin: 0;
  padding: 0.4rem 1rem;
  border: 0;
  border-radius: 999px;
  background: var(--sc-ink);
  color: #FBF6EC;
  font: 0.9rem/1.2 var(--sc-font-text);
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(74, 27, 14, 0.25);
}
.place__done:focus-visible { outline: 2px solid var(--sc-accent); outline-offset: 3px; }
.place__map.is-live .place__done { display: block; }

/* The album's photo corners, in CSS this time. */
.place__corner {
  position: absolute;
  width: clamp(18px, 3vmin, 28px);
  height: clamp(18px, 3vmin, 28px);
  background: #7A2312;
  box-shadow: 0 1px 2px rgba(60, 12, 4, 0.35);
  pointer-events: none;
}
.place__corner--tl { top: 2px; left: 2px; clip-path: polygon(0 0, 100% 0, 0 100%); }
.place__corner--tr { top: 2px; right: 2px; clip-path: polygon(0 0, 100% 0, 100% 100%); }
.place__corner--bl { bottom: 2px; left: 2px; clip-path: polygon(0 0, 100% 100%, 0 100%); }
.place__corner--br { bottom: 2px; right: 2px; clip-path: polygon(100% 0, 100% 100%, 0 100%); }

.place__go {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 0.6rem 1.4rem;
  margin: clamp(1rem, 2.6vh, 1.5rem) 0 0;
}

.place__btn {
  display: inline-block;
  padding: 0.7rem 1.6rem;
  border-radius: 2px;
  background: var(--sc-accent);
  color: var(--sc-accent-ink);
  font-size: 1rem;
  letter-spacing: 0.02em;
  text-decoration: none;
  box-shadow: 0 6px 16px -6px rgba(74, 27, 14, 0.5);
  transition: background 160ms ease;
}
.place__btn:hover { background: #8E3013; }

.place__alt {
  color: var(--sc-accent);
  font-size: 1rem;
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-thickness: 1px;
}

.place__btn:focus-visible,
.place__alt:focus-visible { outline: 2px solid var(--sc-accent); outline-offset: 3px; }

@media (max-width: 760px) {
  .place__map { aspect-ratio: 4 / 3.4; }
}

/* THE PLACE STANDS UP, AND LIFTS AWAY.
   It arrives lying back on the table and is stood upright, hinged at its
   foot, like a card propped up to be read; when the reply is dealt in it
   lifts up and tips toward you on its way out. Neither side-slides nor drops,
   which its neighbours already do. */
.mv--stand {
  transform-origin: 50% 100%;
  transform:
    perspective(1300px)
    translate3d(0, calc((1 - var(--ee, 1)) * 10vh - var(--xe, 0) * 26vh), 0)
    rotateX(calc((1 - var(--ee, 1)) * 64deg - var(--xe, 0) * 22deg));
}

/* ---- 6. the colophon ----------------------------------------------------
   The photographer is credited. Their watermark is left in the plates, and
   this names the studio in words as well. */

.colophon {
  margin: 2.6rem 0 1em;
  font-size: 0.75rem; /* 12px: the photographer credit is read, not scanned */
  letter-spacing: 0.04em;
  color: var(--sc-ink-soft);
  text-align: center;
}

/* ---- motion off ---------------------------------------------------------
   The composition must still be complete and the story still readable. Planes
   hold at a hand-set mid position rather than collapsing to zero, so the
   static page is the composed card rather than a flat stack. */

@media (prefers-reduced-motion: reduce) {
  .plane { transition: none !important; }

  /* The new work, switched off at the source rather than hidden: no sway, no
     aperture, no breathing hint, no pointer tilt, and the boot screen leaves
     by a plain fade. The WebGL field holds a single still frame (uTime is
     pinned to 0 in the shader), so it stays as composition without motion. */
  .plane-sway, .plane-sway--garland, .plane-sway--tr, .plane-sway--bl,
  .plane-sway--sprig, .plane-sway--leaves { animation: none !important; }
  .boot { transition: opacity 240ms ease; }
  .boot__door { transition: none; }
  .boot.is-open { opacity: 0; }
  .boot.is-open .boot__door--l, .boot.is-open .boot__door--r { transform: none; animation: none; }
  .boot.is-ready .boot__wax { animation: none; }
  .boot__corner { animation: none; }
  .plane-sway { translate: none !important; }
  .cardobj__hint { animation: none; opacity: 0.9; }
  html:not(.is-booted) .copy-layer { opacity: 1; scale: 1; }
  .plane--garland { transform: translate3d(1vw, 4vh, 0) scale(1.02); opacity: 0.7; }
  .plane--tr { transform: translate3d(3vw, -3vh, 0) rotate(3deg) scale(1.1); }
  .plane--bl { transform: translate3d(-3vw, 3vh, 0) rotate(-3deg) scale(1.12); }
  .plane--sprig { transform: translate3d(20vw, 10vh, 0) rotate(4deg); opacity: 0.72; }
  .plane--leaves { transform: translate3d(-4vw, 12vh, 0) scale(1.18); opacity: 0.54; }
  .frame__rect { stroke-dashoffset: 0; opacity: 1; }
  .petal { display: none; }
}
