/* ============================================================
   MERCE & IAN - site.css
   The scrolling one-pager: page shell · sticky nav · hero · sections.

   This is the counterpart to coming-soon.css. That file makes its page a
   fixed, unscrollable screen; this one makes a long scroll. They are never
   loaded together.

   Load order: fonts.css → tokens.css → scene.css → invite.css → gate.css → this file

   NO TYPE SCALE YET. Every size in this project is a hand-tuned clamp() bound
   to its own context, and there are no real section headings to generalise
   from - .sec-h below is the first specimen. Revisit once three real sections
   exist, so the scale is evidence rather than a guess.
   ============================================================ */
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}

:root{
  /* Shell measurements, not design tokens - hence here and not tokens.css. */
  --nav-h:clamp(56px,7vw,76px);
  /* How far the light half rides up over the hero, and equally how far the
     pack lifts so the panel does not slice it. One number so they cannot
     drift apart.

     ON :root, NOT ON .hero. Custom properties inherit down the tree, not
     across it, and the section that needs this is the hero's SIBLING - set on
     .hero the negative margin resolved to nothing and the two simply butted
     together, which looked exactly like it had worked. */
  --seam:clamp(64px,9vh,132px);
}

html{scroll-behavior:smooth}
body{
  background:var(--espresso);color:var(--text);
  font-family:var(--font-text);
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden;   /* net for .cs-title's white-space:nowrap on tiny screens */
}

/* ---- skip link (distinct from the gate's "Skip the intro") ---- */
.skip-link{position:absolute;left:-9999px;top:0;z-index:calc(var(--z-gate) + 1);
  font-family:var(--font-ui);font-size:12px;letter-spacing:.18em;text-transform:uppercase;
  background:var(--espresso);color:var(--tan-soft);border:1px solid var(--gold);
  padding:14px 22px;border-radius:3px}
.skip-link:focus{left:12px;top:12px}

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

/* ============================================================
   NAV - fixed, so the hero can run full-bleed underneath it
   ============================================================ */
/* THE BAR IS NOT THERE UNTIL YOU HAVE LEFT THE HERO. It used to sit over it,
   transparent, and it cost the composition its whole top-right corner - which
   is exactly where the invitation wants to be. It now slides down once the
   hero is behind you, and the rail on the left comes with it.
   pointer-events go with the opacity, or an invisible bar keeps eating clicks
   meant for the hero. */
.nav{position:fixed;top:0;left:0;right:0;z-index:var(--z-nav);
  display:flex;align-items:center;justify-content:space-between;gap:clamp(.8rem,2vw,2rem);
  min-height:var(--nav-h);padding:0 var(--sp-gutter);
  background:transparent;border-bottom:1px solid transparent;
  opacity:0;transform:translateY(-100%);pointer-events:none;
  transition:background .45s ease,border-color .45s ease,opacity .5s ease,
             transform .55s cubic-bezier(.16,1,.3,1),
             -webkit-backdrop-filter .45s ease,backdrop-filter .45s ease}
.nav.is-in{opacity:1;transform:none;pointer-events:auto}
/* The blur is applied ONLY once the hero has scrolled past. Keeping it off the
   animating mesh/fireflies is where the frames actually are on mid-range phones. */
/* LIGHT, BECAUSE THE BAR IS ONLY EVER SEEN OVER LIGHT. One observer brings
   .is-in and .is-solid on together once the hero is behind you, and everything
   below the hero is cream - so there is no dark state to carry, and no
   two-theme nav to keep in sync. */
.nav.is-solid{background:rgba(244,239,230,0.92);
  -webkit-backdrop-filter:blur(10px) saturate(1.1);backdrop-filter:blur(10px) saturate(1.1);
  border-bottom-color:var(--rule-ink)}

/* THEIR NAMES, NOT THEIR INITIALS. In the display face rather than the names'
   script: at this size the script's capital I reads as a J, and "Jan" is the
   one thing the bar must never say. The ampersand keeps the script, as it does
   everywhere else, a size up because that face sets small for its em, and in
   gold-deep: plain gold is ornament only on cream. */
/* THE COUPLE'S MONOGRAM. One image, used only for its shape: it is a mask,
   and the colour is whatever the element's own colour is, so the same file
   is burgundy in the bar, gold in the footer and gold on the gate. */
.mono{display:block;aspect-ratio:720/662;background:currentColor;
  -webkit-mask:url('../assets/monogram.webp') center/contain no-repeat;
          mask:url('../assets/monogram.webp') center/contain no-repeat}
.nav-mark{display:inline-flex;align-items:center;gap:.6rem;font-family:var(--font-display);font-size:1.3rem;letter-spacing:.01em;
  color:var(--burgundy);text-decoration:none;white-space:nowrap;padding:8px 0}
/* tall enough that the letters carry; the hairline swashes go faint at
   this size, which is fine, the M and I are what read */
.nav-mono{flex:none;height:clamp(34px,4vw,42px)}
/* its slant leans it into Ian, so it takes a touch more room on that side */
.nav-mark .amp,.nav-panel-mark .amp{font-size:1.35em;color:var(--gold-deep);padding:0 .14em 0 .02em}

.nav-links{display:flex;align-items:center;gap:clamp(.6rem,1.7vw,1.6rem)}
/* the panel's own masthead. It only exists once the bar has collapsed;
   in the wide bar the real .nav-mark is already on the left. */
.nav-panel-mark{display:none}
.nav-links a{font-family:var(--font-ui);font-size:11px;font-weight:500;letter-spacing:.18em;
  text-transform:uppercase;color:var(--ink-mute);text-decoration:none;white-space:nowrap;
  padding:14px 2px;position:relative;transition:color .3s ease}
.nav-links a::after{content:"";position:absolute;left:0;right:0;bottom:8px;height:1px;
  background:var(--olive);transform:scaleX(0);transform-origin:50%;transition:transform .35s ease}
.nav-links a:hover{color:var(--ink)}
.nav-links a:hover::after{transform:scaleX(1)}

/* Filled rather than outlined: gold on cream is 2.1:1 and the outline version
   simply disappeared. Burgundy carries white at 9.6:1. */
/* The same button as every other call to action on the page: square-ish
   corners, the burgundy gradient, the soft burgundy shadow. It was a pill,
   the only one on the site. */
.nav-cta{color:#fff!important;border:1px solid var(--burgundy-deep);border-radius:3px;
  background:linear-gradient(180deg,var(--burgundy),var(--burgundy-deep));
  box-shadow:0 1px 2px rgba(36,28,23,.12),0 8px 18px -10px rgba(110,36,54,.55);
  padding:10px 20px!important;margin-left:clamp(.2rem,1vw,.7rem);
  transition:background .3s ease,border-color .3s ease,box-shadow .3s ease,transform .3s ease}
.nav-cta::after{display:none}
.nav-cta:hover{border-color:var(--burgundy);transform:translateY(-1px);
  background:linear-gradient(180deg,#7d2a3e,var(--burgundy));
  box-shadow:0 2px 5px rgba(36,28,23,.14),0 12px 24px -10px rgba(110,36,54,.6)}

/* z-index is within .nav's own stacking context, where the slide-out panel is a
   sibling at --z-nav-panel. Without this the panel covers its own close button. */
.nav-toggle{display:none;position:relative;z-index:calc(var(--z-nav-panel) + 1);
  width:44px;height:44px;align-items:center;justify-content:center;
  flex-direction:column;gap:5px;background:none;border:0;cursor:pointer}
.nav-toggle span{display:block;width:22px;height:1.5px;background:var(--ink);
  transition:transform .3s ease,opacity .3s ease}
.nav-toggle[aria-expanded="true"] span:nth-child(1){transform:translateY(6.5px) rotate(45deg)}
.nav-toggle[aria-expanded="true"] span:nth-child(2){opacity:0}
.nav-toggle[aria-expanded="true"] span:nth-child(3){transform:translateY(-6.5px) rotate(-45deg)}

/* ---- nav collapse. A category apart from the 640/480 layout breakpoints:
        nine labels can't be clamped into a narrow bar, and clamp() solves
        scaling, not wrapping. Measured: the row overflows below ~1180px
        even with the short labels, and nowrap makes an overflow overlap
        rather than wrap. ---- */
@media (max-width:1180px){
  /* NO BACKDROP-FILTER ON THE BAR AT THIS WIDTH. A backdrop-filter makes an
     element the containing block for its `position:fixed` descendants, and the
     panel is one of them: once the nav went solid past the hero, `inset:0 0 0
     auto` resolved against the BAR instead of the viewport, so the panel became
     a 104px-tall box and its links were laid out above the top of the screen.
     The bar is opaque here instead, which it can afford to be, and the panel
     carries a blur of its own anyway. */
  .nav.is-solid{background:var(--cream);
    -webkit-backdrop-filter:none;backdrop-filter:none}
  .nav-toggle{display:flex}
  /* FULL BLEED, AND LIGHT. It was a 340px drawer in espresso, which on a phone
     is a dark slab beside a strip of the page you can still see moving. Across
     the whole screen it is the page giving way to the menu, and cream because
     that is what the last full-screen overlay here settled on: the processional
     was built dark first and read as a cinema rather than part of the site.

     An explicit height as well as the inset, because the bar's own slide-in is
     a transform and a transform is ALSO a containing block for fixed children:
     during that half second the panel would size itself to the bar again. */
  .nav-links{position:fixed;inset:0;height:100vh;height:100svh;width:100%;
    z-index:var(--z-nav-panel);
    flex-direction:column;align-items:center;justify-content:center;gap:.15rem;
    padding:calc(var(--nav-h) + 1.5rem) var(--sp-gutter) 2rem;
    background:var(--cream);border-left:0;
    -webkit-backdrop-filter:none;backdrop-filter:none;
    transform:translateY(-100%);transition:transform .48s cubic-bezier(.2,.7,.2,1);
    overflow-y:auto;overscroll-behavior:contain}
  .nav-links.is-open{transform:none}
  /* their initials at the head of it: the bar's own mark is behind the panel */
  .nav-panel-mark{display:block;margin-bottom:clamp(1.4rem,5vh,2.8rem);
    font-family:var(--font-display);font-size:1.45rem;letter-spacing:.01em;
    color:var(--burgundy)}
  /* the display face, sentence case and large: a full screen of 12px uppercase
     tracking reads as a table of contents, not a way in */
  .nav-links a{font-family:var(--font-display);font-size:clamp(1.5rem,6.2vw,2.1rem);
    font-weight:400;letter-spacing:.01em;text-transform:none;
    padding:clamp(.45rem,1.6vh,.8rem) .5rem;width:auto;text-align:center;
    color:var(--ink-mute)}
  .nav-links a:hover,.nav-links a:focus-visible{color:var(--ink)}
  .nav-links a::after{display:none}
  .nav-cta{margin-top:clamp(1.2rem,4vh,2.2rem);
    padding:15px 30px!important;text-align:center;
    font-family:var(--font-ui)!important;font-size:11px!important;
    letter-spacing:.22em!important;text-transform:uppercase!important}
}
/* Shared scroll lock. gate.css arms the same mechanism with .gate-armed. */
/* THE PAGE HAS TO STAY WHERE IT WAS, not just come back afterwards. Locking
   with `overflow:hidden` alone drops the scroll to 0, and the panel is only
   84vw wide, so the strip beside it showed the top of the hero while the menu
   was open. Pinning the body at its own negative offset holds the view still;
   js/site.js sets that offset and undoes it. */
html.nav-open{overflow:hidden}
html.nav-open body{position:fixed;left:0;right:0;width:100%;overflow:hidden}

/* ============================================================
   HERO - the forest scene, now a section you can scroll past
   ============================================================ */
/* TALLER BY THE OVERLAP. The light panel rides up over the hero by --seam, so
   a hero of exactly 100svh shows 100svh minus --seam and the next section
   appears at the bottom of the first screen. It has to give back what the
   panel takes. */
.hero{position:relative;min-height:calc(100vh + var(--seam));
      min-height:calc(100svh + var(--seam));
  display:flex;align-items:center;justify-content:center;text-align:center;
  /* clip, not hidden: hidden is a scroll container, and the scroll-out below
     resolves view() against the nearest one. See the note on .sec. */
  overflow:hidden;overflow:clip;
  /* seals the scene's local 0–9 z-ladder so it can never argue with nav/gate */
  isolation:isolate;
  padding-top:clamp(18px,3vh,34px);
  /* The scroll cue and its paw trail are gone for now, so nothing is pinned
     to the hero's foot any more - this is just breathing room under the pack. */
  padding-bottom:clamp(20px,3vh,34px)}
/* Sits at the hero's BOTTOM edge, not its top. site.js observes it with the
   nav's height as a negative top rootMargin, so it stops intersecting at the
   moment the hero has scrolled up behind the bar - which is precisely when
   the nav should go solid. At the top of the hero it would be inside the
   inset from the very first frame and the nav would load solid.
   The 120px lift makes the bar opaque a little before the foot of the hero
   reaches it. */
/* scene.css frames the whole viewport with .cs-frame at inset:22px. Here it's
   a hero flourish sitting under a fixed bar, so drop its top edge clear of
   the nav rather than letting a gold hairline run through the links. */
/* the frame can sit where scene.css puts it now that no bar crosses it */
.hero .cs-frame{top:22px}

.nav-sentinel{position:absolute;bottom:120px;left:0;width:1px;height:1px;pointer-events:none}

/* ============================================================
   THE RAIL - where you are in the page, and how to get elsewhere

   A hairline down the left margin with a numeral per section. The fill
   tracks how far through the page you are; the numeral you are level with
   brightens and unfurls its name.

   This is the one place in the project that genuinely needs a scroll
   listener rather than an IntersectionObserver: a progress fill is a
   continuous value, not a threshold. It is passive and rAF-coalesced, so it
   still only touches the DOM once a frame.
   ============================================================ */
.rail{position:fixed;left:clamp(14px,2.1vw,38px);top:50%;z-index:var(--z-nav);
  transform:translateY(-50%);height:min(58vh,520px);
  display:flex;align-items:stretch;
  opacity:0;pointer-events:none;transition:opacity .7s ease}
.rail.is-in{opacity:1}
.rail.is-in.is-away{opacity:0;pointer-events:none}
.rail-line{position:relative;display:block;width:1px;
  background:var(--rule-ink)}
/* --gold-deep, not --gold: see the contrast table in tokens.css. */
.rail-fill{position:absolute;top:0;left:0;width:1px;height:0%;
  background:var(--gold-deep);transition:height .18s linear}
.rail-nodes{display:flex;flex-direction:column;justify-content:space-between;
  margin-left:13px;padding:2px 0}
.rail-node{position:relative;pointer-events:auto;text-decoration:none;line-height:1;
  display:block;padding:4px 0}
/* Every other print steps to the other side and tips the other way, the way
   a real trail does - --s is the node's index, set inline. */
.rail-paw{display:block;width:11px;height:10px;fill:var(--ink-mute);
  transform:translateX(calc((var(--s) % 2) * 6px - 3px)) rotate(calc((var(--s) % 2) * 16deg - 8deg));
  transition:fill .4s ease,opacity .4s ease;opacity:.55}
.rail-node:hover .rail-paw{fill:var(--ink-mute);opacity:.9}
/* the glow was a light bloom for a dark margin; on cream it only smeared */
.rail-node.is-on .rail-paw{fill:var(--burgundy);opacity:1;filter:none}
/* the name only unfurls for the section you are actually in */
.rail-l{position:absolute;left:calc(100% + 12px);top:50%;white-space:nowrap;
  transform:translateY(-50%) translateX(-6px);
  font-family:var(--font-ui);font-size:9px;letter-spacing:.2em;text-transform:uppercase;
  color:var(--ink-mute);opacity:0;pointer-events:none;
  transition:opacity .45s ease,transform .45s ease}
.rail-node.is-on .rail-l{opacity:1;transform:translateY(-50%) translateX(0)}
/* THE WHOLE TRAIL ON HOVER. Point at the rail, or tab into it, and every
   section's name comes up, muted, so it reads as a map rather than a column of
   paws; the one under the pointer, and the one you are in, stand out. Names
   are part of their links, so they take a click once they are showing. */
.rail:hover .rail-l,.rail:focus-within .rail-l{opacity:.55;transform:translateY(-50%) translateX(0);pointer-events:auto}
.rail:hover .rail-node.is-on .rail-l,.rail:focus-within .rail-node.is-on .rail-l{opacity:1}
.rail .rail-node:hover .rail-l,.rail .rail-node:focus-visible .rail-l{opacity:1;color:var(--ink)}
.rail-node:focus-visible{outline:none}
.rail-node:focus-visible .rail-paw{fill:var(--burgundy);opacity:1}

/* Below 1100px there is no margin to live in, and the header carries the same
   links - so it goes rather than being squeezed. */
@media (max-width:1100px){ .rail{display:none} }

/* ============================================================
   THE HERO - two columns, and a sequence

   THE SHAPE. The couple hold the left half, the pack and the invitation hold
   the right. That is not decoration: the source photograph is a tight two-up
   portrait, so anything laid across its centre lands on a face. Giving them
   their own half solves the legibility problem and the composition at once.

   THE SEQUENCE. Forest → the pack walks in centre-frame → they say their
   line → they carry it into the right-hand column as the couple fade in on
   the left → the rest of the invitation follows.

   It is built from ONE moving part. .hero-stage holds the dogs AND their
   words, and starts shifted left and scaled up, which puts the pack centre
   screen with nothing else on it. When the stage settles, everything it
   carries arrives in the right-hand column together. No element is animated
   from one layout to another; the layout itself never changes.

   The clock is the gate's. .gate-armed sits on <html> until the gate has
   finished leaving, so every animation here is paused behind it and needs no
   JavaScript to start. With no JS the class is never added and the hero just
   renders, which is the correct degraded state.
   ============================================================ */
/* scene.css paints the couple as a ghost behind the woods, dead centre, and
   that markup is inside the SCENE fence so it cannot be removed from this
   page. The hero has its own .hero-photo, held to the left and on its own
   beat, so the shared one is simply switched off here. Leave it on and they
   appear twice, once in each half. */
.hero .cs-photo{display:none}

.hero{--stage-x:76%;       /* where the right-hand column is centred */
      --pack-lead:10vw;    /* mirrors Percy's inline --w */
      --pack-h:calc(var(--pack-lead) * 1.372);   /* percy.webp is 904x1240 */
      --lead-drop:44vh; /* how far below its resting place the line is spoken */
      display:block}

/* ---- the couple, holding the left ---- */
.hero-photo{position:absolute;inset:0;z-index:0;pointer-events:none;overflow:hidden;
  opacity:0;
  /* held to the left and faded out long before the text column starts */
  -webkit-mask-image:radial-gradient(66% 112% at 24% 40%, #000 0%, #000 32%, transparent 76%);
          mask-image:radial-gradient(66% 112% at 24% 40%, #000 0%, #000 32%, transparent 76%);
  animation:heroPhoto 2.6s ease-out both 3.6s}
.hero-photo::before{content:"";position:absolute;inset:0;
  background:url("../assets/bg-couple.jpg") center 20% / cover no-repeat;
  /* NUDGED LEFT so Ian is not buried under the text column. background-position
     alone cannot do it: the source is 1.30:1 against a far wider viewport, so
     `cover` scales it by WIDTH and there is no horizontal slack to shift into.
     Scaling up first creates the slack, then the translate uses it. */
  transform:scale(1.22) translateX(-13%);
  /* graded, never crisp: the source is 1440x1110 and upscales about 1.8x on a
     large monitor. Whatever replaces it will want the same treatment. */
  filter:grayscale(.42) contrast(1.06) brightness(.78) saturate(1.25)}
.hero-photo::after{content:"";position:absolute;inset:0;mix-blend-mode:color;opacity:.46;
  background:linear-gradient(152deg, var(--burgundy) 0%, var(--olive) 52%, var(--gold) 100%)}
@keyframes heroPhoto{from{opacity:0;transform:scale(1.07)} to{opacity:1;transform:none}}

/* ---- the countdown ----
   Reuses .cs-count's children wholesale, so .cc / .cc-n / .cc-l / .cc-sep come
   from invite.css already styled for a dark surface. Only the container needs
   anything: it sits between the venue line and the button, and it is smaller
   here than on the coming-soon page, where it is the whole point of the screen
   and here it is one beat of six. */
.hero-count{margin-top:1.5rem}
.hero-count .cc{min-width:56px}
.hero-count .cc-n{font-size:clamp(1.5rem,3vw,2.1rem)}
.hero-count .cc-l{font-size:8.5px;margin-top:.35rem}
.hero-count .cc-sep{font-size:clamp(1.1rem,2.2vw,1.6rem)}

/* the shade the right-hand column sits on */
.hero-veil{position:absolute;inset:0;z-index:1;pointer-events:none;
  background:
    linear-gradient(90deg, rgba(20,15,11,0) 30%, rgba(17,12,9,.42) 52%,
                           rgba(16,11,8,.72) 76%, rgba(16,11,8,.66) 100%),
    radial-gradient(120% 96% at 50% 30%, rgba(28,21,15,0) 0%, rgba(24,18,13,.3) 54%,
                                          rgba(16,11,8,.88) 100%),
    linear-gradient(to bottom, rgba(20,15,11,.58) 0%, rgba(20,15,11,.08) 22%,
                               rgba(20,15,11,.16) 56%, rgba(12,9,6,.94) 100%)}

/* ---- the one moving part ---- */
/* TOP IS THE NAV, not zero. The column centres inside the stage, and with the
   stage covering the whole hero it centred behind the fixed bar - on a laptop
   the pack's line ran straight through the menu. */
.hero-stage{position:absolute;top:clamp(18px,3vh,34px);left:0;right:0;bottom:0;z-index:8;
  animation:heroStage 2.4s cubic-bezier(.36,.02,.2,1) both 3.4s}
/* PURELY HORIZONTAL. It used to scale up as well, which made the move read as
   a zoom-and-slide - two things at once, and the sharp, edgy feel that came
   with it. One axis, one long ease, and it glides. */
@keyframes heroStage{
  from{transform:translateX(calc(50% - var(--stage-x)))}
  to  {transform:none}}

/* ---- the column ---- */
/* position lives here, where GSAP cannot reach it - see the note in the
   markup. Lifted by half the pack so the column centres on the space ABOVE
   the dogs rather than on the whole screen. */
/* above the pack: the dogs carry z-indexes of their own and would otherwise
   paint straight over the line they are supposed to be saying */
.hero-col{position:absolute;z-index:7;top:50%;left:var(--stage-x);
  /* wide enough that the closing paragraph sets in three lines rather
     than four - measured, not guessed */
  width:min(64ch,45vw);
  transform:translate(-50%,-50%);
  /* Half the pack, PLUS the room Percy's name needs above it. His is the only
     one that sits above a halo rather than on fur, so it reaches about a fifth
     of his height higher than the pack does - and on a laptop, where the hero
     is shortest, that is exactly where the closing line was sitting. --name-room
     is that clearance, so the column always leaves it whatever the pack size. */
  --name-room:calc(var(--pack-h) * .23);
  /* The names grew and the venue line collapsed to one row, so the column
     rides higher to keep clear of the pack, which had started to cover the
     closing paragraph.

     HOW MUCH higher has to depend on the screen's HEIGHT, not its width. The
     pack is sized off vw, so on a laptop it is nearly as tall as on a monitor
     while there is far less room above it, and a fixed lift that looked right
     at 1080 pinned the copy to the top of the frame at 800. This ramps from
     -18px on a tall screen to +30px on a short one. */
  margin-top:calc(var(--pack-h) / -2 - var(--name-room)
                  + clamp(-18px, 167px - 17.1vh, 30px))}
/* AND NEVER DOWN AMONG THE DOGS. Centring the column worked on a monitor, but
   on a laptop (1280 to 1600 wide, 640 to 800 tall) the pack, sized off the
   width, rises nearly as high while the column stays as tall, and the closing
   paragraph ran into "I'm Percy" by 47 to 94px. So the column keeps its centred
   place where that clears the pack, and otherwise its foot stops just above
   the dogs' names and it rises into the room above instead, of which there
   was 100 to 150px going spare.

   The dogs' names sit 1.2 pack-heights above the bottom of the first screen,
   measured from 1280 to 2560 wide. That is the SCREEN's bottom, 100svh, not
   the stage's: the hero runs on past the fold, so 100% was the wrong floor.
   The stage itself starts its own clamp() below the top, hence the subtraction.
   --col-h is the column's height, which CSS cannot know, so js/site.js
   measures it. Without it the second term is never the smaller one and the
   column centres as before. Landscape only: portrait stacks the column over
   the pack and has its own rule. */
@media (min-aspect-ratio:5/4){
  .hero-col{--clear:calc(var(--pack-h) * 1.2 + 14px);
    top:max(calc(var(--col-h, 0px) / 2 + 4px),
            min(calc(50% + var(--pack-h) / -2 - var(--name-room) + clamp(-18px, 167px - 17.1vh, 30px)),
                calc(100svh - clamp(18px, 3vh, 34px) - var(--clear) - var(--col-h, 0px) / 2)));
    margin-top:0}
}
/* A LAPTOP IS SHORT FOR ITS WIDTH, and everything in the column is sized off
   the width. Below 820px tall the names are capped by the height as well, and
   the gaps between the lines close up, so the column fits in the room above
   the dogs rather than spilling into them. A monitor never sees any of this. */
@media (min-aspect-ratio:5/4) and (max-height:820px){
  /* The script's line box is far taller than its letters, so the invitation
     line floated a long way above the names and sat pinned to the frame.
     Pulling the names up into that empty box lets the column, anchored at
     its foot here, drop the line closer to them. */
  .hero .hero-names{font-size:clamp(3.6rem,min(11vw,19vh),9rem);margin-top:0}
  .hero .hero-where{margin-top:.55rem}
  .hero .hero-count{margin-top:.9rem}
  .hero .hero-count .cc-n{font-size:clamp(1.3rem,min(3vw,4.6vh),2.1rem)}
  .hero .hero-act{margin-top:1rem}
  .hero .hero-act .btn{padding:12px 32px}
  .hero .hero-rest{margin-top:1.25rem;line-height:1.5}
}
/* and on the shortest of them, a browser on a 768px screen, tighter again.
   Here the column has nowhere left to rise, so the pack gives a little
   instead, the same trade the short-phone rule makes: 14% smaller, and each
   dog drawn in towards Percy by as much, so they stay a huddle rather than
   drifting apart. Everything that clears them is worked from --pack-lead. */
@media (min-aspect-ratio:5/4) and (max-height:680px){
  .hero{--pack-s:.86;--pack-lead:calc(10vw * var(--pack-s))}
  /* .hero first: the dogs' own rule comes later in this file and would win */
  .hero .hero-dog{width:calc(var(--w) * var(--pack-s));
    left:calc(var(--stage-x) + (var(--x) - var(--stage-x)) * var(--pack-s))}
  .hero .hero-names{font-size:clamp(3.2rem,min(11vw,16.5vh),9rem);margin-top:0}
  .hero .hero-count{margin-top:.6rem}
  .hero .hero-act{margin-top:.75rem}
  .hero .hero-act .btn{padding:10px 28px}
  .hero .hero-rest{margin-top:.9rem;font-size:.9rem}
}
.hero-content{display:flex;flex-direction:column;align-items:center;text-align:center}

/* each child arrives on its own beat; --d is inline, the house idiom */
.hero-content > *{opacity:0;animation:heroIn .95s cubic-bezier(.2,.7,.25,1) both var(--d,0s)}
@keyframes heroIn{from{opacity:0;transform:translateY(14px);filter:blur(5px)}
                  to  {opacity:1;transform:none;filter:blur(0)}}

/* THE LINE THEY SAY, and it does not simply fade in where it will end up.
   Hidden siblings still take their space, so as the column's first child it
   would appear at the TOP of a 400px stack - half a screen above the dogs
   who are supposedly saying it. So it arrives low, beside them, holds while
   they are alone centre-frame, then rises into the column as the stage
   settles. It overrides the generic heroIn for that reason. */
/* The resting size is small: this is the opening line of a letter, not a
   headline. The centre-frame moment is got by scaling UP from here rather
   than by setting it large and shrinking, so the column never relayouts. */
.hero-lead{font-family:var(--font-display);font-style:italic;
  font-size:clamp(.88rem,1.12vw,1.06rem);line-height:1.56;color:var(--tan-soft);
  text-shadow:0 2px 16px rgba(0,0,0,.92),0 0 34px rgba(0,0,0,.6);
  animation:heroLead 3.6s cubic-bezier(.36,.02,.2,1) both var(--d,0s)}
/* It is spoken centre-frame at full size and in full parchment, then settles
   into the column a little smaller and a little quieter - it has stopped
   being the only thing on screen and become the opening line of a letter.
   The size change is a scale, not a font-size: font-size would relayout the
   column on every frame of the move. */
@keyframes heroLead{
  0%  {opacity:0;color:var(--tan-soft);filter:blur(5px);
       transform:translateY(calc(var(--lead-drop) + 14px)) scale(1.42)}
  12% {opacity:1;color:var(--tan-soft);filter:blur(0);
       transform:translateY(var(--lead-drop)) scale(1.42)}
  36% {opacity:1;color:var(--tan-soft);
       transform:translateY(var(--lead-drop)) scale(1.42)}
  100%{opacity:1;color:var(--tan-mute);transform:none}}
/* THE SAME ARRIVAL AS THE GATE. Letters tracked wide and out of focus,
   closing to their set width while a gold flare traces the glyphs and dies.
   Lifted from pgNames in gate.css, less the translateX(-50%) - there the
   names are absolutely positioned and centre themselves, here the column
   does it. margin-right cancels the trailing letter-space, without which the
   block is half a gap too wide and the title slides right as it settles.
   Three shadows at every stop: a list that changes length cross-fades
   instead of blooming. */
.hero-names{font-family:var(--font-script);font-weight:400;
  /* a script is already joined up, so the tracking that suited a serif pulls
     it apart. Bigger too: these letterforms carry far less weight per inch. */
  /* a script carries far less ink per inch than a serif, so it needs more size
     to hold the same presence in the frame */
  /* Passions Conflict sets very small for its em, so the size here is not
     comparable with another script's. Anything swapped in needs re-measuring
     against the frame rather than kept at the same rem. */
  font-size:clamp(3.6rem,11vw,9rem);line-height:1.02;letter-spacing:0;
  color:var(--tan-soft);white-space:nowrap;margin-top:.7rem;
  --names-track:.16em;
  animation:heroNames 1.9s cubic-bezier(.16,.84,.3,1) both var(--d,0s)}
/* GLINTS, the same four catchlights the gate gives the names. Drawn rather
   than lit - a glint is a hard little cross of light, so it stays legible at
   12px where another soft glow would just be haze. Three gradients make the
   star: a wide flat lens, a tall narrow one crossing it, and a small core.
   Sized in em so they track the names at every breakpoint. */
.hero-names{position:relative}
/* The gate hands focus here so a screen reader starts at the invitation. It
   is not a control, so no ring: Safari drew a gold box round the names until
   the first tap moved focus on. */
.hero-names:focus{outline:none}
.hero-glints{position:absolute;inset:0;pointer-events:none}
.hero-glints i{position:absolute;left:var(--gx);top:var(--gy);
  width:calc(var(--gs,1) * .4em);aspect-ratio:1;
  transform:translate(-50%,-50%) scale(0);opacity:0;
  background:
    radial-gradient(ellipse 50% 6% at 50% 50%, rgba(255,252,242,1), transparent 74%),
    radial-gradient(ellipse 6% 50% at 50% 50%, rgba(255,252,242,1), transparent 74%),
    radial-gradient(closest-side, rgba(255,247,226,.75) 0%, rgba(255,244,212,.28) 26%, transparent 58%);
  /* --d is the names' own delay and inherits; +1.5s lands the first as the
     letters come into focus. --gd spreads the rest around the cycle so they
     take turns rather than blinking together. */
  animation:heroGlint 5.4s ease-in-out infinite
            calc(var(--d,0s) + 1.5s + var(--gd,0s))}
/* on for under a fifth of the cycle: catching the light as you move, not
   flashing at you */
@keyframes heroGlint{
  0%,100%{opacity:0;transform:translate(-50%,-50%) scale(.15) rotate(0deg)}
  5%     {opacity:1;transform:translate(-50%,-50%) scale(1)   rotate(9deg)}
  18%    {opacity:0;transform:translate(-50%,-50%) scale(.25) rotate(20deg)}}

@keyframes heroNames{
  0%  {opacity:0;letter-spacing:var(--names-track);
       margin-right:calc(-1 * var(--names-track));
       transform:scale(1.09);filter:blur(14px);
       text-shadow:0 0 0 rgba(255,248,230,0),
                   0 0 0 rgba(240,209,137,0),
                   0 0 0 rgba(201,162,75,0)}
  26% {opacity:1}
  58% {text-shadow:0 0 11px rgba(255,248,230,.62),
                   0 0 34px rgba(240,209,137,.42),
                   0 0 76px rgba(201,162,75,.22)}
  /* settles at ZERO, not .02em: that was right for a serif and wrong for a
     script, which is drawn already joined up */
  100%{opacity:1;letter-spacing:0;margin-right:0;
       transform:scale(1);filter:blur(0);
       text-shadow:0 0 30px rgba(20,14,10,.9),
                   0 2px 30px rgba(20,14,10,.8),
                   0 0 60px rgba(201,162,75,.3)}}
/* two deliberate lines rather than one that breaks wherever it runs out of
   column - at this measure it was splitting "Naga City" across the wrap */
.hero-where{margin-top:.8rem;font-family:var(--font-ui);font-size:clamp(10px,.95vw,12.5px);
  font-weight:500;letter-spacing:.18em;text-transform:uppercase;color:var(--gold);
  line-height:1.7;text-shadow:0 2px 12px rgba(0,0,0,.9);
  /* one row, so the names above can have the height it gives back */
  white-space:nowrap}
.hero-where span{display:inline;opacity:.55;margin:0 .3em}
.hw-bar{margin:0 .75em!important;opacity:.4!important}
.hero-rest{margin-top:1.15rem;font-family:var(--font-display);font-style:italic;
  font-size:clamp(.92rem,1.2vw,1.04rem);line-height:1.6;color:var(--tan-mute);
  text-shadow:0 2px 14px rgba(0,0,0,.92)}

.hero-act{margin-top:1.6rem}

/* ---- the pack ---- */
/* THE GLOW AND THE FADE BELONG TO THE PACK, NOT TO EACH DOG.

   Per-dog they cast onto each other: the front dog's drop-shadow landed on
   the one behind it and drew a light-then-dark band along its whole
   silhouette - five overlapping cut-outs, five bands, which is what read as
   grey lines between them. One filter over the group traces the union of the
   five instead, so there is nothing to cast onto and nothing to seam. It is
   also one compositing layer rather than five.

   filter runs before mask in the rendering order, so the glow is traced from
   the whole pack and the mask then fades glow and dogs together. */
/* Above the pack, so it covers their feet rather than dissolving them. Its
   height is a share of the pack, so the band scales with the dogs. */
/* the floor rides up with the pack, or they would stand on nothing */
.hero-floor{position:absolute;left:0;right:0;bottom:var(--seam);z-index:6;pointer-events:none;
  /* Shallow on purpose. A fixed band takes proportionally MORE from the short
     dogs than from Percy - at .42 of the pack it was eating Olga's and Sid's
     faces while barely reaching his chest. This only needs to soften the cut
     at the very foot of the frame. */
  height:calc(var(--pack-h) * .24);
  background:linear-gradient(to top,
    rgb(15,10,7) 0%, rgba(15,10,7,.86) 16%, rgba(15,10,7,.32) 58%, rgba(15,10,7,0) 100%)}

.hero-pack{position:absolute;inset:0;pointer-events:none;overflow:hidden;
  filter:drop-shadow(0 0 1.5px rgba(255,246,222,.5))
         drop-shadow(0 0 8px rgba(201,162,75,.36))
         drop-shadow(0 12px 22px rgba(0,0,0,.5));
  /* NO MASK HERE. Only the FILTER belonged on the pack - that is what was
     casting each dog's glow onto its neighbour. The bottom fade has to stay
     per-dog: as a share of the PACK it is a share of the whole hero, and the
     dogs only occupy its last fifth, so on a 1024x640 laptop three of the
     five had their tops at 84% of the pack and vanished into a fade that
     starts at 82%. It belongs to each dog's own height. */}
.hero-dog{position:absolute;bottom:var(--seam);left:var(--x);width:var(--w);
  z-index:var(--z,1);transform:translateX(-50%);
  pointer-events:auto;cursor:default;
  /* `translate` is an INDEPENDENT transform property, so the hover lift
     composes with the centring `transform` above instead of replacing it.
     That is the whole reason the rise does not have to fight the walk. */
  transition:translate .45s cubic-bezier(.2,.7,.25,1)}
.hero-dog:hover{translate:0 -14px}

.hero-dog img{display:block;width:100%;height:auto;transform-origin:50% 100%;
  /* Two animations, and heroSway is listed SECOND so it wins the transform
     once it starts. It has no fill, so through its delay it contributes
     nothing and heroTrot's `both` still holds the pose - which is how the
     walk hands over to the breathing without a seam. */
  animation:heroTrot 1.7s linear both var(--d,0s),
            heroSway var(--sw,7s) ease-in-out infinite calc(var(--d,0s) + 1.7s)}
/* barely there on purpose: enough that they are not five photographs, not so
   much that anything is bobbing while you are trying to read */
@keyframes heroSway{
  0%,100%{transform:translateY(0) rotate(0deg) scale(1,1)}
  28%    {transform:translateY(-.9%) rotate(.5deg) scale(.998,1.004)}
  62%    {transform:translateY(.35%) rotate(-.4deg) scale(1.003,.997)}}

/* WHAT THEY SAY WHEN YOU HOVER THEM, set on a curve that follows the dome of
   their head rather than in a bubble. SVG textPath is the only way to run
   live text along an arc - a bubble was the easy version and it sat on them
   like a sticker.

   The arc lives once in the sprite sheet and every dog references it, so the
   curve is identical and only the width changes: the svg is a share of the
   slot, so a wide Shar Pei gets a wide arc and a Boston terrier a narrow one,
   with no per-dog geometry to maintain. */
/* The viewBox matches the dog's own pixel dimensions, extended upward for the
   lifted baseline - so at width:100% of the slot the SVG sits exactly over
   the image and the traced path lands on the real fur, at any size. */
.hero-dog svg{position:absolute;left:0;bottom:0;width:100%;height:auto;
  overflow:visible;pointer-events:none;
  opacity:0;translate:0 6px;
  transition:opacity .4s ease,translate .4s cubic-bezier(.2,.7,.25,1)}
.hero-dog:hover svg{opacity:1;translate:0 0}
/* font-size is a presentation attribute on each svg, in ITS user units, so
   every dog's name renders at about the same size on screen despite their
   viewBoxes differing. letter-spacing is in user units too. */
.hero-dog svg text{font-family:var(--font-display);font-style:italic;
  /* brighter than --tan-soft: these sit over fur and against a halo, and at
     parchment they read as grey */
  letter-spacing:.04em;fill:#fff7e6;
  /* A SHADOW, NOT AN OUTLINE. A stroke was tried at two widths and measured:
     even at .085em it is ~1.2px a side against Ibarra's italic hairlines,
     which are about that thick to begin with - it ate the letters and cut the
     name's bright pixels to a third. A shadow does the same job of holding
     the text against fur of any colour without touching the glyph. */
  filter:drop-shadow(0 1px 2px rgba(8,5,3,.95))
         drop-shadow(0 0 7px rgba(8,5,3,.85))
         drop-shadow(0 0 9px rgba(255,238,198,.4))}

/* The hovered dog comes to the front, and brings its name with it. Without
   this a dog with a lower z-index has its name drawn UNDER its neighbours:
   Sid sits behind Percy, so half of "Sid, charmed." disappeared into a
   Shar Pei. 20 clears the halo at 6 as well. */
.hero-dog:hover{z-index:20}
/* PERCY'S HALO. He has one in the gate and it would be odd for it to be a
   gate flourish rather than part of him. Sized and placed off --pack-lead and
   his own --x, so it tracks him at every breakpoint without a second number
   to keep in sync. It arrives with him, on his delay. */
/* A CHILD OF PERCY'S SLOT now, not of the pack - so when he rises on hover it
   rises with him. As a sibling it stayed put and the halo came off his head.
   Everything is a share of the SLOT, which is exactly his image box, so it
   needs no numbers of its own. */
.hero-halo{position:absolute;z-index:6;left:50%;
  bottom:98.5%;
  width:66%;aspect-ratio:3.4/1;
  transform:translateX(-50%);pointer-events:none;opacity:0;
  border:.14em solid rgba(255,248,224,.95);border-radius:50%;
  font-size:calc(var(--pack-lead) * .12);   /* gives the border an em to scale by */
  filter:drop-shadow(0 0 5px rgba(240,209,137,.95))
         drop-shadow(0 0 14px rgba(201,162,75,.7))
         drop-shadow(0 0 30px rgba(201,162,75,.34));
  /* 2.3s is NOT a guess: Percy sets off at --d:.6s and heroTrot runs 1.7s,
     so that is the instant he plants. Any earlier and the halo is drawn
     over empty ground while he is still walking into it. */
  animation:heroHalo 1.1s ease-out both 2.3s}
@keyframes heroHalo{
  from{opacity:0;transform:translateX(-50%) translateY(6px) scaleX(.86)}
  to  {opacity:1;transform:translateX(-50%)}}

/* NO ::after GROUND SHADE HERE ANY MORE. It was a full-width rectangle, and
   once the glow moved onto .hero-pack the pack's drop-shadow traced ITS alpha
   too - producing a warm haze running the entire width of the screen. The
   mask above already settles the dogs onto the floor, and .hero-veil already
   darkens the foot of the hero, so the shade was doing a third time what two
   other things were doing once. */

/* IN FROM THE SIDE WITH SQUASH AND STRETCH, three strides and a settle.

   The previous version only nudged scaleY by 1%, which is why it read as five
   photographs sliding rather than five dogs arriving. Proper squash needs the
   two axes to fight each other: TALL AND NARROW at the top of a bound, SHORT
   AND WIDE on landing, and volume roughly preserved between them. The origin
   is at their feet, so a squash flattens them onto the ground instead of
   shrinking them in place.

   The easing is per-keyframe rather than one curve for the whole run: falling
   accelerates (ease-in), landing settles (ease-out). One curve across all of
   it is what makes CSS walk cycles look robotic.

   Deliberately NOT the gate's entrance - there they come head-on out of the
   dark, which reads as a greeting at a door. Lateral travel reads as falling
   in beside you. */
/* NO -50% IN HERE. The SLOT centres each dog; this only carries the walk.
   It used to end on translate(-50%,0) - left over from when the image was
   the slot - while heroSway starts on translateY(0) with no X at all, so the
   instant the sway took over the dog jumped right by half its own width.
   That was the snap. Two animations that hand over must agree on every axis
   the first one touches. */
@keyframes heroTrot{
  0%   {opacity:0;transform:translateX(var(--enter)) scale(1,1);
        animation-timing-function:cubic-bezier(.2,.5,.5,1)}
  12%  {opacity:1}
  /* first bound: up and stretched */
  30%  {transform:translate(calc(var(--enter) * .56),-6%) scale(.94,1.08);
        animation-timing-function:cubic-bezier(.5,0,.85,.5)}
  /* landing: flattened */
  44%  {transform:translate(calc(var(--enter) * .40),0) scale(1.09,.9);
        animation-timing-function:cubic-bezier(.2,.6,.5,1)}
  /* second bound, smaller */
  60%  {transform:translate(calc(var(--enter) * .20),-4.2%) scale(.96,1.055);
        animation-timing-function:cubic-bezier(.5,0,.85,.5)}
  74%  {transform:translate(calc(var(--enter) * .08),0) scale(1.06,.935);
        animation-timing-function:cubic-bezier(.2,.6,.5,1)}
  /* last little hop onto the mark */
  86%  {transform:translate(calc(var(--enter) * .015),-1.8%) scale(.985,1.022);
        animation-timing-function:cubic-bezier(.5,0,.85,.5)}
  94%  {transform:translateY(0) scale(1.028,.972)}
  100% {opacity:1;transform:translateY(0) scale(1,1)}}

/* ============================================================
   THE HERO LEAVES.

   Scroll-linked, on the same `view()` timeline the sections use, so it tracks
   the wheel rather than firing once. The column sinks and fades; the pack
   parts and drifts down, each dog the way it came in, which is also the way it
   will go to its own section further down the page.

   `exit` is the range where the element is on its way out of the viewport, so
   this only ever plays as the hero leaves and is fully reset at the top.

   Gated identically to the section reveals: @supports so an unsupporting
   browser simply keeps a static hero, and no-preference so reduced motion
   never opts in. */
/* BOTH OF THESE USE THE INDEPENDENT PROPERTIES ON PURPOSE.
   .hero-col holds its centring in `transform`, so animating transform here
   would wipe translate(-50%,-50%) and drop the column - the same trap GSAP
   fell into on this element earlier. `translate` composes with it instead.
   .hero-dog then cannot use `translate`, because its hover lift already does;
   it recedes on `scale` and opacity, which nothing else touches. */
@keyframes heroOut{
  from{opacity:1;translate:0 0;filter:blur(0)}
  to  {opacity:0;translate:0 78px;filter:blur(4px)}
}
/* ON THE PACK, NOT ON EACH DOG, and that is what fixes the hover.
   .hero-dog's hover lift is a `translate`, so the moment this keyframe also
   animated `translate` the animation won and the dogs stopped rising. Moving
   the drift up to the container leaves the property free on the slots. */
@keyframes packOut{
  from{opacity:1;translate:0 0}
  to  {opacity:.12;translate:0 34px}
}
@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    /* SCROLL, NOT VIEW. On a view() timeline the column's own `exit` range
       does not open until the column has already reached the top of the
       screen, so nothing happened for the first 250px and the whole effect was
       over before you noticed it had started. Tied to the page's scroll
       instead, it begins on the first wheel click and is done by the time the
       hero is gone, which is the behaviour being copied. */
    .hero-col{animation:heroOut linear both;
      animation-timeline:scroll(root block);animation-range:0 78vh}
    /* the pack leaves a beat after the words, not with them */
    .hero-pack{animation:packOut linear both;
      animation-timeline:scroll(root block);animation-range:14vh 96vh}
  }
}

/* nothing starts until the gate has cleared - see the note at the top */
.gate-armed .hero-dog img,
.gate-armed .hero-halo,
.gate-armed .hero-stage,
.gate-armed .hero-photo,
.gate-armed .hero-content > *{animation-play-state:paused}

/* ============================================================
   SECTIONS - the light half of the page.

   Dark hero, light body, dark footer. The gate and the hero are the night
   before; everything between them and the footer is the day itself, and the
   footer closes the woods again. That also means the page's two overscroll
   edges are both dark, which is why the footer was left alone.

   Ornament is foliage rather than rules, because that is what the bride's
   Decoration page actually is. See tokens.css for which accents survive on
   cream - the short version is that --gold does not.
   ============================================================ */
section[id]{scroll-margin-top:calc(var(--nav-h) + 12px)}

/* overflow:CLIP, not hidden, and the difference is load-bearing.

   `hidden` makes an element a scroll container - one that happens not to
   scroll. `animation-timeline: view()` resolves against the nearest scroll
   container, so with `hidden` here every heading in the page was measured
   against its own section, which never moves, and sat pinned at 100% progress:
   the reveal was attached, reported a ViewTimeline, and did nothing at all.
   `clip` clips identically without becoming a scroll container.

   Both are declared because Safari below 16 has no `clip` and would otherwise
   drop the clipping entirely and let the foliage push the page sideways. Those
   browsers keep `hidden`, and they have no view() either, so nothing is lost. */
.sec{position:relative;overflow:hidden;overflow:clip;padding:var(--sp-section) 0;
  min-height:60svh;display:flex;align-items:center;
  background:var(--cream);color:var(--ink)}
/* ============================================================
   TONAL DRIFT.

   The page alternates two stocks rather than holding one, so descending it
   feels like light changing rather than like scrolling a single sheet. The
   two are close on purpose, about eleven levels apart: any further and it
   reads as banding rather than as weather.

   NO HAIRLINE BETWEEN THEM. A 1px rule is exactly the hard edge the hero
   transition had to lose, and it would have reappeared seven more times down
   the page. Each section instead opens with a short ramp out of the previous
   stock into its own, which is the same move as the dawn, quieter.

   nth-of-type counts SECTION siblings, and #hero is the first of them, so the
   light sections start at 2. `.sec + .sec` deliberately excludes #details:
   that one follows the hero and already carries the dawn on its ::before. */
.sec:nth-of-type(odd){background:var(--cream-deep)}

.sec + .sec::before{content:"";position:absolute;left:0;right:0;top:0;
  height:clamp(80px,13vh,170px);z-index:1;pointer-events:none}
.sec + .sec:nth-of-type(even)::before{
  background:linear-gradient(to bottom, var(--cream-deep) 0%, var(--cream) 100%)}
.sec + .sec:nth-of-type(odd)::before{
  background:linear-gradient(to bottom, var(--cream) 0%, var(--cream-deep) 100%)}
.sec-wrap{position:relative;z-index:2;width:var(--wrap);margin-inline:auto;text-align:center}

/* DAWN. The dogs stand on an opaque floor at the very bottom of the hero, so
   cutting straight from espresso to cream put a hard line under their feet.
   This is a short band of the hero's own floor colour fading into the cream,
   on the FIRST light section only - one transition for the whole page. */
/* The scene's gold frame closes neatly at the bottom of one fixed screen on
   the coming-soon page. Here its lower edge is buried under the panel that
   rides up over the hero, so it is faded out before it gets there rather than
   being chopped in half by it. Hero only: the live page still wants it closed. */
.hero .cs-frame{-webkit-mask-image:linear-gradient(to bottom,#000 0%,#000 72%,transparent 97%);
                        mask-image:linear-gradient(to bottom,#000 0%,#000 72%,transparent 97%)}

/* AN EDGE, NOT A BLEND.

   Three attempts at dissolving the hero into the page all failed the same way.
   A short ramp is a hard band; a long one is a visible smear whichever hues it
   runs through, because a full-width sweep from near-black to cream is simply
   a large area of something that is neither. Green got it closer than brown,
   and it was still a thing you could point at.

   So it stops pretending. The light half rises OVER the hero as a panel with
   its own edge, lifted on a shadow that falls back onto the dark. The seam is
   now a deliberate join instead of a failed disguise, and the page gets back
   the ~300px the gradient was spending on being subtle.

   The pack lifts by the same amount, so the dogs still stand on their own dark
   floor rather than being sliced off by the panel coming up underneath them. */
.hero + .sec{
  margin-top:calc(-1 * var(--seam));
  padding-top:max(var(--sp-section), var(--seam) + 2.6rem);
  box-shadow:0 -22px 46px -20px rgba(10,7,4,.62)}
/* a couple of pixels of softness on the edge itself, so it reads as paper
   rather than as a screenshot butted against another screenshot */
.hero + .sec::before{content:"";position:absolute;left:0;right:0;top:0;height:3px;
  z-index:1;pointer-events:none;
  background:linear-gradient(to bottom, rgba(255,250,238,.5), transparent)}

.sec-eyebrow{display:flex;align-items:center;justify-content:center;gap:.8em;
  min-height:clamp(38px,4.2vw,52px);
  font-family:var(--font-ui);font-size:11px;font-weight:500;letter-spacing:.34em;
  text-transform:uppercase;color:var(--olive);margin-bottom:1.1rem}
/* The pack marks each section, the same print the rail uses - the numerals
   belonged to the reference site's world, not to a wedding the dogs narrate. */
.sec-paw{flex:none;width:15px;height:14px;fill:var(--olive);opacity:.75}

/* THE DOG THAT GUARDS THIS SECTION.
   A bust beside the eyebrow, and the lead line below it is that dog speaking.
   Cropped from the same cutouts the hero uses, by scratchpad/heads.py, which
   finds the head from the alpha channel rather than by eye. RSVP keeps the paw
   instead: that one belongs to all five of them. */
/* NO DISC BEHIND THEM. A circle cropped the bow ties off, which are the whole
   reason they look dressed for a wedding. The shadow is a drop-shadow on the
   filter rather than a box-shadow, so it follows the cut-out silhouette,
   ears and all, instead of a square that is mostly transparent. */
.sec-dog{flex:none;width:clamp(42px,4.8vw,58px);height:auto;margin:-.4em 0;
  filter:drop-shadow(0 1px 1px rgba(36,28,23,.22))
         drop-shadow(0 6px 12px rgba(36,28,23,.2))}

/* who just spoke */
.sec-who{margin-top:.85rem;font-family:var(--font-ui);font-size:11px;font-weight:600;
  letter-spacing:.22em;text-transform:uppercase;color:var(--olive)}
.sec-who::before{content:"";display:inline-block;width:18px;height:1px;
  margin:0 .7em .3em 0;background:var(--olive);opacity:.5;vertical-align:middle}

/* ---- the cursor follows the same casting ----
   THE POINTER IS STILL A POINTER. A bare head is charming and useless: nothing
   in it says where the click lands. Each cursor is the ordinary shape, white
   with a dark keyline the way the OS draws it, with the dog tucked in behind
   and below, and the hotspot on the tip. Built by scratchpad/cursors.py.

   48px, with a 96px for retina. An arrow AND a head inside 32px is mush, and
   Chrome and Firefox IGNORE any cursor image over 128px square and fall back
   to the keyword without warning, so that is the room available. PNG rather
   than webp: Chrome takes webp cursors, Safari is not dependable about it.

   RSVP belongs to all five of them. Five heads at this size is a smudge, so it
   gets the paw, which already means the pack everywhere else on the page.

   The whole value including the trailing keyword is passed in on the element,
   because `url(x) 16 16` alone is not a valid cursor - it must end in one.

   LINKS AND BUTTONS KEEP THEIR OWN CURSOR. A dog's head is 32px where a
   pointer's hotspot is one, and being able to hit the thing you are aiming at
   matters more than the joke does. */
/* The hero belongs to all five of them, so it takes the paw. It is not a .sec,
   so it needs naming here: the inline --cur alone does nothing without a rule
   that reads it. */
.hero,.sec{cursor:var(--cur,auto)}
/* The link cursor is the pointing hand with the same dog behind it, so the
   section keeps its character without losing the affordance. Both values carry
   their own trailing keyword, which a cursor must end in. */
.hero a,.hero button,.sec a,.sec button,.sec [role="button"]{cursor:var(--cur-link,pointer)}
.sec-h{font-family:var(--font-display);font-weight:300;line-height:1.1;
  font-size:clamp(2rem,5.4vw,3.4rem);color:var(--ink)}
.sec-h em{font-style:italic;color:var(--olive)}
.sec-soon{margin-top:1.2rem;font-family:var(--font-display);font-style:italic;
  font-size:clamp(1rem,2.6vw,1.25rem);color:var(--ink-mute)}
/* ONE LINE. These are single spoken sentences and a 46ch measure was breaking
   most of them across two rows for no reason; the wrap is what made them read
   as body copy rather than as somebody talking. --wrap still bounds them, and
   a phone still wraps, which is unavoidable and fine. */
.sec-lead{margin:1.1rem auto 0;max-width:none;font-weight:300;
  font-family:var(--font-display);font-style:italic;
  font-size:clamp(1rem,2.4vw,1.2rem);line-height:1.6;color:var(--ink-mute)}

/* ============================================================
   THE REVEAL, tied to the scroll rather than fired by it.

   `animation-timeline: view()` drives each element from its OWN position in
   the viewport, so a block low in a section arrives after one high in it with
   no stagger to hand-maintain, and the whole thing tracks the wheel instead of
   playing once on a trigger. It is also pure CSS: no observer, no GSAP, and
   nothing to run on the main thread.

   Gated three ways, and each gate matters:
     @supports  - where view() is unsupported the rules never apply, so the
                  content is simply visible. Never leave an element at
                  opacity:0 behind a feature query it might not get.
     no-preference - this is the reduced-motion contract for this block. It is
                  opt-in rather than opt-out, so a new element added to the
                  list below cannot be forgotten later.
     the list   - explicit, per the house rule, never `.sec-wrap > *`.
   ============================================================ */
@keyframes secIn{
  from{opacity:0;transform:translateY(24px);filter:blur(5px)}
  to  {opacity:1;transform:none;filter:blur(0)}
}
@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    .sec-eyebrow,.sec-h,.sec-lead,.sec-who,.sec-soon,.wh-card,.wh-foot,.tl-beat,.ent-head,.fq-card,
    .at-code,.at-view,.at-pick,.at-say,.at-sp,.st-open,.st-pair,.st-lines,.st-moments,.st-end,.so-letter,
    .ch-node,.ch-side,.ch-main,.ch-pack{
      animation:secIn linear both;
      animation-timeline:view();
      /* ENTRY IS TOO SHORT TO END ON. The `entry` range spans only the
         element's own height, so a one-line heading revealed itself over
         about 47px of scroll, which is a flicker rather than a dissolve.
         Ending on `cover` scales the range to the viewport instead: roughly
         290px for a heading, 480px for a card, and it still finishes while
         the element is well below centre. */
      animation-range:entry 0% cover 30%}
  }
}
/* ON A PHONE, THE BLUR IS SAVED FOR THE ENTRANCES THAT MATTER. A blur has
   to be recomputed on every frame of the scroll for every block arriving,
   and the repeated ones (twelve FAQ cards, the timeline, the venue cards,
   attire, story) are large and come several at a time. They keep the fade
   and the rise; only the blur goes. Each section's heading group and the
   closing letter keep all of it. Desktop is unchanged. */
@keyframes secInFlat{
  from{opacity:0;transform:translateY(24px)}
  to  {opacity:1;transform:none}
}
@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference) and (max-width:640px){
    .wh-card,.wh-foot,.tl-beat,.ent-head,.fq-card,
    .at-code,.at-view,.at-pick,.at-say,.at-sp,.st-open,.st-pair,.st-lines,.st-moments,.st-end,
    .ch-node,.ch-side,.ch-main,.ch-pack{
      animation-name:secInFlat}
  }
}

/* ---- a photograph, ghosted into the stock ----
   Shared class, URL passed in on the element as --photo, which is the same
   idiom as --d on the reveals and --mt on the gate's motes. Any section can
   take one without another rule being written.

   THE PATH MUST BE ROOT-RELATIVE. Although --photo is declared in an inline
   style attribute, the url() is resolved where it is SUBSTITUTED, which is
   this stylesheet: 'assets/x.webp' was requested as /css/assets/x.webp and
   404ed. A leading slash sidesteps the question. The site is served from the
   domain root, so this is safe here and would not be under a subdirectory.

   It sits at z-index 0 with the foliage, and the foliage comes later in the
   markup, so the leaves draw over it. The dawn band is z-index 1 above both,
   and .sec-wrap is 2.

   THE STOCK IS PUT BACK ON TOP, hardest in the middle. The photo carries the
   edges of the section and gets out of the way under the type, so the text
   never has to fight a face for contrast. */
.sec-photo{position:absolute;inset:0;z-index:0;pointer-events:none}
/* A phone-only second layer, off by default. See the note in the 640 query. */
.sec-photo-band{display:none}
/* inset is NEGATIVE because of the blur: a blurred layer samples nothing past
   its own box and feathers to transparent at the edges, which would draw a
   pale border inside the section. Bleeding it past the clip hides that.
   The blur is the point - unblurred you start reading the photograph, and a
   recognisable hand at the foot of the section pulls against the type. */
/* THE VEIL WAS DOING NOTHING BUT HIDING THE PHOTOGRAPH. Measured across four
   treatments, taking the photo from 28% to 92% opacity moved the body copy's
   worst-case contrast from 14.6:1 to 12.6:1, which is nowhere near mattering,
   while the visible detail in the image went from 5.9 to 26.6 standard
   deviations. Dark type on a LIGHT photograph is simply not the fight that a
   dark overlay is protecting against. What DOES hurt is busyness, which no
   contrast figure measures: at full strength the couple and the railing behind
   them competed with the type even while measuring 12.6:1. So the photo sits
   at 60%, lifted by brightness and flattened by low contrast and a small blur
   rather than hidden under a veil, and the veil is now only enough to keep the
   middle calm.

   inset is NEGATIVE because of the blur: a blurred layer samples nothing past
   its own box and feathers to transparent at the edge, which would draw a pale
   border inside the section. */
.sec-photo::before{content:"";position:absolute;inset:-14px;
  /* Cover, so it can never leave a gap, held at 60% of the vertical.

     THE PHOTOGRAPH IS PORTRAIT AND THAT IS THE POINT. Against a landscape
     section, cover always has to scale a portrait source by WIDTH, so the full
     width is on screen at every size and nothing at the left or right edge can
     be cropped away. This one has the couple at opposite edges with an empty
     stand of trees between them, which is exactly where the two postcards sit,
     so the cards land in the gap instead of on a face. The landscape shot
     before it put them shoulder to shoulder in the middle, and no horizontal
     position could clear both. 60% keeps their faces in the visible band on
     everything from a phone to a 2560. */
  background:var(--photo) 50% 60% / cover no-repeat;
  filter:grayscale(.3) brightness(1.46) contrast(.68) saturate(1.2) blur(1.6px);
  opacity:.4}
/* VERTICAL, NOT RADIAL, because what needs protecting is a BAND not a centre.
   On a short wide screen the two postcards and the couple want the same strip
   of the section, and there is no horizontal position that clears both faces
   of both cards: the faces span about 44% of the width and the free gaps are
   25%, 16% and 25%. So the top of the photo is held back where the cards live
   and released below them, which works at every aspect instead of only the
   one it was tuned on. */
.sec-photo::after{content:"";position:absolute;inset:0;
  background:linear-gradient(to bottom,
    rgba(244,239,230,.88) 0%, rgba(244,239,230,.83) 26%,
    rgba(244,239,230,.6) 54%, rgba(244,239,230,.35) 78%,
    rgba(244,239,230,.25) 100%)}
/* the deeper stock reasserts its own tone instead of cream */
.sec:nth-of-type(odd) .sec-photo::after{
  background:linear-gradient(to bottom,
    rgba(234,226,212,.88) 0%, rgba(234,226,212,.83) 26%,
    rgba(234,226,212,.6) 54%, rgba(234,226,212,.35) 78%,
    rgba(234,226,212,.25) 100%)}

/* ---- the foliage, generated by scratchpad/sprig.py ----
   Ornament, so it is aria-hidden in the markup and never carries meaning.
   Sized off the section rather than the viewport so it keeps its proportion
   to the type at every width. */
.sec-leaf{position:absolute;z-index:0;width:clamp(150px,26vw,330px);height:auto;
  pointer-events:none;opacity:.17;
  fill:none;stroke:var(--olive);stroke-width:1.5;
  stroke-linecap:round;stroke-linejoin:round}
.sec-leaf-a{left:calc(var(--sp-gutter) * -0.5);bottom:-4%}
.sec-leaf-b{right:calc(var(--sp-gutter) * -0.5);top:-4%;transform:scale(-1)}

/* ============================================================
   THE DAY - seven beats on a paw trail.

   An <ol>, because it is an ordered sequence and nothing else describes it.
   Three columns shared across every row so the times right-align against the
   trail and the events left-align off it: one reading line down each side,
   rather than seven independently centred rows.
   ============================================================ */
.tl{display:grid;grid-template-columns:max-content auto minmax(0,1fr);
  justify-content:center;text-align:left;
  width:fit-content;max-width:100%;margin:clamp(2.4rem,5vw,3.6rem) auto 0;
  padding:0;list-style:none}

/* The row is a subgrid so the three columns line up down the whole list.
   Without support each row sets its own, which stays readable, just ragged. */
.tl-beat{display:grid;grid-column:span 3;grid-template-columns:max-content auto minmax(0,1fr);
  align-items:start}
@supports (grid-template-rows:subgrid){
  .tl-beat{grid-template-columns:subgrid}
}

.tl-time{grid-column:1;justify-self:end;padding-top:.1em;
  font-family:var(--font-display);font-weight:400;line-height:1;
  font-size:clamp(1.15rem,2.2vw,1.55rem);color:var(--ink);white-space:nowrap}
.tl-time i{margin-left:.35em;font-family:var(--font-ui);font-style:normal;
  font-size:.54em;font-weight:600;letter-spacing:.16em;color:var(--ink-faint)}

/* THE TRAIL. The line is drawn on the marker cell and runs its full height, so
   consecutive rows join into one unbroken spine - which is why the rows are
   spaced with padding rather than a row-gap, since a gap would cut it. The
   first and last are trimmed so the trail starts and stops at a paw. */
/* ONE ROW PER BEAT, which is why the name and the note share a cell.

   They were two cells, so a beat was two rows, and the marker in row one
   stretched only as far as the event name: the spine came out in disconnected
   pieces with a 55px gap over every note. `grid-row: 1 / -1` does NOT fix
   that, and it is worth knowing why: -1 counts back from the last line of the
   EXPLICIT grid, and these rows are all implicit, so it resolves to line 1 and
   the span quietly collapses to a single row. Wrapping the pair removes the
   second row instead of trying to span it. */
.tl-mark{grid-column:2;position:relative;align-self:stretch;
  padding:0 clamp(15px,2.6vw,30px);
  /* THE PAW HAS TO SIT ON THE TYPE, NOT ON THE ROW.
     Sitting at the top of the cell it lined up with the top of the line BOX,
     which is above the letters by half the leading, so every print floated
     over its own time. The cell is given the same font-size as .tl-name, so
     --tl-lift can be written in em and stay true at every clamp step: half a
     line box, less half the paw. */
  font-size:clamp(1.05rem,2.1vw,1.35rem);
  --tl-lift:calc(.625em - 7px)}
.tl-mark::before{content:"";position:absolute;top:0;bottom:0;left:50%;width:1px;
  transform:translateX(-50%);background:var(--rule-ink)}
/* the trail begins and ends at a paw, not at the edge of a row */
.tl-beat:first-child .tl-mark::before{top:calc(var(--tl-lift) + 7px)}
.tl-beat:last-child  .tl-mark::before{bottom:auto;height:calc(var(--tl-lift) + 7px)}
/* an opaque disc so the spine does not run through the print. It carries the
   section's own stock, so a section that later takes a photograph will need
   this looked at. */
.tl-mark::after{content:"";position:absolute;left:50%;width:20px;height:20px;
  top:calc(var(--tl-lift) - 3px);
  transform:translateX(-50%);border-radius:50%;background:var(--cream)}
.sec:nth-of-type(odd) .tl-mark::after{background:var(--cream-deep)}
.tl-paw{position:relative;z-index:1;display:block;width:15px;height:14px;
  margin-top:var(--tl-lift);fill:var(--olive);opacity:.85}

.tl-what{grid-column:3;padding-bottom:1.9rem}
.tl-name{display:block;
  font-family:var(--font-display);font-weight:400;line-height:1.25;
  font-size:clamp(1.05rem,2.1vw,1.35rem);color:var(--ink)}
.tl-note{display:block;margin-top:.4rem;
  font-family:var(--font-display);font-style:italic;line-height:1.55;
  font-size:clamp(.92rem,1.7vw,1.02rem);color:var(--ink-mute)}
.tl-beat:last-child .tl-what{padding-bottom:0}

/* THE UNPLUGGED ASK: a beat with no time, a crossed-out phone where the paw
   would be, and the couple's own words, quoted and signed. Set like every
   other beat (the name, then an italic note), so it reads as part of the day
   rather than a notice pinned over it. */
.tl-off{position:relative;z-index:1;display:block;width:17px;height:17px;margin-top:calc(var(--tl-lift) - 2px);
  fill:none;stroke:var(--olive);stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;opacity:.85}
.tl-card{display:block;max-width:29rem}
.tl-card-h{display:block;font-family:var(--font-display);font-weight:400;line-height:1.25;
  font-size:clamp(1.05rem,2.1vw,1.35rem);color:var(--ink)}
.tl-card-p{display:block;margin-top:.4rem;font-family:var(--font-display);font-style:italic;line-height:1.55;
  font-size:clamp(.92rem,1.7vw,1.02rem);color:var(--ink-mute)}
.tl-card-p::before{content:"\201C"}
.tl-card-p::after{content:"\201D"}
.tl-card-sig{display:block;margin-top:.45rem;font-family:var(--font-ui);font-size:10px;font-weight:600;
  letter-spacing:.16em;text-transform:uppercase;color:var(--ink-faint)}

@media (max-width:640px){
  /* the time moves above the event rather than beside it: at this width a
     max-content column for both leaves the events about twelve characters */
  .tl{grid-template-columns:auto minmax(0,1fr)}
  .tl-beat{grid-column:span 2;grid-template-columns:auto minmax(0,1fr)}
  @supports (grid-template-rows:subgrid){ .tl-beat{grid-template-columns:subgrid} }
  /* stacked, the paw lines up with the TIME, which is the first line here and
     is set solid, so the lift is half its own size rather than half a leaded
     line box */
  /* stacked there are exactly two rows, the time and the pair, so this span is
     a known quantity rather than the -1 that collapsed above */
  .tl-mark{grid-column:1;grid-row:1 / span 2;padding:0 14px 0 0;
    font-size:clamp(.95rem,4.2vw,1.15rem);--tl-lift:calc(.5em - 7px)}
  .tl-time{grid-column:2;justify-self:start;padding-bottom:.3rem;
    font-size:clamp(.95rem,4.2vw,1.15rem)}
  .tl-what{grid-column:2;padding-bottom:1.6rem}
  /* no time to stack above it: the phone sits level with the card's heading */
  .tl-ask .tl-time{display:none}
  .tl-ask .tl-mark{grid-row:1}
  .tl-ask .tl-what{grid-row:1}
  .tl-off{margin-top:.2rem}
}

/* ============================================================
   ENTOURAGE - a two-column programme, groom's side left, bride's side right.

   It is NOT in processional order: the parents open it, the way the printed
   programme does. The processional order belongs to the cinematic walkthrough,
   which is a separate thing.

   THREE columns, not two: names left and right of a narrow middle lane. A
   group heading spans all three. A pair splits left and right, meeting at the
   middle. A solo spans and centres.

   The lane is never left bare. Where a pair has a role, Candle, Veil, Cord,
   Ribbon, that used to sit in the lane as a word; it is now a glyph and a
   label above the pair, so the four roles read the same way as the Bearers and
   the Offertory do. What the lane carries instead is a small diamond joining
   the two names, the same one at the centre of the group sigil.
   ============================================================ */
.ent{display:grid;grid-template-columns:1fr auto 1fr;
  column-gap:clamp(12px,2.2vw,30px);row-gap:.36rem;align-items:baseline;
  width:min(100%, 54rem);margin:clamp(2rem,4vw,3rem) auto 0}

.ent-head{grid-column:1/-1;display:flex;flex-direction:column;align-items:center;
  gap:.55rem;margin:clamp(1.9rem,3.6vw,2.9rem) 0 .95rem}
.ent-head:first-child{margin-top:0}
.ent-head span{font-family:var(--font-ui);font-size:12.5px;font-weight:600;
  letter-spacing:.3em;text-transform:uppercase;color:var(--olive)}
.ent-sigil{display:block;width:clamp(56px,7vw,74px);height:auto;
  color:var(--olive);opacity:.7}

.ent{--ent-name:clamp(1.12rem,2.05vw,1.42rem)}
.ent-n{margin:0;font-family:var(--font-display);line-height:1.5;color:var(--ink);
  font-size:var(--ent-name)}
.ent-l{grid-column:1;text-align:right}
.ent-r{grid-column:3;text-align:left}
.ent-solo{grid-column:1/-1;text-align:center}
/* the middle lane: empty on most rows, and an empty <p> must not open a gap */
.ent-mid{grid-column:2;margin:0;display:flex;align-items:center;justify-content:center;
  font-family:var(--font-ui);font-size:9.5px;
  font-weight:600;letter-spacing:.16em;text-transform:uppercase;
  color:var(--ink-faint);white-space:nowrap}
/* A PAIR WITH NO ROLE STILL NEEDS SOMETHING IN THE LANE. Left empty, the two
   names read as two unrelated columns with a hole down the middle; a small
   mark joins them, and it is the same diamond the group sigil has at its
   centre, so the lane always carries either a word or that. */
.ent-pip{display:block;width:4px;height:4px;transform:rotate(45deg);
  background:var(--olive);opacity:.42}

/* ROLE ABOVE NAME, CENTRED. Bearers, the readings and the offertory are a role
   with ONE name, and pushing the label into the left column and the name into
   the right made a lopsided two-column table out of what is really a caption
   and its answer. Stacked, they centre on the same axis as everything else. */
.ent-sub{grid-column:1/-1;margin:1.15rem 0 .1rem;
  display:flex;flex-direction:column;align-items:center;gap:.3rem}
.ent-sub:first-of-type{margin-top:.2rem}
.ent-sub span{font-family:var(--font-ui);font-size:10.5px;font-weight:600;
  letter-spacing:.2em;text-transform:uppercase;color:var(--ink-faint)}
.ent-gl{display:block;width:28px;height:28px;color:var(--olive);opacity:.72}
.ent-under{margin-bottom:.15rem}

/* A CELL is a role and its name kept together, so the pair can sit in one
   column. The Bearers are five, which does not halve: the Ring spans and
   centres, the other four fall into two rows of two.

   A cell in a side column reads to the middle lane, exactly as a pair of names
   does: right column ranged right, left column ranged left, glyph and label
   riding with the name. Centring each cell in its own 1fr instead left the four
   names floating at four unrelated x positions while every other group in the
   section met at the gutter. */
.ent-cell{display:flex;flex-direction:column;gap:.25rem;margin-top:1.15rem}
.ent-cell-c{grid-column:1/-1;align-items:center}
.ent-cell-l{grid-column:1;align-items:flex-end;text-align:right}
.ent-cell-r{grid-column:3;align-items:flex-start;text-align:left}
.ent-cell .ent-sub{margin:0}
/* a cell holding two names, the parents: they are a unit, so they sit at the
   grid's own row rhythm rather than the cell's wider label gap */
.ent-cell .ent-n + .ent-n{margin-top:calc(.36rem - .25rem)}
.ent-cell-c .ent-n{text-align:center}
/* THE LANE BESIDE A PAIR OF CELLS. A cell is a stack, so the row is as tall as
   glyph + label + name and the diamond centred in it would sit up beside the
   labels. It hangs from the bottom instead, half a name's line box up, which is
   where the diamond sits on every other pair in the section. */
.ent-cell-m{align-self:stretch;align-items:flex-end;
  padding-bottom:calc(var(--ent-name) * .75 - 2px)}
/* their own sheet marks the departed with a cross; kept, in the gold */
.ent-dag{font-style:normal;color:var(--gold-deep);margin-left:.15em}

/* BETWEEN THE AISLE AND THE MASS. A bare 46px hairline read as a cut across
   the page, which is the one thing a programme should not look like. It is an
   ornament now: the rules fade out at both ends instead of stopping dead, and
   the diamond at the centre is the same mark the group sigils carry and the
   lane puts between a pair of names. */
.ent-rule{display:flex;align-items:center;justify-content:center;gap:.9rem;
  width:min(260px,62%);margin:clamp(2.6rem,5vw,3.8rem) auto 0}
.ent-rule::before,.ent-rule::after{content:"";flex:1;height:1px;
  background:linear-gradient(90deg,transparent,var(--rule-ink))}
.ent-rule::after{background:linear-gradient(90deg,var(--rule-ink),transparent)}
.ent-rule .ent-pip{width:5px;height:5px;opacity:.5}

@media (max-width:640px){
  /* one column, and the role lane becomes a line of its own between the pair */
  .ent{grid-template-columns:1fr;row-gap:.1rem}
  .ent-l,.ent-r,.ent-solo,.ent-mid,.ent-sub,
  .ent-cell-l,.ent-cell-r,.ent-cell-c{grid-column:1;text-align:center;
    align-items:center}
  /* THE MARK BECOMES A SEPARATOR BETWEEN COUPLES. In two columns it sits in
     the lane and joins a pair across the gutter; stacked, that same position
     would drive a wedge down the middle of the couple instead. So the lane
     goes, and the diamond is drawn after the SECOND name of each pair:

         Nilo Reazon          <- .ent-l
         Charity Besinio      <- .ent-r
              *               <- .ent-r::after
         Leo Arnel Caayao

     A pseudo-element rather than moving the markup, because the lane is the
     middle of three grid items and reordering it would push it onto a row of
     its own on the desktop grid, whose auto-placement does not backtrack. */
  .ent-l{margin-top:.5rem}
  .ent-mid{display:none}
  .ent-r::after{content:"";display:block;width:4px;height:4px;
    margin:.8rem auto 0;transform:rotate(45deg);
    background:var(--olive);opacity:.42}
  /* not after the last couple in a group: the next heading already separates
     them, and at the very end of the list the section's own rule follows */
  .ent-r:last-child::after{display:none}
  .ent-head{margin:2.2rem 0 .8rem}
}
/* Kept out of the list above on purpose: a selector list is thrown away whole
   if any part of it fails to parse, and :has() is newer than the rest of this
   file. Alone, it simply does nothing on an engine without it, and the only
   cost is one extra diamond before a heading. */
@media (max-width:640px){
  /* nor before an offertory role, whose glyph and label already separate it */
  .ent-r:has(+ .ent-head)::after,
  .ent-r:has(+ .ent-sub)::after{display:none}
}

/* ============================================================
   WHEN AND WHERE - two venues, side by side.
   ============================================================ */
/* COLUMN GAP ONLY. Once the cards became subgrids the shared `gap` started
   applying between all six rows as well, spacing the lines of a single card
   apart by the width meant to separate the two venues. The vertical rhythm is
   the elements' own margins. */
.wh{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
  column-gap:clamp(1.6rem,4vw,3.4rem);row-gap:0;
  margin-top:clamp(2.4rem,5vw,3.8rem);align-items:start}

.wh-card{display:flex;flex-direction:column;align-items:center;text-align:center}

/* ONE SET OF ROWS FOR BOTH CARDS. The church's name runs to two lines and the
   resort's to one, which put every line below them out of step across the
   pair. Subgrid hands the cards the section's rows instead of each inventing
   its own. Where it is unsupported the flex column above still stacks
   correctly - the rows simply stop lining up, which is the old behaviour. */
@supports (grid-template-rows:subgrid){
  @media (min-width:641px){
    .wh{grid-template-rows:repeat(5,auto)}
    /* align-items:start, not the default stretch: a one-line name in a row
       sized by a two-line one was being centred in the leftover space, which
       is the single row that would not line up. */
    .wh-card{display:grid;grid-row:span 5;grid-template-rows:subgrid;
      justify-items:center;align-items:start;align-content:start}
    /* The name row is as tall as the LONGEST name, so a one-line name left a
       hole below it while the two-line name sat hard against the line under.
       Bottom-aligned, both names keep the same gap to the city line and the
       slack moves above, where the crest already provides breathing room. */
    .wh-name{align-self:end}
  }
}

/* ---- the postcard ----
   Both venue images arrived already illustrated, on transparency with a torn
   paper edge, which is the reference's right-hand panel almost exactly. So the
   job here is the paper they sit on: a fixed portrait panel, the art contained
   rather than cropped (the torn edge IS the composition, and cover would eat
   it), and a typewriter caption in the corner.

   One aspect ratio for both cards although the two sources differ, 900x562
   against 760x747, or the pair reads as a mistake rather than a set. The card
   is very nearly square rather than the reference's portrait, and that is what
   the two sources cost: contained in a tall frame the landscape image can only
   ever be 180px against the square one's 281 and looks stranded in dead paper.
   Cropping it would fix the ratio and destroy the torn edge, which is the
   whole effect, so the frame gives way instead. */
.pc{position:relative;display:flex;flex-direction:column;
  width:100%;max-width:clamp(240px,30vw,360px);aspect-ratio:1/1.06;
  margin-bottom:1.6rem;padding:clamp(13px,1.4vw,19px);
  background:var(--paper);border:1px solid rgba(36,28,23,.11);
  box-shadow:0 1px 2px rgba(36,28,23,.07),0 16px 34px -18px rgba(36,28,23,.34);
  overflow:hidden}

/* Paper, in two parts: grain, then age in the corners. The grain is an inline
   feTurbulence rather than an image request, and multiplies so it darkens the
   stock instead of veiling it. */
.pc::before{content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
  opacity:.34;mix-blend-mode:multiply;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size:140px 140px}
.pc::after{content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
  background:radial-gradient(118% 96% at 50% 38%, transparent 52%, rgba(122,98,66,.20) 100%)}

.pc-art{position:relative;z-index:2;flex:1;min-height:0;width:100%;
  object-fit:contain;object-position:50% 50%}
/* The illustrations breathe. See #mi-boil in the sprite for what and why.
   Behind no-preference because it is continuous ambient movement, which is
   exactly what that setting is for; without it the drawings simply sit still.
   A browser that cannot resolve the filter reference also just shows the
   image, which is the same correct degraded state. */
@media (prefers-reduced-motion:no-preference){
  .pc-art{filter:url(#mi-boil)}
}

/* ---- birds across the church ----
   They sit ABOVE the art and outside the boil: the filter is on .pc-art alone,
   so the birds keep their own crisp line while the drawing behind them moves.
   Clipped by .pc's overflow, which is what lets them enter and leave. */
/* container-type so the flight path can be written in cqw. A percentage in
   `translate` resolves against the ELEMENT's own box, so a 19px bird animated
   to 116% moved 22px and sat in the corner; cqw is a share of this overlay,
   which is the card. */
.pc-birds{position:absolute;inset:0;z-index:3;pointer-events:none;
  container-type:inline-size}
.pc-bird{position:absolute;top:var(--t);left:0;
  width:calc(19px * var(--s,1));height:auto;overflow:visible;
  fill:none;stroke:rgba(52,42,32,.62);
  /* the stroke holds its weight whatever --s scales the bird to, or the small
     ones vanish and the near one turns into a smudge */
  stroke-width:1.05;vector-effect:non-scaling-stroke;
  stroke-linecap:round;stroke-linejoin:round;
  opacity:0}
@media (prefers-reduced-motion:no-preference){
  .pc-bird{animation:pcFly var(--dur,22s) linear infinite var(--d,0s)}
}
/* One keyframe set does the crossing AND the rise and fall, because `translate`
   takes both axes: two animations would need two properties and the second
   would overwrite the first. */
@keyframes pcFly{
  0%   {translate:-14cqw 0;opacity:0}
  8%   {opacity:.9}
  25%  {translate:16cqw -9px}
  50%  {translate:48cqw 4px}
  75%  {translate:80cqw -6px}
  92%  {opacity:.9}
  100% {translate:116cqw 2px;opacity:0}
}

.pc-cap{position:relative;z-index:2;margin-top:clamp(7px,.9vw,11px);text-align:left;
  font-family:var(--font-mono);font-size:clamp(9.5px,.92vw,11px);line-height:1.72;
  color:var(--ink-mute)}
.pc-cap span{display:block}
.pc-place{color:var(--ink);letter-spacing:.05em}
.pc-role{color:var(--ink-mute)}
/* The time is what a guest is actually here for, so it is the one thing in the
   caption set above the typewriter's own size. In em, so it tracks the clamp
   above rather than needing a second one. */
.pc-time{margin-top:.34em;font-size:1.72em;line-height:1.2;letter-spacing:.01em;
  color:var(--ink)}

.wh-name{margin-top:.75rem;font-family:var(--font-display);font-weight:300;
  font-size:clamp(1.3rem,2.9vw,1.95rem);line-height:1.22;color:var(--ink);
  text-wrap:balance}
.wh-where{margin-top:.85rem;font-family:var(--font-ui);font-size:clamp(11px,1.2vw,12.5px);
  letter-spacing:.14em;text-transform:uppercase;color:var(--ink-mute)}
/* 42ch, not 34. The measure is in ch and Cormorant's character is narrower
   than Gambetta's was, so the swap quietly shrank this in pixels and pushed the
   longer of the two notes onto a third line while the other stayed at two.
   Measured: 38ch is where the church note comes back to two lines at every
   width the pair is side by side; 42 leaves margin. */
.wh-note{margin-top:1.05rem;max-width:42ch;font-family:var(--font-display);font-style:italic;
  font-size:clamp(.95rem,1.9vw,1.06rem);line-height:1.62;color:var(--ink-mute)}

.wh-map{display:inline-flex;align-items:center;gap:.55em;margin-top:1.4rem;
  min-height:44px;padding:0 1.25rem;
  font-family:var(--font-ui);font-size:11px;font-weight:600;letter-spacing:.17em;
  text-transform:uppercase;text-decoration:none;color:var(--ink);
  border:1px solid var(--rule-ink);border-radius:999px;background:var(--paper);
  transition:background .3s ease,border-color .3s ease,color .3s ease}
.wh-map svg{width:13px;height:13px;fill:currentColor;opacity:.75}
.wh-map:hover{background:var(--burgundy);border-color:var(--burgundy);color:#fff}

/* The one line that belongs to both cards rather than either. */
.wh-foot{margin-top:clamp(2.2rem,4.5vw,3.2rem);padding-top:clamp(1.6rem,3.2vw,2.4rem);
  border-top:1px solid var(--rule-ink);
  display:flex;flex-direction:column;align-items:center;gap:.9rem}
.wh-foot-l{font-family:var(--font-ui);font-size:clamp(10px,1.1vw,11px);font-weight:600;
  letter-spacing:.24em;text-transform:uppercase;color:var(--ink-faint)}
/* the date is the thing being asked of them, so it is set like a date and not
   like a caption */
.wh-foot-d{margin-top:-.35rem;font-family:var(--font-display);font-weight:300;
  font-size:clamp(1.5rem,3.2vw,2.2rem);line-height:1.15;color:var(--ink)}
.wh-foot-d em{font-style:italic;color:var(--olive)}
.wh-foot .btn{margin-top:.35rem}
/* the hero's call to action is a link now, to the RSVP; links underline */
.hero-act a.btn{text-decoration:none}

/* ---- the button, ported to the light half ----
   invite.css builds .btn for the coming-soon page, which is espresso all the
   way down, and every one of its choices assumes that:

     background at 62% ALPHA   - deep burgundy over espresso, dusty mauve over
                                 cream, because the stock shows through
     backdrop-filter blur(2px) - invisible over a flat dark page; over the
                                 section photograph it smears it, which is the
                                 haze
     color --tan-soft          - pale parchment, meant for a dark face
     border gold at 50%        - 2.1:1 on cream

   and because this one is an <a> rather than a <button> it was underlined too.
   Opaque burgundy, white text, no backdrop-filter, and the gold bloom off:
   btnGlow is a light bloom for a dark room and does nothing here but muddy. */
.sec .btn{color:#fff;text-decoration:none;
  background:linear-gradient(180deg,var(--burgundy),var(--burgundy-deep));
  border-color:var(--burgundy-deep);
  -webkit-backdrop-filter:none;backdrop-filter:none}
.sec .btn-primary{animation:none;
  box-shadow:0 1px 2px rgba(36,28,23,.12),0 10px 24px -12px rgba(110,36,54,.55)}
.sec .btn-primary:hover{border-color:var(--burgundy);
  background:linear-gradient(180deg,#7d2a3e,var(--burgundy));
  box-shadow:0 2px 5px rgba(36,28,23,.14),0 15px 32px -12px rgba(110,36,54,.6)}
.sec .btn .btn-ic{filter:none}

/* ---- one column, and the crest shrinks with it ---- */
@media (max-width:640px){
  /* stacked, so the row gap is what separates the two venues again */
  .wh{grid-template-columns:minmax(0,1fr);row-gap:clamp(2.6rem,9vw,3.4rem)}
  /* The 30vw in the base clamp is sized for two cards side by side, so on a
     phone it collapses to nothing and the card sat pinned at its 240px floor
     inside a 351px column. Stacked, it can have the whole measure. */
  .pc{max-width:min(100%,400px)}

  /* TWO LAYERS, BECAUSE ONE PHOTOGRAPH CANNOT DO BOTH JOBS HERE.

     Stacked, this section is about 390 by 1920, an aspect of 0.20, and `cover`
     keeps only 0.20/aspect of a source's width: 27% of a 3:4, 30% of a 2:3,
     36% of a 9:16. Worse, the reader only ever sees 844 of those 1920 pixels
     at a time, so they scroll THROUGH the picture rather than looking at it,
     and every horizontal slice has to stand on its own.

     That is fine for texture and fatal for a face. And the faces were not even
     being cropped out: hiding the copy showed them present but sitting behind
     the postcards, because on a phone the content column IS the full width.
     Measured, the only clear vertical gaps here are 0-197, 338-405 and
     1026-1103 out of 1922.

     So the forest carries the whole section, which is what it is good at when
     sliced narrow, and the couple get the one place with room for them. */
  .sec-photo::before{background-position:50% 50%}

  .sec-photo-band{display:block;top:auto;height:clamp(330px,56vh,580px);
    -webkit-mask-image:linear-gradient(to bottom,transparent 0%,#000 30%);
            mask-image:linear-gradient(to bottom,transparent 0%,#000 30%)}
  .sec-photo-band::before{background-position:50% 32%}
  /* it sits over the forest, so its veil only has to lift the copy, not the
     picture underneath it */
  .sec-photo-band::after{background:linear-gradient(to bottom,
    rgba(244,239,230,.56) 0%, rgba(244,239,230,.42) 42%, rgba(244,239,230,.32) 100%)}
  .sec:nth-of-type(odd) .sec-photo-band::after{background:linear-gradient(to bottom,
    rgba(234,226,212,.56) 0%, rgba(234,226,212,.42) 42%, rgba(234,226,212,.32) 100%)}
  /* Two cards stacked need a mark between them or they read as one long
     column of text; the second card's postcard is that mark. */
  .wh-card + .wh-card{padding-top:clamp(2.2rem,7vw,2.8rem);border-top:1px solid var(--rule-ink)}
  .wh-note{max-width:40ch}
  .sec-leaf{opacity:.13;width:clamp(128px,42vw,200px)}
}

/* ============================================================
   SIGN-OFF - the letter's close, signed by the pack.

   The names in their script, then five paw prints stamped in burgundy ink,
   each the size of its dog and pressed at its own angle (--z and --r
   inline), roughened a touch by #soInk so they read as pressed rather than
   drawn. Percy signs with his halo. A print wiggles when you point at it.

   The photograph uses the shared .sec-photo layer, but its veil is its own:
   the letter sits in the middle rather than at the top, so the photo is
   held back most behind the names and let through at the edges.
   ============================================================ */
.so{min-height:0;padding:clamp(3.6rem,8vw,6rem) 0 clamp(3.8rem,8vw,6.2rem)}
.so-defs{position:absolute}
/* Every section opens with a short ramp out of the one before, painted over
   it; over a photograph that ramp was a band with a hard lower edge. Here it
   fades from the stock above into nothing, so the photo comes up through it. */
.sec + .sec.so:nth-of-type(even)::before{background:linear-gradient(to bottom, var(--cream-deep) 0%, rgba(234,226,212,0) 100%)}
.sec + .sec.so:nth-of-type(odd)::before{background:linear-gradient(to bottom, var(--cream) 0%, rgba(244,239,230,0) 100%)}
.so .so-photo::before{background-position:50% 45%;opacity:.5;
  filter:grayscale(.2) brightness(1.3) contrast(.72) saturate(1.15) blur(2px)}
.so .so-photo::after,.sec:nth-of-type(odd).so .so-photo::after{
  background:radial-gradient(ellipse 62% 70% at 50% 52%,
    rgba(244,239,230,.9) 0%, rgba(244,239,230,.8) 45%, rgba(244,239,230,.45) 78%, rgba(244,239,230,.3) 100%)}
.so-letter{max-width:560px;margin:0 auto;text-align:center}
.so-rule{width:38px;height:1px;margin:0 auto 1.6rem;background:var(--gold-deep)}
.so-with{font-family:var(--font-display);font-style:italic;font-size:clamp(1.1rem,2.4vw,1.35rem);color:var(--ink-mute)}
.so-names{margin-top:.1rem;font-family:var(--font-script);font-size:clamp(3.2rem,9vw,5.2rem);line-height:1.1;color:var(--ink)}
.so-names .amp{color:var(--gold-deep);font-size:.9em}
.so-and{margin-top:1.4rem;font-family:var(--font-ui);font-size:10px;font-weight:600;letter-spacing:.24em;
  text-transform:uppercase;color:var(--olive)}
.so-paws{display:flex;justify-content:center;align-items:flex-end;gap:clamp(.8rem,3.4vw,1.8rem);
  margin:1.2rem 0 0;padding:0;list-style:none}
.so-paw{position:relative;display:flex;flex-direction:column;align-items:center;gap:.5rem}
.so-paw svg{width:var(--z);height:auto;fill:var(--burgundy);opacity:var(--o,.82);rotate:var(--r);
  filter:url(#soInk);transition:rotate .35s cubic-bezier(.34,1.56,.64,1),translate .35s cubic-bezier(.34,1.56,.64,1)}
.so-paw:hover svg{rotate:calc(var(--r) * -1);translate:0 -4px}
.so-paw b{font-family:var(--font-display);font-style:italic;font-weight:400;
  font-size:clamp(.9rem,1.9vw,1.02rem);color:var(--ink-mute)}
.so-paw-percy::before{content:"";position:absolute;left:50%;top:-11px;width:26px;height:7px;translate:-50% 0;
  border:1.4px solid var(--gold);border-radius:50%;opacity:.9;box-shadow:0 0 6px rgba(201,162,75,.35)}
@media (prefers-reduced-motion:reduce){ .so-paw svg{transition:none} }

/* ============================================================
   BACK TO TOP - a paw in the bottom right corner.

   Just the print, no disc: the same loose paw the rail and the headings use,
   with a small caret over it and a faint cream halo so it holds over photos.
   On hover it takes a step up. Over the dark footer it turns gold, the
   hero's colour on dark, because olive on espresso needs a glow so big it
   reads as a smudge; site.js sets .is-dark. The box stays 48px, which is
   the tap target, whatever the paw is drawn at. It arrives with the nav bar
   and the rail, once the hero is behind you. Below the music capsule's tier.
   ============================================================ */
.totop{position:fixed;right:clamp(12px,2vw,30px);bottom:clamp(12px,2.4vh,26px);z-index:54;
  width:48px;height:52px;display:grid;place-items:center;align-content:center;gap:2px;
  color:var(--olive-deep);text-decoration:none;
  opacity:0;translate:0 12px;pointer-events:none;
  transition:opacity .4s ease,translate .4s cubic-bezier(.16,1,.3,1),color .4s ease}
.totop.is-in{opacity:1;translate:none;pointer-events:auto}
.totop-up{width:12px;height:7px;fill:none;stroke:currentColor;stroke-width:1.7;
  stroke-linecap:round;stroke-linejoin:round;opacity:.75;
  filter:drop-shadow(0 0 3px rgba(251,248,242,.9));transition:translate .3s ease}
.totop-paw{width:32px;height:30px;fill:currentColor;rotate:-8deg;
  filter:drop-shadow(0 1px 1px rgba(251,248,242,.85)) drop-shadow(0 0 5px rgba(251,248,242,.7));
  transition:translate .3s cubic-bezier(.34,1.56,.64,1),rotate .3s ease}
.totop.is-dark{color:var(--gold)}
.totop.is-dark .totop-paw,.totop.is-dark .totop-up{filter:drop-shadow(0 0 6px rgba(201,162,75,.35))}
.totop:hover .totop-paw{translate:0 -3px;rotate:6deg}
.totop:hover .totop-up{translate:0 -2px;opacity:1}
.totop:focus-visible{outline:2px solid var(--olive);outline-offset:2px;border-radius:6px}
/* on a narrow screen the footer's last line runs under the paw's corner, so
   the footer ends with room for it */
@media (max-width:640px){ main > .foot{padding-bottom:88px} }   /* main >: .foot's own rule comes later */
@media (prefers-reduced-motion:reduce){
  .totop,.totop-paw,.totop-up{transition:none}
}

/* ============================================================
   MUSIC - the capsule on the right edge, and what it opens to.

   Jewelle & Paolo's placement: a tab tucked against the right edge at mid
   height, lower on a phone where a thumb is. It shows four bars moving while
   the music plays and a crossed-out speaker when it is muted, and it opens a
   card with the track, its credit, mute and volume. Olive on the tab, so it
   reads on the dark hero and the cream sections alike.
   ============================================================ */
.mu{position:fixed;right:0;top:50%;z-index:55;pointer-events:none}
.mu > *{pointer-events:auto}
.mu-tab{position:absolute;right:0;top:0;translate:0 -50%;width:46px;height:74px;
  display:grid;place-items:center;padding:0 0 0 5px;cursor:pointer;color:var(--paper);
  background:linear-gradient(135deg,var(--olive-deep),var(--forest));
  border:1px solid rgba(201,162,75,.55);border-right:0;border-radius:26px 0 0 26px;
  box-shadow:-6px 0 18px rgba(36,28,23,.28);
  transition:translate .3s cubic-bezier(.34,1.56,.64,1),opacity .25s ease}
.mu-tab:hover{translate:-4px -50%}
.mu-tab:focus-visible{outline:2px solid var(--gold);outline-offset:2px}
.mu.is-open .mu-tab{opacity:0;pointer-events:none}
/* THE NOTE. The TV's own glyph, the beamed pair out of its speaker. While
   the music plays it sways, and single notes drift up and out of the capsule
   in gold, the way they rise from the television; muted, it dims and takes a
   slash. Gold is fine here: these are ornament, not type. */
.mu-glyph{width:26px;height:26px;transform-origin:50% 80%}
.mu-slash{position:absolute;left:50%;top:50%;width:30px;height:2px;margin:-1px 0 0 -12px;
  border-radius:1px;background:currentColor;rotate:-45deg;opacity:0;
  box-shadow:0 0 0 1.5px var(--olive-deep)}
.mu.is-muted .mu-glyph{opacity:.55}
.mu.is-muted .mu-slash{opacity:1}
.mu-float{position:absolute;left:14px;top:50%;width:0;height:0;pointer-events:none}
.mu-float svg{position:absolute;left:0;top:0;width:var(--s);height:var(--s);margin:calc(var(--s) / -2) 0 0 calc(var(--s) / -2);
  color:var(--gold);opacity:0;overflow:visible}
@media (prefers-reduced-motion:no-preference){
  .mu.is-playing .mu-glyph{animation:muSway 2.4s ease-in-out infinite}
  .mu.is-playing .mu-float svg{animation:muFloat var(--t) ease-out infinite var(--d)}
}
@keyframes muSway{0%,100%{rotate:-7deg}50%{rotate:7deg}}
@keyframes muFloat{
  0%  {opacity:0;  transform:translate(0,4px) rotate(0) scale(.7)}
  18% {opacity:.9; transform:translate(calc(var(--x) * .3),calc(var(--y) * .25)) rotate(calc(var(--r) * .4)) scale(1)}
  70% {opacity:.55;transform:translate(calc(var(--x) * .8),calc(var(--y) * .75)) rotate(var(--r)) scale(1.05)}
  100%{opacity:0;  transform:translate(var(--x),var(--y)) rotate(calc(var(--r) * 1.3)) scale(1.1)}}
.mu svg path{fill:currentColor}
.mu svg .mu-x,.mu svg .mu-wave{fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round}

.mu-panel{position:absolute;right:0;top:0;width:min(86vw,300px);padding:1.1rem 1.2rem 1.2rem;
  background:var(--paper);border:1px solid var(--rule-ink);border-right:0;border-radius:18px 0 0 18px;
  box-shadow:-18px 0 40px -12px rgba(36,28,23,.35);text-align:left;
  opacity:0;translate:106% -50%;pointer-events:none;
  transition:translate .42s cubic-bezier(.34,1.3,.5,1),opacity .25s ease}
.mu.is-open .mu-panel{opacity:1;translate:0 -50%;pointer-events:auto}
.mu-now{font-family:var(--font-ui);font-size:10px;font-weight:600;letter-spacing:.18em;
  text-transform:uppercase;color:var(--olive)}
.mu-title{margin-top:.35rem;font-family:var(--font-display);font-size:1.45rem;line-height:1.15;color:var(--ink)}
.mu-by{margin-top:.2rem;font-family:var(--font-display);font-style:italic;font-size:.98rem;color:var(--ink-mute)}
.mu-by a{color:var(--burgundy);text-underline-offset:3px}
.mu-ctrls{display:flex;align-items:center;gap:.8rem;margin-top:1rem;padding-top:.9rem;border-top:1px solid var(--rule-ink)}
.mu-mute{flex:none;display:inline-flex;align-items:center;gap:.4rem;min-height:40px;padding:.4rem .85rem .4rem .6rem;
  border:1px solid var(--olive-deep);border-radius:999px;background:var(--olive-deep);color:var(--paper);
  cursor:pointer;font-family:var(--font-ui);font-size:12px;font-weight:500}
.mu-mute svg{width:20px;height:20px}
.mu-mute .mu-ic-off{display:none}
.mu.is-muted .mu-mute{background:transparent;color:var(--olive-deep)}
.mu.is-muted .mu-mute .mu-ic-on{display:none}
.mu.is-muted .mu-mute .mu-ic-off{display:block}
.mu-mute:focus-visible,.mu-vol:focus-visible{outline:2px solid var(--olive);outline-offset:2px}
.mu-vol-l{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
/* the slider: a hairline track filled to the level, and a round olive thumb */
.mu-vol{--fill:55%;flex:1;min-width:0;height:24px;margin:0;background:transparent;cursor:pointer;
  -webkit-appearance:none;appearance:none}
.mu-vol::-webkit-slider-runnable-track{height:4px;border-radius:2px;
  background:linear-gradient(90deg,var(--olive) var(--fill),rgba(36,28,23,.16) var(--fill))}
.mu-vol::-moz-range-track{height:4px;border-radius:2px;background:rgba(36,28,23,.16)}
.mu-vol::-moz-range-progress{height:4px;border-radius:2px;background:var(--olive)}
.mu-vol::-webkit-slider-thumb{-webkit-appearance:none;width:18px;height:18px;margin-top:-7px;border-radius:50%;
  background:var(--paper);border:2px solid var(--olive-deep);box-shadow:0 1px 3px rgba(36,28,23,.3)}
.mu-vol::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:var(--paper);border:2px solid var(--olive-deep)}
/* the player itself: there, because the API needs it to be, and nowhere */
.mu-src{position:absolute;right:0;top:0;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none}
@media (max-width:760px){ .mu{top:74%} }
@media (prefers-reduced-motion:reduce){
  .mu-glyph,.mu-float svg{animation:none}
  .mu-tab,.mu-panel{transition:none}
}

/* ============================================================
   RSVP - find your invitation, answer for everyone on it.

   One paper card, the FAQ's stock, and one screen at a time inside it.
   Fields are 16px on purpose: anything smaller and iOS zooms the page the
   moment a guest taps into it.
   ============================================================ */
.rv{max-width:560px;margin:clamp(2rem,5vw,3rem) auto 0;text-align:left}
/* .btn sets its own display, which beats the browser's own [hidden] rule:
   the calendar button stayed up under "We'll miss you" */
.rv [hidden]{display:none!important}
.rv-preview{margin:0 0 1rem;padding:.7rem .9rem;border:1px dashed var(--gold-deep);border-radius:3px;
  font-family:var(--font-ui);font-size:13px;line-height:1.55;color:var(--ink-mute);text-align:center;text-wrap:balance}
.rv-preview b{font-weight:600;color:var(--ink)}
.rv-card{padding:clamp(1.3rem,3.6vw,2.2rem);background:var(--paper);border:1px solid var(--rule-ink);
  border-radius:3px;box-shadow:0 1px 2px rgba(36,28,23,.06),0 14px 30px -16px rgba(36,28,23,.24)}
.rv-deadline{margin-bottom:.3rem;font-family:var(--font-display);font-style:italic;
  font-size:clamp(1rem,2.2vw,1.1rem);color:var(--ink-mute);text-align:center}
.rv-deadline b{font-style:normal;font-weight:500;color:var(--burgundy)}
.rv-lab{display:block;margin:1.1rem 0 .45rem;font-family:var(--font-ui);font-size:11px;font-weight:600;
  letter-spacing:.16em;text-transform:uppercase;color:var(--olive)}
.rv-opt{margin-left:.4em;font-weight:400;letter-spacing:.04em;text-transform:none;color:var(--ink-faint)}
.rv-input{display:block;width:100%;padding:.75rem .9rem;border:1px solid rgba(36,28,23,.24);border-radius:3px;
  background:#fff;font-family:var(--font-text);font-size:16px;line-height:1.4;color:var(--ink)}
.rv-input::placeholder{color:var(--ink-faint)}
.rv-input:focus{outline:2px solid var(--olive);outline-offset:1px;border-color:var(--olive)}
.rv-note{resize:vertical;min-height:4.5rem}
.rv-find{display:flex;gap:.6rem;align-items:stretch}
.rv-find .rv-input{flex:1;min-width:0}
.rv .btn{justify-content:center;padding:14px 22px;white-space:nowrap}
.rv .btn:disabled{opacity:.6;cursor:wait;transform:none}
.rv-msg{min-height:1.5em;margin-top:.7rem;font-family:var(--font-ui);font-size:14px;line-height:1.5;color:var(--burgundy)}
.rv-h{font-family:var(--font-display);font-weight:400;font-size:clamp(1.5rem,3.6vw,1.9rem);
  line-height:1.2;color:var(--ink);outline:none}
.rv-p{margin-top:.6rem;font-family:var(--font-text);font-size:15.5px;line-height:1.65;color:var(--ink-mute)}
.rv-p a{color:var(--burgundy);text-underline-offset:3px}
/* the unplugged ask, on the thank-you for anyone coming */
.rv-ask{margin:1rem 0 0;font-family:var(--font-text);font-size:15.5px;line-height:1.65;color:var(--ink-mute)}
.rv-ask b{font-weight:600;color:var(--ink)}
.rv-who{margin:1rem 0 .2rem;font-family:var(--font-display);font-style:italic;
  font-size:clamp(1.3rem,3.2vw,1.6rem);color:var(--burgundy)}
.rv-actions{display:flex;flex-wrap:wrap;align-items:center;gap:.8rem 1.2rem;margin-top:1.4rem}
.rv-link{appearance:none;padding:.5rem 0;border:0;background:none;cursor:pointer;
  font-family:var(--font-ui);font-size:13px;color:var(--ink-mute);
  text-decoration:underline;text-decoration-color:rgba(36,28,23,.3);text-underline-offset:3px}
.rv-link:hover{color:var(--ink)}
.rv-link:focus-visible{outline:2px solid var(--olive);outline-offset:3px}

/* everyone on the invitation, a yes or a no each */
.rv-people{margin:1.1rem 0 0;padding:0;list-style:none;border-top:1px solid var(--rule-ink)}
.rv-person{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.55rem 1rem;
  padding:.85rem 0;border-bottom:1px solid var(--rule-ink)}
.rv-name{font-family:var(--font-display);font-size:1.15rem;color:var(--ink)}
.rv-pills{display:flex;gap:.4rem}
.rv-pill{appearance:none;min-height:40px;padding:.45rem .9rem;border:1px solid rgba(36,28,23,.22);border-radius:999px;
  background:#fff;cursor:pointer;font-family:var(--font-ui);font-size:13px;color:var(--ink-mute);
  transition:background-color .2s ease,color .2s ease,border-color .2s ease}
.rv-pill:hover{border-color:var(--olive);color:var(--ink)}
.rv-pill:focus-visible{outline:2px solid var(--olive);outline-offset:2px}
.rv-pill[data-v="yes"][aria-pressed="true"]{background:var(--olive-deep);border-color:var(--olive-deep);color:var(--paper)}
.rv-pill[data-v="no"][aria-pressed="true"]{background:var(--ink-mute);border-color:transparent;color:var(--paper)}
.rv-more .rv-lab{margin-top:1.3rem}

/* ---- the real form's extra pieces ----
   Each question is its own block with room above it. Groups of fields sit in
   plain fieldsets; the tick boxes are drawn, not the browser's, in the same
   olive as the Attending pill. */
.rv-set{margin:1.9rem 0 0;padding:0;border:0;min-width:0}
.rv-set > legend.rv-lab{float:left;width:100%;margin:0 0 .6rem;padding:0}
.rv-set > legend.rv-lab + *{clear:left}
.rv-more > .rv-lab{margin-top:1.9rem}
.rv-more > .rv-set:first-child{margin-top:1.6rem}
.rv-companions{display:grid;gap:.5rem}
.rv-ticks{display:flex;flex-wrap:wrap;gap:.5rem .7rem}
.rv-tick{position:relative;display:inline-flex;align-items:center;gap:.6rem;min-height:44px;padding:.45rem .95rem .45rem .7rem;
  border:1px solid rgba(36,28,23,.22);border-radius:999px;background:#fff;cursor:pointer;
  font-family:var(--font-ui);font-size:14px;color:var(--ink-mute)}
.rv-tick input{position:absolute;opacity:0;pointer-events:none}
/* the box sits beside the words, not among them, so a wrapped line flows
   under the words and the box stays level with the first line */
.rv-tick > span{position:relative;display:block;padding-left:28px}
.rv-tick > span::before{content:"";position:absolute;left:0;top:calc(.7em - 9px);top:calc(.5lh - 9px);width:18px;height:18px;
  border:1.5px solid rgba(36,28,23,.35);border-radius:5px;background:#fff center/12px no-repeat}
.rv-tick:has(input:checked){border-color:var(--olive-deep);color:var(--ink)}
.rv-tick:has(input:checked) > span::before{border-color:var(--olive-deep);background-color:var(--olive-deep);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 6.2 5 8.6l4.5-5' fill='none' stroke='%23fff' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")}
.rv-tick:has(input:focus-visible){outline:2px solid var(--olive);outline-offset:2px}
/* the follow-up to a question hangs under it, on a thin rule */
.rv-sub{margin:.9rem 0 0 .6rem;padding:.1rem 0 .2rem 1rem;border-left:2px solid var(--olive)}
.rv-lab-sub{margin:0 0 .45rem}
.rv-need + .rv-need{margin-top:.55rem}
/* THE SLUMBOOK ENTRY: message, photo and the share tick, one panel */
.rv-slum{margin-top:2.1rem;padding:1.1rem;background:var(--cream-deep);border-radius:10px}
.rv-slum > legend.rv-lab{margin-bottom:.6rem}
.rv-slum .rv-input{background:#fff}
.rv-email-kept{margin:0;font-family:var(--font-text);font-size:15px;line-height:1.6;color:var(--ink-mute)}
.rv-email-kept b{font-weight:600;color:var(--ink)}
.rv-email-kept .rv-link{margin-left:.4rem;font-size:14px}
.rv-locked{padding:.2rem 0 .1rem}
.rv-locked-h{margin:0;font-family:var(--font-display);font-style:italic;font-size:1.15rem;line-height:1.45;color:var(--ink)}
.rv-locked-p{margin:.35rem 0 0;font-family:var(--font-ui);font-size:13px;color:var(--ink-mute)}
.rv-photo{display:flex;flex-wrap:wrap;align-items:center;gap:.6rem 1rem;margin-top:.8rem}
.rv-file{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
.rv-photo-btn{display:inline-flex;align-items:center;gap:.5rem;min-height:44px;padding:.45rem 1rem;border:1px dashed rgba(36,28,23,.35);
  border-radius:999px;background:#fff;cursor:pointer;font-family:var(--font-ui);font-size:14px;color:var(--ink-mute)}
.rv-photo-btn:hover{border-color:var(--olive);color:var(--ink)}
.rv-file:focus-visible + .rv-photo-btn{outline:2px solid var(--olive);outline-offset:2px}
.rv-photo-btn svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linejoin:round}
.rv-photo-prev{width:64px;height:64px;border-radius:8px;border:1px solid var(--rule-ink);background:var(--paper) center/cover}
.rv-share{margin-top:.9rem;border-radius:12px;padding:.65rem 1rem .65rem .75rem;line-height:1.4}
.rv-share b{font-weight:600;color:var(--burgundy)}
.rv-slum + .rv-lab{margin-top:2rem}
.rv-choices{display:flex;flex-wrap:wrap;gap:.6rem;margin-top:1rem}
.rv-choice{appearance:none;min-height:46px;padding:.5rem 1.2rem;border:1px solid var(--olive-deep);border-radius:999px;background:#fff;cursor:pointer;
  font-family:var(--font-display);font-size:1.15rem;color:var(--ink)}
.rv-choice:hover{background:var(--olive-deep);color:var(--paper)}
.rv-choice:disabled{opacity:.5;cursor:wait}
/* THE KEEPSAKE: what they sent, framed to be screenshotted */
.rv-keep{margin-top:1.1rem;padding:1.3rem 1.2rem 1.1rem;text-align:left;background:#fff;border:1px solid var(--rule-ink);border-radius:10px;
  box-shadow:0 12px 30px -20px rgba(36,28,23,.45)}
.rv-keep-mono{display:block;width:56px;aspect-ratio:720/662;margin:0 auto .5rem;background:var(--burgundy);
  -webkit-mask:url('../assets/monogram.webp') center/contain no-repeat;mask:url('../assets/monogram.webp') center/contain no-repeat}
.rv-keep-for{margin:0 0 .4rem;text-align:center;font-family:var(--font-ui);font-size:10.5px;font-weight:600;letter-spacing:.18em;
  text-transform:uppercase;color:var(--olive)}
.rv-keep .rv-summary{margin-top:.4rem}
.rv-keep .rv-summary li:last-child{border-bottom:0}
.rv-keep-msg{margin-top:.8rem;padding-top:.8rem;border-top:1px solid var(--rule-ink)}
.rv-keep-msg .rv-sum-k{display:block;width:auto;margin:0 0 .4rem}
.rv-keep-text{margin:0;white-space:pre-wrap;font-family:var(--font-display);font-style:italic;font-size:1.15rem;line-height:1.5;color:var(--ink)}
.rv-keep-photo{display:block;max-width:100%;max-height:340px;margin-top:.8rem;border-radius:8px}
.rv-summary{margin:1rem 0 0;padding:0;list-style:none}
.rv-summary li{display:flex;gap:1rem;padding:.55rem 0;border-bottom:1px solid var(--rule-ink)}
.rv-sum-k{flex:none;width:8.5rem;font-family:var(--font-ui);font-size:11px;font-weight:600;letter-spacing:.14em;
  text-transform:uppercase;color:var(--olive);padding-top:.2rem}
.rv-sum-v{font-family:var(--font-display);font-size:1.1rem;color:var(--ink)}

@media (max-width:640px){
  .rv-find{flex-direction:column}
  .rv-sum-k{width:6.5rem}
}

/* ============================================================
   OUR STORY - before they met.

   Old prints, not screen images: a white border, a little fade, and each
   pair tilted in towards the other. One pair at a time, because three rows
   of them were most of a screen before the story had even started. Each
   side is a stack of its three prints in one grid cell, and Percy's lines
   are stacked the same way, so the tallest of each sets the height and
   nothing below moves when the moment changes. The prints come in from
   their own side, so each pair meets in the middle.
   ============================================================ */
.st{margin-top:clamp(2.2rem,5vw,3.4rem)}
.st-say{max-width:44ch;margin:0 auto;font-family:var(--font-display);font-style:italic;
  font-size:clamp(1.05rem,2.4vw,1.25rem);line-height:1.55;color:var(--ink-mute);text-wrap:balance}
.st-open{font-size:clamp(1.15rem,2.8vw,1.4rem);color:var(--ink)}
.st-names{display:grid;grid-template-columns:1fr 1fr;gap:clamp(1rem,4vw,2.4rem);
  max-width:520px;margin:clamp(1.6rem,4vw,2.2rem) auto .9rem;
  font-family:var(--font-ui);font-size:11px;font-weight:600;letter-spacing:.3em;
  text-transform:uppercase;color:var(--olive)}
.st-pair{display:grid;grid-template-columns:1fr 1fr;gap:clamp(1rem,4vw,2.4rem);
  max-width:520px;margin:0 auto}
.st-slot{display:grid}
/* the tilt is `rotate`, the arrival is `translate`: two properties, so
   neither overwrites the other */
/* A CROSSFADE WITHOUT THE GHOST. Two prints fading through each other are
   both half-transparent halfway, and the paper shows through both: a double
   exposure, and three of them after a quick run of taps. So the one arriving
   fades in ON TOP, and the one being left (.was-on, set by site.js) stays
   solid underneath and only drops out once it is covered. Any other print,
   one skipped past in a quick run of taps, goes at once. */
.st-print{grid-area:1/1;margin:0;padding:clamp(6px,1.2vw,10px);background:var(--paper);
  box-shadow:0 1px 2px rgba(36,28,23,.12),0 14px 28px -14px rgba(36,28,23,.4);
  rotate:var(--r,0deg);opacity:0;translate:var(--from,0) 0;transition:none}
.st-print.was-on{opacity:0;translate:none;transition:opacity 0s .6s}
.st-slot-l .st-print{--from:-18px}
.st-slot-r .st-print{--from:18px}
.st-print.is-on{z-index:1;opacity:1;translate:none;
  transition:opacity .6s ease,translate .6s cubic-bezier(.16,1,.3,1)}
/* the print is the button that opens it larger */
.st-zoom{display:block;width:100%;padding:0;border:0;background:none;cursor:var(--cur-link,zoom-in)}
.st-zoom:focus-visible{outline:2px solid var(--olive);outline-offset:4px}
/* one shape for all six, top-weighted so a crop never takes a face */
.st-print img{display:block;width:100%;height:auto;aspect-ratio:3/4;object-fit:cover;
  object-position:50% 20%;filter:sepia(.14) saturate(.92) contrast(.97)}
/* PERCY SAYS IT. His head beside each thing he says, the way Jackson answers
   in the FAQ, with his halo. The one drawn into head-percy.webp is a hairline
   of cream that vanishes at this size on this paper, so it is drawn again
   over it in the hero halo's glow, scaled to the head. The text sits to his
   right, left-aligned, and the pair is centred as one. */
.st-voice{display:flex;align-items:flex-start;justify-content:center;gap:.65rem;
  max-width:min(100%,34rem);margin:0 auto;text-align:left}
/* pretty, not balance: balanced lines stop short of the box, and beside a
   head that left the pair sitting off-centre on a phone */
.st-voice .st-say{margin:0;max-width:none;text-wrap:pretty}
.st-av{position:relative;flex:none;width:40px;height:40px;margin-top:-.2rem}
.st-av img{display:block;width:100%;height:100%;object-fit:contain;
  filter:drop-shadow(0 1px 1px rgba(36,28,23,.22)) drop-shadow(0 4px 8px rgba(36,28,23,.18))}
.st-av::before{content:"";position:absolute;z-index:1;left:50%;top:3%;width:62%;
  aspect-ratio:3.4/1;translate:-50% 0;border:max(1px,.04em) solid rgba(255,248,224,.95);border-radius:50%;font-size:36px;
  filter:drop-shadow(0 0 2px rgba(240,209,137,.95)) drop-shadow(0 0 5px rgba(201,162,75,.8))}
.st-lines{display:grid;margin-top:clamp(1.1rem,2.8vw,1.5rem)}
/* ON A PHONE HE SITS ABOVE, NOT BESIDE. Once a line wraps, its box takes the
   full width while the words stop short, so a head at the left of it always
   read as off-centre. Stacked and centred, it is symmetric at any length. */
@media (max-width:640px){
  .st-voice{flex-direction:column;align-items:center;gap:.35rem;text-align:center}
  .st-voice .st-say{text-wrap:balance}
  .st-line{align-items:center}
}
/* ONE PERCY PER LINE. With one head for all three the head sat at the left of
   a box as wide as the longest line, and drifted away from the short ones.
   Each line now carries its own, centred with it as one, a little smaller
   than the head that opens the section. Text has no background to hide
   behind, so these cannot overlap the way the prints do: the old line and
   its Percy go first, and the new ones come in after, Percy turning in from
   his own side of the page each time. */
.st-line{grid-area:1/1;align-items:center;opacity:0;transition:opacity .3s ease}
.st-line.is-on{opacity:1;transition:opacity .45s ease .3s}
.st-line .st-av{width:30px;height:30px;margin-top:0;
  rotate:var(--tilt,0deg);translate:var(--tx,0) 0;
  transition:rotate .3s ease,translate .3s ease}
.st-line.is-on .st-av{rotate:0deg;translate:none;
  transition:rotate .7s cubic-bezier(.34,1.56,.64,1) .3s,translate .7s cubic-bezier(.16,1,.3,1) .3s}
.st-moments{display:flex;justify-content:center;gap:clamp(.2rem,1.4vw,.8rem);margin-top:.9rem}
.st-moment{appearance:none;border:0;background:transparent;position:relative;
  padding:.5rem .55rem;min-height:40px;font-family:var(--font-ui);font-size:10px;font-weight:600;
  letter-spacing:.2em;text-transform:uppercase;color:var(--ink-faint);transition:color .25s ease}
.st-moment:hover,.st-moment[aria-pressed="true"]{color:var(--olive)}
.st-moment[aria-pressed="true"]::after{content:"";position:absolute;left:.55rem;right:.55rem;
  bottom:.3rem;height:1px;background:var(--olive);opacity:.6}
.st-moment:focus-visible{outline:2px solid var(--olive);outline-offset:2px}
/* the hand-off to whatever the couple write next */
.st-end{margin:clamp(2rem,5vw,3rem) auto 0;font-family:var(--font-display);font-style:italic;
  font-size:clamp(1.25rem,3.2vw,1.6rem);color:var(--ink)}
.st-end::after{content:"";display:block;width:38px;height:1px;margin:1.1rem auto 0;background:var(--gold)}

/* ---- their chapters ----------------------------------------------------
   The couple's own words, so the page stays out of their way: a title, one
   line of theirs pulled out large, and the chapter itself set for reading.
   A line runs down the middle with a mark for each chapter; titles sit on
   one side and text on the other, swapping every chapter, and each title
   stays in view while its chapter is read. On a phone the line moves to the
   left edge and everything reads straight down beside it. */
.ch-run{--node:46px;position:relative;max-width:1040px;margin:clamp(3.4rem,8vw,5.4rem) auto 0;
  display:grid;gap:clamp(3.4rem,8vw,5.6rem);text-align:left}
.ch-line{position:absolute;left:50%;top:calc(var(--node) / 2);bottom:0;width:1px;margin-left:-.5px;transform-origin:top;
  background:linear-gradient(to bottom,var(--gold) 0,var(--gold) calc(100% - 140px),rgba(201,162,75,0) 100%);opacity:.7}
.ch{display:grid;grid-template-columns:minmax(0,1fr) var(--node) minmax(0,1fr);column-gap:clamp(1.4rem,3.6vw,2.8rem);align-items:start}
.ch-node{grid-column:2;grid-row:1;position:relative;z-index:1;display:grid;place-items:center;width:var(--node);height:var(--node);
  border:1px solid var(--gold);border-radius:50%;background:var(--cream-deep);color:var(--olive-deep)}
.ch-node svg{width:24px;height:24px}
.ch-node .ch-paw{width:21px;height:auto;fill:currentColor}
.ch-side{grid-column:1;grid-row:1;position:sticky;top:calc(var(--nav-h) + 1.6rem);text-align:right}
.ch-main{grid-column:3;grid-row:1}
.ch:nth-child(odd) .ch-side{grid-column:3;text-align:left}
.ch:nth-child(odd) .ch-main{grid-column:1}
.ch-n{padding-top:.2rem;font-family:var(--font-ui);font-size:10px;font-weight:600;letter-spacing:.24em;text-transform:uppercase;color:var(--olive)}
.ch-t{margin:.45rem 0 0;font-family:var(--font-display);font-weight:400;font-size:clamp(1.8rem,4vw,2.5rem);line-height:1.08;color:var(--ink)}
.ch-pull{position:relative;margin:1.3rem 0 0;padding-top:1.6rem;font-family:var(--font-display);font-style:italic;
  font-size:clamp(1.12rem,2.2vw,1.36rem);line-height:1.45;color:var(--burgundy);text-wrap:balance}
.ch-pull::before{content:"\201C";position:absolute;top:-.35rem;font-size:3.1rem;line-height:1;font-style:normal;color:var(--gold)}
.ch-side .ch-pull::before{right:-.04em}
.ch:nth-child(odd) .ch-side .ch-pull::before{right:auto;left:-.04em}
.ch-text{--lines:6;position:relative;font-family:var(--font-book);font-size:clamp(1.1rem,1.75vw,1.2rem);line-height:1.75;color:var(--ink)}
.ch-text p{margin:0;text-wrap:pretty}
/* THE HIGHLIGHTS: the turns of their story, marked the way a reader would
   mark a letter, a gold highlighter stroke under the book face's own italic,
   which answers the italic pull quote beside each chapter. */
.ch-hi{padding:0 .06em;color:inherit;font-style:italic;
  background:linear-gradient(to top,rgba(201,162,75,.30) 0,rgba(201,162,75,.30) .46em,transparent .46em);
  -webkit-box-decoration-break:clone;box-decoration-break:clone}

/* FOLDED: the first few lines, the last of them going soft. The blur is a
   strip laid over the bottom lines, strongest at the fold, and the lines
   also fade into the ground; either alone read as a rendering fault. It is
   only there while a chapter is folded, and it never moves. */
.ch.is-folding .ch-text{max-height:calc(var(--lines) * 1.75em);overflow:hidden;
  transition:max-height .75s cubic-bezier(.3,.7,.2,1)}
.ch.is-folding:not(.is-open) .ch-text{
  -webkit-mask-image:linear-gradient(to bottom,#000 calc(100% - 3.4em),rgba(0,0,0,.12) 100%);
          mask-image:linear-gradient(to bottom,#000 calc(100% - 3.4em),rgba(0,0,0,.12) 100%)}
.ch.is-folding:not(.is-open) .ch-text::after{content:"";position:absolute;left:0;right:0;bottom:0;height:3.4em;pointer-events:none;
  -webkit-backdrop-filter:blur(2.6px);backdrop-filter:blur(2.6px);
  -webkit-mask-image:linear-gradient(to bottom,transparent,#000 80%);mask-image:linear-gradient(to bottom,transparent,#000 80%)}
.ch-more{margin-top:.8rem;padding:.35rem 0;border:0;background:none;cursor:pointer;
  font-family:var(--font-ui);font-size:11px;font-weight:600;letter-spacing:.18em;text-transform:uppercase;color:var(--olive-deep);
  text-decoration:underline;text-decoration-color:var(--gold);text-underline-offset:5px}
.ch-more:focus-visible{outline:2px solid var(--olive);outline-offset:3px}

/* the pack, as the last chapter names them, closing the run under the line */
.ch-pack{display:flex;flex-wrap:wrap;justify-content:center;gap:clamp(.8rem,3vw,1.6rem);
  margin:clamp(2.6rem,6vw,3.6rem) auto 0;padding:0;list-style:none}
.ch-dog{position:relative;display:flex;flex-direction:column;align-items:center;gap:.35rem}
.ch-dog img{width:clamp(48px,7vw,64px);height:auto;border-radius:50%;box-shadow:0 4px 12px rgba(36,28,23,.14)}
.ch-dog b{font-family:var(--font-display);font-style:italic;font-weight:400;font-size:.95rem;color:var(--ink-mute)}

@media (max-width:640px){
  .ch-run{--node:38px}
  .ch-line{left:calc(var(--node) / 2)}
  .ch{grid-template-columns:var(--node) minmax(0,1fr);column-gap:1rem}
  .ch-node{grid-column:1}
  .ch-node svg{width:20px;height:20px}
  .ch-node .ch-paw{width:18px}
  .ch-side,.ch:nth-child(odd) .ch-side{grid-column:2;grid-row:1;position:static;text-align:left}
  .ch-main,.ch:nth-child(odd) .ch-main{grid-column:2;grid-row:2;margin-top:1rem}
  .ch-side .ch-pull::before{right:auto;left:-.04em}
  .ch-pull{margin-top:1rem}
  .ch-text{--lines:7}
}

/* THE LINE DRAWS DOWN AS YOU READ, its tip level with the middle of the
   screen: it starts when the top of the run reaches the middle and is done
   when the bottom does. A scale, so the compositor does it. */
@keyframes chDraw{from{scale:1 0}to{scale:1 1}}
@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    .ch-line{animation:chDraw linear both;animation-timeline:view();
      animation-range:cover 50vh cover calc(100% - 50vh)}
  }
}
@media (prefers-reduced-motion:reduce){
  .ch.is-folding .ch-text{transition:none}
}

/* ============================================================
   ATTIRE - three looks from the palette, a lady and a gentleman each.

   A CHIP IS A PAIR. The gentlemen are in suits, not barong, and each suit
   is drawn with a tie that matches its gown, so the colours only ever move
   together. Each chip is split between the two it puts on screen, gown on
   the left and suit on the right, the same way round as the figures.

   The swatches are the moodboard's own five, sampled from the palette page,
   not the site's tokens: the tokens were tuned for type on paper, and these
   are cloth.
   ============================================================ */
.at{margin-top:clamp(1.6rem,4vw,2.4rem)}
/* TWO LINES, ONE THOUGHT EACH: what, then who wears what. Each is its own
   line rather than a measure that happens to break in two, so a wider
   screen cannot pull them back into one and a narrower one wraps each
   within itself. */
.at-code{margin:0 auto;font-family:var(--font-display);font-style:italic;
  font-size:clamp(1rem,2vw,1.12rem);line-height:1.62;color:var(--ink-mute)}
.at-code span{display:block}

/* THE COUPLE STANDS TOGETHER. The drawings are fashion sketches, one figure
   to a frame, all normalised in assets/attire to one 800x1150 canvas: feet on
   the same line, body centred, the ladies a touch shorter than the gentlemen.
   A lady's gown takes about 55-60% of her frame's width, a gentleman about
   45%, so his frame starts --ov of a frame along from hers, which stands
   them shoulder to shoulder with her skirt just behind him. --fw is one
   figure's frame; a couple is 1 + --ov of them. Each couple carries its own
   --ov inline, since each gown flares differently: the olive one sweeps out
   to her right, so that pair stands closest.

   ALL THREE COUPLES ARE ALWAYS HERE, stacked on the same spot. One look is
   the couple that is .is-on; the others wait at opacity 0, so a change is a
   crossfade in place. The group photo is the same three couples moved apart
   into a line, which is why they are separate elements rather than a stack
   of ladies and a stack of gentlemen.

   The view's height is fixed by the figures, whatever is showing, so
   nothing below it moves when the looks change. */
.at-view{--fw:min(300px,calc(33vw + 52px));--ov:.44;position:relative;
  height:calc(var(--fw) * 1.4375);margin:clamp(1.6rem,4vw,2.6rem) auto 0}
.at-stage{position:absolute;inset:0}
.at-couple{position:absolute;left:50%;bottom:0;height:100%;
  width:calc(var(--fw) * (1 + var(--ov)));margin-left:calc(var(--fw) * (1 + var(--ov)) / -2);
  transform-origin:50% 100%;
  transition:transform .95s cubic-bezier(.65,0,.25,1)}
.at-img{position:absolute;bottom:0;width:var(--fw);height:auto;
  opacity:0;translate:0 6px;
  transition:opacity .9s ease,translate .9s cubic-bezier(.16,1,.3,1)}
.at-lady{left:0}
.at-gent{left:calc(var(--fw) * var(--ov))}
.at-couple.is-on .at-img{opacity:1;translate:none}
/* he follows her by a beat, so a change reads as the two of them turning up
   together rather than one picture being swapped for another */
.at-gent{transition-delay:.14s}

/* ---- the group photo ----
   Everyone at once, in a line: the three couples slide out from the middle
   to their places and scale down to fit the width. How far and how small
   depends on how wide the view is against how wide a figure is, which CSS
   cannot divide, so site.js measures it and hands each couple its --gx, --gy
   and --gs. They stand in the chips' order, olive, burgundy, oak leaf, and
   the order is not arbitrary: that way the couples at either end are
   turned in towards the middle. Oak leaf in the middle, which keeps the
   dark suits apart, had them looking out of the picture. Whoever is in the
   middle stands in front, the way a group photo is arranged. */
.at-view.is-all .at-img{opacity:1;translate:none}
.at-view.is-all .at-couple{transform:translate(var(--gx,0),var(--gy,0)) scale(var(--gs,1))}

.at-pick{margin-top:clamp(1rem,2.6vw,1.5rem)}
.at-chips{display:flex;justify-content:center;gap:.35rem}
/* 44px to tap, a 30px disc to see */
.at-chip{display:grid;place-items:center;width:44px;height:44px;padding:0;border:0;
  background:none;border-radius:50%}
.at-chip span{width:30px;height:30px;border-radius:50%;
  background:linear-gradient(135deg,var(--a) 50%,var(--b) 50%);
  box-shadow:inset 0 0 0 1px rgba(36,28,23,.16);
  transition:transform .2s ease,box-shadow .2s ease}
/* the group photo is everyone, so its disc is the whole palette */
.at-chip-all span{background:conic-gradient(#60613a 0 72deg,#91896a 0 144deg,
  #b88f6b 0 216deg,#332118 0 288deg,#4d0011 0)}
.at-chip:hover span{transform:scale(1.1)}
/* pressed follows whatever is on show, turning or pinned, so the ring moves
   along the row with the pictures and says which pair you are looking at */
.at-chip[aria-pressed="true"] span{transform:scale(1.1);
  box-shadow:inset 0 0 0 1px rgba(36,28,23,.16),0 0 0 2px var(--cream),0 0 0 3.5px var(--olive)}
.at-chip:focus-visible{outline:2px solid var(--olive);outline-offset:1px}

.at-name{margin-top:.55rem;font-family:var(--font-display);font-style:italic;
  font-size:clamp(1.05rem,2.2vw,1.2rem);color:var(--ink)}
/* ---- the palette ----
   The moodboard's five as small plain dots, a size and a shape apart from
   the chips above so they read as a key rather than more buttons. The name
   rises over a dot on hover, and on focus, which is what a tap gives a phone
   that has no hover. Each dot's box is 28px so a thumb can find it; the
   colour is the 16px drawn inside. */
.at-palette{display:flex;align-items:center;justify-content:center;gap:.7rem;
  margin-top:.85rem}
.at-palette-l{font-family:var(--font-ui);font-size:10px;font-weight:600;
  letter-spacing:.2em;text-transform:uppercase;color:var(--olive)}
.at-dots{display:flex;margin:0;padding:0;list-style:none}
.at-dot{position:relative;display:grid;place-items:center;width:28px;height:28px;
  border-radius:50%;cursor:default;outline:none}
.at-dot::before{content:"";width:16px;height:16px;border-radius:50%;background:var(--c);
  box-shadow:inset 0 0 0 1px rgba(36,28,23,.16);transition:transform .2s ease}
.at-dot:hover::before,.at-dot:focus::before{transform:scale(1.2)}
.at-dot:focus-visible::before{box-shadow:inset 0 0 0 1px rgba(36,28,23,.16),
  0 0 0 2px var(--cream),0 0 0 3.5px var(--olive)}
/* the name, over the dot, in the dot's own colour. Light type on the three
   dark ones; oak leaf and tan are too pale to hold it at this size (about
   3:1), so those two carry --t and set it in ink (about 5:1). Opacity rather
   than display, so it is read out whether or not it is showing. It opens
   BELOW the dot: above, it sat over the name of the look on show. */
.at-dot-n{position:absolute;top:calc(100% + 4px);left:50%;z-index:3;
  padding:.32rem .6rem;border-radius:3px;background:var(--c);color:var(--t,var(--paper));
  font-family:var(--font-ui);font-size:10px;font-weight:600;letter-spacing:.16em;
  text-transform:uppercase;white-space:nowrap;pointer-events:none;
  opacity:0;transform:translate(-50%,-3px);transition:opacity .2s ease,transform .2s ease}
.at-dot-n::after{content:"";position:absolute;bottom:100%;left:50%;margin-left:-4px;
  border:4px solid transparent;border-bottom-color:var(--c)}
.at-dot:hover .at-dot-n,.at-dot:focus .at-dot-n{opacity:1;transform:translate(-50%,0)}
.at-live{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}

/* YUMI HAS THE LAST WORD. The one rule of the section was a bordered box in
   the UI face, the only thing on the page that looked like a form. It is
   hers to say instead, in the bubble the FAQ answers in (.fq-a, .fq-b and
   .fq-av, defined with the FAQ below), centred here rather than hung off
   the left of a card. Named through .at so it outranks the FAQ's own
   margin and width, which come later in this file. */
/* the room above her is also where a palette dot's name opens */
/* ---- for the principal sponsors ----
   Set off from the guests' looks by a gold rule and its own label, the same
   small caps as "Our palette", so it reads as a separate note rather than
   one more look. The swatch is the chocolate of the couple's reference
   gowns, warmer and lighter than espresso, which it must not be mistaken
   for. */
.at-sp{margin:clamp(2.4rem,6vw,3.4rem) auto 0;text-align:center}
.at-sp::before{content:"";display:block;width:38px;height:1px;margin:0 auto 1.3rem;background:var(--gold)}
.at-sp-l{font-family:var(--font-ui);font-size:10px;font-weight:600;letter-spacing:.2em;text-transform:uppercase;color:var(--olive)}
/* a little smaller than the guests' pair: a note for a few, not the main event */
.at-view-sp{--fw:min(250px,calc(28vw + 44px));margin-top:.9rem}
.at-sp-c{display:inline-flex;align-items:center;gap:.6rem;margin-top:.9rem;font-family:var(--font-display);font-style:italic;
  font-size:clamp(1.2rem,2.6vw,1.45rem);color:var(--ink)}
.at-sp-dot{flex:none;width:22px;height:22px;border-radius:50%;background:#56331f;box-shadow:inset 0 0 0 1px rgba(36,28,23,.2)}
.at .at-say{width:fit-content;max-width:min(100%,30rem);
  margin:clamp(2rem,4.4vw,2.6rem) auto 0;text-align:left}

/* ============================================================
   FAQ - Jackson answers, one exchange per card.

   The cards are the reference's tilted paper, the chat inside is ours. Each
   card is a single question and a single answer, so it reads at a glance
   the way a list does, and the voice is still the dog's.

   THE TILT IS `rotate`, NOT `transform`. The scroll reveal animates transform
   and fills `both`, so a rotate() in transform would be overwritten by its
   last keyframe and every card would land dead straight. The individual
   property composes with it instead. Per card, inline, as --r.

   COLUMNS, NOT A GRID. Cards differ in height by a line or three, and a grid
   would leave a hole beside every short one. Columns fill down, so the order
   runs down the left then the right, which the numbering makes plain.
   ============================================================ */
.fq{margin-top:clamp(2.4rem,5vw,3.6rem);columns:2;column-gap:clamp(1rem,2.6vw,2rem);text-align:left}

.fq-card{break-inside:avoid;position:relative;margin:0 0 clamp(1.1rem,2.6vw,2rem);
  padding:clamp(1.2rem,2.4vw,1.7rem);
  background:var(--paper);border:1px solid var(--rule-ink);border-radius:3px;
  box-shadow:0 1px 2px rgba(36,28,23,.06),0 12px 26px -14px rgba(36,28,23,.22);
  rotate:var(--r,0deg);
  transition:rotate .5s cubic-bezier(.16,1,.3,1),translate .5s cubic-bezier(.16,1,.3,1),box-shadow .5s ease}
.fq-card:hover{rotate:0deg;translate:0 -4px;
  box-shadow:0 1px 2px rgba(36,28,23,.06),0 22px 40px -16px rgba(36,28,23,.28)}

/* gold-deep, not gold: this is type on paper, and gold is ornament only here */
.fq-no{display:flex;align-items:center;gap:.7em;margin-bottom:.9rem;font-family:var(--font-mono);
  font-size:12px;letter-spacing:.06em;color:var(--gold-deep)}
.fq-no::after{content:"";width:34px;height:1px;background:var(--gold);opacity:.7}

/* the guest, on the right */
.fq-q{display:block;width:fit-content;max-width:86%;margin:0 0 0 auto;padding:.65rem 1rem;
  background:var(--cream);border:1px solid var(--rule-ink);border-radius:16px 16px 4px 16px;
  font-family:var(--font-ui);font-size:15px;font-weight:400;line-height:1.45;color:var(--ink)}
/* the read receipt is a paw */
.fq-seen{display:block;width:11px;height:10px;margin:.3rem .15rem 0 auto;fill:var(--olive);opacity:.45}

/* Jackson, on the left. Paper on olive-deep is about 9:1. */
.fq-a{display:flex;align-items:flex-end;gap:.5rem;max-width:92%;margin-top:.25rem}
.fq-av{flex:none;width:36px;height:36px;object-fit:contain;
  filter:drop-shadow(0 1px 1px rgba(36,28,23,.22)) drop-shadow(0 4px 8px rgba(36,28,23,.18))}
.fq-b{padding:.7rem 1rem;background:var(--olive-deep);color:var(--paper);
  border-radius:16px 16px 16px 4px;font-family:var(--font-text);font-size:15px;line-height:1.6}
.fq-b a{color:#fff;text-decoration-color:rgba(255,255,255,.5);text-underline-offset:3px}
.fq-b a:hover,.fq-b a:focus-visible{text-decoration-color:#fff}
/* the route is a place to go, so it reads as one: its own line, a pin */
.fq-map{display:inline-block;margin-top:.35rem}
/* an answer with more than one paragraph (where to stay): one bubble still,
   the couple's list of places inside it, each a link to the map */
.fq-b p{margin:0}
.fq-b p + *,.fq-list + p{margin-top:.55rem}
.fq-list{margin:.55rem 0 0;padding:0 0 0 1.1rem}
.fq-list li{padding:.1rem 0}
.fq-list li::marker{color:var(--gold)}
.nw{white-space:nowrap}
/* THE GIFT CODES arrive the way a photo does in a chat: two pictures inside
   Jackson's reply. Nobody can scan a code on the screen they are holding, so
   each opens large in the photobook's lightbox, where it can be saved and
   then opened from a banking app. */
.fq-pics{display:grid;grid-template-columns:1fr 1fr;gap:.6rem;margin-top:.8rem}
.fq-pic{display:block;padding:0;border:0;border-radius:8px;overflow:hidden;background:#fff;cursor:zoom-in;
  box-shadow:0 1px 3px rgba(0,0,0,.2)}
.fq-pic img{display:block;width:100%;height:auto}
.fq-pic:focus-visible{outline:2px solid #fff;outline-offset:2px}
.fq-pics-hint{display:block;margin-top:.55rem;font-size:.82em;opacity:.78}

/* ---- the sign-off ----
   The last card in the columns, where it evens them out and still closes
   the list. Its number becomes a label with a rule either side. */
.fq-end .fq-no{justify-content:center}
.fq-end .fq-no::before{content:"";width:34px;height:1px;background:var(--gold);opacity:.7}

/* ---- Jackson types ----
   Every card, once, for as long as the answer is long. site.js inserts the
   paws and sets .is-typing, so without JS, and under reduced motion, the
   answers are simply there.

   THE ANSWER KEEPS ITS SPACE. A card that grew from a small typing bubble to
   its full answer would push everything under it down, and in columns it
   would reshuffle cards from one column to the other mid-scroll. So the
   answer is laid out from the start at opacity 0 and the paws sit over its
   bottom-left corner; the card is never a different size. Opacity rather
   than visibility, so a screen reader still has the answer while he types. */
.fq-a{position:relative}
.fq-typing{position:absolute;left:calc(36px + .5rem);bottom:0;display:none;gap:5px;
  padding:.85rem 1rem;background:var(--olive-deep);border-radius:16px 16px 16px 4px}
.fq-card.is-typing .fq-typing{display:flex}
.fq-card.is-typing .fq-b{opacity:0}
.fq-typing svg{width:10px;height:9px;fill:var(--paper);opacity:.45;
  animation:fqDot 1.2s ease-in-out infinite}
.fq-typing svg:nth-child(2){animation-delay:.15s}
.fq-typing svg:nth-child(3){animation-delay:.3s}
@keyframes fqDot{0%,60%,100%{transform:none;opacity:.45}30%{transform:translateY(-3px);opacity:1}}
.fq-card.is-said .fq-b{animation:fqSay .45s cubic-bezier(.16,1,.3,1) both}
@keyframes fqSay{from{opacity:0;transform:translateY(6px) scale(.97)}to{opacity:1;transform:none}}

@media (max-width:640px){
  .fq{columns:1}
  .fq-q{max-width:92%}
  .fq-a{max-width:100%}
}

/* ============================================================
   HERO - PORTRAIT
   Switching on ASPECT, not width, for the same reason the gate does: what
   decides whether two columns fit is the slack beside them, and a 1024x768
   desktop has it while a 768x1024 tablet does not.

   THERE IS NO SECOND COLUMN HERE, so the stage has nowhere to slide to. It
   stays put, and the sequence collapses to what still works vertically: the
   pack walks in, says its line, and the couple fade in behind them.

   THE DOGS GET BIGGER, NOT SMALLER. 7.8% of a phone is 30px of Shar Pei. The
   lesson from the gate's wings applies unchanged - when there is no room,
   CROP, never shrink. They come forward as an overlapping huddle and let the
   bottom edge cut them, exactly as they do in the gate.
   ============================================================ */
@media (max-aspect-ratio:5/4){
  .hero{--stage-x:50%;--pack-lead:32vw;--lead-drop:30vh}

  /* nowhere to slide to, so it does not slide */
  .hero-stage{animation:none;transform:none}

  .hero-col{width:min(90vw,34ch);margin-top:calc(var(--pack-h) / -2 - 4px)}

  /* full bleed behind the column rather than held to one half */
  .hero-photo{-webkit-mask-image:radial-gradient(128% 104% at 50% 22%, #000 0%, #000 36%, transparent 78%);
                      mask-image:radial-gradient(128% 104% at 50% 22%, #000 0%, #000 36%, transparent 78%)}

  /* THE SIDE-SHADE COMES OFF. Landscape's first veil layer is a 90deg ramp
     that darkens the right half, because that is where the text column sits
     and it has to stay legible over the photo. Portrait has no second column - the copy is centred over the full width - so the ramp shades nothing
     but Ian, who happens to stand on the right: his side measured luminance
     28 against Merce's 75. Symmetric here, and the contrast the copy needs is
     taken from the horizontal band instead, which costs both of them the same. */
  .hero-veil{background:
    radial-gradient(120% 96% at 50% 30%, rgba(28,21,15,0) 0%, rgba(24,18,13,.3) 54%,
                                          rgba(16,11,8,.88) 100%),
    linear-gradient(to bottom, rgba(20,15,11,.58) 0%, rgba(20,15,11,.30) 18%,
                               rgba(20,15,11,.40) 46%, rgba(20,15,11,.26) 68%,
                               rgba(12,9,6,.94) 100%)}
  /* darker than landscape: with no second column the text sits ON them
     rather than beside them, and the gold venue line was disappearing into
     her face */
  /* NO TRANSLATE HERE. The landscape rule scales up first to manufacture
     horizontal slack, because there `cover` scales the 1.30:1 source by WIDTH
     and there is none. Portrait is the opposite case - `cover` scales by
     HEIGHT, so the photo already overhangs by about 700px and the inherited
     -13% was simply carrying Merce off the left edge. Centred, the window is
     the middle ~36% of the frame, which holds her face whole and all of Ian
     but the far edge of his brim. */
  .hero-photo::before{background-position:50% 50%;transform:none;
    filter:grayscale(.5) contrast(1.06) brightness(.56) saturate(1.2)}

  .hero-names{font-size:clamp(3.2rem,16vw,5rem);margin-top:.6rem;--names-track:.08em}
  .hero-lead{font-size:clamp(1rem,4.4vw,1.2rem)}
  .hero-rest{font-size:clamp(.88rem,3.6vw,1rem);line-height:1.55}
  .hero-where{letter-spacing:.18em;line-height:1.7}

  /* Portrait widths are a RATIO OF PERCY, not absolute percentages. They were
     absolute, and --pack-lead was documented as mirroring his width while
     being a separate number - so shrinking the pack on a short phone moved
     everything that clears it without moving the dogs, and the scroll cue
     landed on their heads. One source of truth now. */
  .hero-dog{left:var(--xp,var(--x));width:calc(var(--pack-lead) * var(--wr,1))}
}

/* A very short phone - an SE-sized 568px - cannot hold the column AND a pack
   sized for a modern screen, and the scroll cue ends up on their heads. The
   pack gives way rather than the invitation: everything that clears it is
   derived from --pack-lead, so one number does it. */
/* PHONES KEEP THE WORDS ABOVE THE DOGS' NAMES, the laptop rule turned on
   its side. The column centres over the pack where that clears their names,
   and otherwise its foot stops just above "I'm Percy" and it rises into the
   room above. The names sit about 1.2 pack-heights up from the bottom of the
   screen (100svh, the height with the browser's bars showing). --col-h is
   measured by site.js, so a paragraph that wraps to four lines on one phone
   and three on another is covered either way. */
@media (max-aspect-ratio:5/4){
  .hero-col{--clear:calc(var(--pack-h) * 1.22 + 16px);
    top:max(calc(var(--col-h, 0px) / 2 + 26px),
            min(calc(50% + var(--pack-h) / -2 - 4px),
                calc(100svh - clamp(18px, 3vh, 34px) - var(--clear) - var(--col-h, 0px) / 2)));
    margin-top:0}
}
/* a phone that is short for its width (most of them, once Safari's bars are
   up) closes the gaps between the lines rather than letting the words and the
   dogs meet */
@media (max-aspect-ratio:5/4) and (max-height:780px){
  .hero .hero-count{margin-top:.9rem}
  .hero .hero-act{margin-top:1.05rem}
  .hero .hero-act .btn{padding:12px 30px}
  .hero .hero-rest{margin-top:.8rem;line-height:1.5}
  .hero .hero-where{margin-top:.55rem}
}

@media (max-aspect-ratio:5/4) and (max-height:640px){
  .hero{--pack-lead:20vw;--lead-drop:22vh}
}

/* the very shortest landscape windows (600 to 630 tall): the pack gives a
   little more, and the column can come back down off the top edge */
@media (min-aspect-ratio:5/4) and (max-height:630px){
  .hero{--pack-s:.76}
}

/* ---- footer ---- */
/* a sign-off, not a section: about half a section's breathing room */
.foot{padding:clamp(2.2rem,5vw,3.6rem) 0 clamp(1.8rem,4vw,2.8rem);text-align:center;
  border-top:1px solid rgba(216,198,168,0.1)}
/* the nav's names, on dark: the serif in parchment, the script ampersand in
   gold, and the hero's venue line under it, one row of it where it fits */
.foot-mono{height:clamp(72px,9vw,96px);margin:0 auto 1rem;color:var(--gold)}
.foot-mark{font-family:var(--font-display);font-size:1.6rem;letter-spacing:.01em;color:var(--tan-soft)}
.foot-mark .amp{font-size:1.35em;color:var(--gold);padding:0 .14em 0 .02em}
.foot-date span{opacity:.6;margin:0 .3em}
.foot-date .fd-bar{margin:0 .75em;opacity:.45}
.foot-date{margin-top:1.1rem;font-family:var(--font-ui);font-size:10.5px;letter-spacing:.24em;
  text-transform:uppercase;color:var(--tan-faint)}

/* ---- reduced motion: this file's selectors only ---- */
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  .nav,.nav-links,.nav-toggle span{transition:none}
  .rail,.rail-fill,.rail-n,.rail-l{transition:none}
  /* everything is already where it ends up. The transforms that survive are
     the centring, not the motion. */
  .hero-dog img{animation:none;opacity:1;transform:none}
  .hero-dog{translate:none;transition:none}
  .hero-halo{animation:none;opacity:1;transform:translateX(-50%)}
  .hero-stage{animation:none;transform:none}
  .hero-photo{animation:none;opacity:1;transform:none}
  .hero-content > *,.hero-lead,.hero-names{animation:none;opacity:1;transform:none;
    filter:none;letter-spacing:.02em;margin-right:0}
  /* The sparkles on the names loop forever and were the one animated class
     that never got added here by hand. Held lit rather than hidden, so the
     names keep their catchlights without anything moving. */
  .hero-glints i{animation:none;opacity:.55;transform:scale(1)}
  .hero-col{transform:translate(-50%,-50%)}
  /* The light half animates only on hover, so there is nothing to stop but the
     one transition. The scroll-linked reveal is NOT listed here on purpose:
     it sits inside a `prefers-reduced-motion: no-preference` query further up,
     so it is opt-in and never needs switching off. */
  .wh-map{transition:none}
  /* the cards keep their tilt and stop lifting; site.js never starts the
     typing, but the dots are listed in case the class is ever set by hand */
  .fq-card{transition:none}
  /* the looks change only when a chip is tapped, and change at once */
  .at-img,.at-couple,.at-chip span,.at-dot::before,.at-dot-n{transition:none}
  .st-print,.st-print.is-on,.st-print.was-on,.st-line,.st-line.is-on,.st-line .st-av,.st-line.is-on .st-av,.st-moment{transition:none}
  .fq-typing svg,.fq-card.is-said .fq-b{animation:none}
  }

/* ============================================================
   AMBIENT - dust and drifting botanicals, over the light half only.

   FIVE NESTED BOXES PER PIECE. CSS gives an element one `translate` and one
   `transform`, and this needs five things composed independently:

     .bo     translate Y   the fall, on a timer, so the page is alive at rest
     .bo-s   translate Y   the fall again, on the scroll, so it answers the reader
     .bo-g   translate XY  gusts
     .bo-d   transform     the pendulum swing, hung from far above
     svg     rotate + opacity   the turn-over

   The first attempt was one vertical translate and one sine on X. That is a
   metronome, not a leaf. What sells it is the turn-over: each piece rotates
   about its own vertical axis and goes edge-on twice a revolution.

   THE FLIP IS NOT LINEAR. Spun at a constant rate a leaf spends as long
   edge-on as face-on, so a third of them were invisible slivers at any moment,
   and at the crossing the round caps on near-zero-width geometry break into
   dashes. The faces are held, the edges crossed at twice the speed, and the
   opacity dips through the crossing so the artifact never shows.

   Depth is carried by size, opacity, fall speed and blur, in three bands, so
   they do not read as one sheet sliding past.
   ============================================================ */
.bo-layer{position:fixed;inset:0;pointer-events:none;z-index:var(--z-ambient);
  overflow:hidden;opacity:0;transition:opacity 1.2s ease}
.bo-layer.is-on{opacity:1}
/* HUSHED OVER THE PHOTO BOOK. The layer is fixed over everything, so while
   the book is on screen every page turn was being composited under twenty-six
   drifting leaves and a field of dust: measured, it more than halved the frame
   rate of a turn. Nobody wants leaves across a photograph they are reading
   anyway. It fades out, then goes to visibility:hidden once the fade is done,
   because opacity:0 alone still has the browser drawing all of it. */
.bo-layer.is-on.is-hushed{opacity:0;visibility:hidden;
  transition:opacity .6s ease,visibility 0s .6s}

/* dust: soft and out of focus, so it reads as air. Drawn as hard dots first,
   which on cream read as dirt on the screen. */
.bo-mote{position:absolute;left:var(--x);top:var(--y);width:var(--sz);height:var(--sz);
  border-radius:50%;opacity:var(--o);
  background:radial-gradient(circle at 50% 50%,var(--olive) 0%,transparent 68%)}

.bo{position:absolute;left:var(--x);top:0;width:var(--sz);height:var(--sz);
  opacity:var(--o);color:var(--olive);filter:blur(var(--bl))}
.bo-s,.bo-g,.bo-d{display:block;width:100%;height:100%}
.bo svg{display:block;width:100%;height:100%;fill:none;stroke:currentColor;
  stroke-width:.85;stroke-linecap:round;stroke-linejoin:round}

/* a leaf a sigil has just let go of, on its way down past the names */
.bo-shed{position:fixed;width:15px;height:15px;color:var(--olive);opacity:0;
  pointer-events:none;z-index:calc(var(--z-ambient) + 1)}
.bo-shed svg{display:block;width:100%;height:100%;fill:none;stroke:currentColor;
  stroke-width:.85;stroke-linecap:round;stroke-linejoin:round}

@media (prefers-reduced-motion:no-preference){
  .bo-mote{animation:boDrift var(--t) ease-in-out infinite alternate var(--td)}
  .bo{animation:boFall var(--fall) linear infinite var(--fd)}
  .bo-s{animation:boFallS 1s linear var(--n);animation-timeline:scroll(root block)}
  .bo-g{animation:boGust var(--gt) ease-in-out infinite var(--gd)}
  .bo-d{transform-origin:50% -320%;
    animation:boArc var(--dt) cubic-bezier(.36,0,.64,1) infinite alternate var(--dd)}
  .bo svg{animation:boFlip calc(var(--flip) * 1.7) linear infinite var(--fld)}
}

@keyframes boDrift{to{translate:var(--mx) var(--my)}}
@keyframes boFall {from{translate:0 -16vh}to{translate:0 116vh}}
@keyframes boFallS{from{translate:0 -8vh} to{translate:0 108vh}}
@keyframes boArc  {from{transform:rotate(calc(var(--sw) * -1))}
                   to  {transform:rotate(var(--sw))}}
/* quiet, then wind, then quiet. The delay is set from each piece's own
   horizontal position, so the gust visibly travels across the screen. */
@keyframes boGust{
  0%,54%{translate:0 0}
  62%   {translate:calc(var(--dx) * .5) 5px}
  74%   {translate:calc(var(--dx) * 1.5) 16px}
  86%   {translate:calc(var(--dx) * 1.1) 10px}
  100%  {translate:0 0}}
@keyframes boFlip{
  0%  {rotate:y 0deg;   opacity:1}
  22% {rotate:y 74deg;  opacity:.34}
  26% {rotate:y 106deg; opacity:.34}
  47% {rotate:y 180deg; opacity:1}
  53% {rotate:y 180deg; opacity:1}
  72% {rotate:y 254deg; opacity:.34}
  76% {rotate:y 286deg; opacity:.34}
  97% {rotate:y 360deg; opacity:1}
  100%{rotate:y 360deg; opacity:1}}

/* ---- the gestures ----------------------------------------------------
   Now and then a glyph does the thing it depicts. Every one of them fires
   the first time it comes into view, because nobody waits out a timer.

   WHAT LIMITS THE VOCABULARY: each glyph is a <use> of a sprite symbol, and
   nothing reaches into a <use> shadow tree, so there is no selecting the
   flame apart from the candle. Only inherited properties cross: `color`,
   `fill-opacity`, `stroke-dashoffset`. Everything else is whole-glyph
   motion. js/ambient.js holds the gestures themselves. */
.ent-gl{fill-opacity:0;backface-visibility:visible}
.ent-sub{perspective:340px}
/* hung rather than sat, so they pivot at the top */
.ent-gl.is-hung{transform-origin:50% 14%}

/* ON A PHONE, HALF THE WEATHER. Twenty-six leaves and eleven motes, each with
   up to five animations of its own, were the one thing still moving all the
   way down the page, and a phone felt it. A phone keeps the eight large, near
   leaves, four of the middle ones spread across the width, and five motes;
   the small far leaves go, which were blurred and faint enough at this size to
   read as smudges anyway. What stays is a touch stronger, so the page still
   reads as weathered rather than bare. Positions are the markup order: motes
   are children 1-11, near leaves 12-19, middle 20-28, far 29-37. */
@media (max-width:640px){
  #ambient > :nth-child(2),#ambient > :nth-child(4),#ambient > :nth-child(5),
  #ambient > :nth-child(7),#ambient > :nth-child(9),#ambient > :nth-child(10),
  #ambient > :nth-child(20),#ambient > :nth-child(21),#ambient > :nth-child(24),
  #ambient > :nth-child(25),#ambient > :nth-child(26),#ambient > :nth-child(n+29){display:none}
  #ambient .bo{opacity:calc(var(--o) * 1.25)}
  #ambient .bo-mote{opacity:calc(var(--o) * 1.2)}
}
@media (prefers-reduced-motion:reduce){
  .bo-layer,.bo-shed{display:none}
}
