/* Abdelrahman & Rana — 1 October 2026
   Layout B: full-width reflow on desktop, frame capped at its native 1440px. */

@font-face {
  font-family: 'Cormorant Garamond';
  src: url('../fonts/cormorant-garamond.woff2') format('woff2');
  font-weight: 300 700;
  font-display: swap;
}
@font-face {
  font-family: 'Revive 80';
  src: url('../fonts/revive80.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}

:root {
  --cream: #FAF8F0;
  --bronze: #AB7B49;
  --bronze-dark: #96683A;
  --gold: #BA946B;
  --gold-soft: #C9AC8A;
  --rule: #D8C6AB;

  --frame-max: 1440px;
  --base: clamp(15px, 3.95vw, 17.5px);

  /* Sampled from the rose clusters in border roses.png — a dusty mauve, not
     the pink you would reach for by instinct. The drifting petals use it so
     they read as having come off the border art. */
  --petal: #A05A64;
  --petal-light: #C79BA2;

  /* Shared by the arch's resting state and its fade-in keyframe, so the two
     cannot drift apart. */
  --arch-opacity: 0.28;

  /* Border geometry, measured from the source art: the side vines occupy
     140 of the original 1440px width. Percentages keep them in proportion. */
  --vine-w: 9.722%;
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--cream);
  color: var(--gold);
  font-family: 'Cormorant Garamond', Garamond, 'Times New Roman', serif;
  font-weight: 400;
  line-height: 1.65;
  display: flex;
  justify-content: center;
}

img { display: block; max-width: 100%; height: auto; }

/* <picture> is inline by default, so layout classes on it are ignored.
   display:contents removes its box entirely and lets the inner <img>
   participate in layout directly, which is where every class here lives.
   That also exposes <source> to the parent, and source computes to block —
   inside a grid or flex container each one would claim its own cell, so it
   must be explicitly removed. Format selection is unaffected by display. */
picture { display: contents; }
picture > source { display: none; }

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.page {
  width: 100%;
  max-width: var(--frame-max);
  /* The arch is centred on its own peak rather than on its bounding box
     (translateX(-49.57%)), which leaves it hanging 0.43% past the right edge —
     about 5px, enough for a real horizontal scrollbar and a sideways wobble on
     a phone. Clipping it costs nothing: the part being cut is already off the
     side of the screen.

     `clip` rather than `hidden` deliberately. `hidden` would make this a
     scroll container, which changes what position:sticky measures against and
     lets stray scrollTop values strand the page sideways. `clip` just clips. */
  overflow-x: clip;
}

/* ---------- frame ----------
   Three rows: top cap at natural height, tiled middle absorbing whatever
   the content needs, bottom cap. Content spans all three and sits above.
   This is what lets the border work at any page height. */

.frame {
  position: relative;
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: auto 1fr auto;
}

.frame__cap {
  grid-column: 1;
  width: 100%;
  z-index: 2;
  pointer-events: none;
  user-select: none;
}
.frame__cap--top { grid-row: 1; align-self: start; }
.frame__cap--bottom { grid-row: 3; align-self: end; }

.frame__sides {
  grid-row: 2;
  grid-column: 1;
  z-index: 1;
  background-image: image-set(url('../img/border-tile-left.avif') type('image/avif'),
                              url('../img/border-tile-left.webp') type('image/webp')),
                    image-set(url('../img/border-tile-right.avif') type('image/avif'),
                              url('../img/border-tile-right.webp') type('image/webp'));
  background-repeat: repeat-y, repeat-y;
  background-position: left top, right top;
  background-size: var(--vine-w) auto, var(--vine-w) auto;
}

.content {
  grid-row: 1 / -1;
  grid-column: 1;
  z-index: 3;
  position: relative;
  font-size: var(--base);
  /* The bottom cap's dense corner art begins 224px up from the bottom of the
     1440px-wide source, i.e. 15.6% of the frame width. Percentage padding
     resolves against that same width, so this one value keeps the closing
     text clear of the roses at every viewport size. */
  padding: 0 15% 19%;
}

/* ---------- typography ---------- */

section { text-align: center; }

/* Curved overline. The path is drawn by initOverlineArc() in main.js, which
   sets the viewBox to the element's own pixel width so SVG units map 1:1 to CSS
   pixels — that keeps the font-size below honest instead of scaling with the
   container the way a fixed viewBox would. Arc shape measured off the
   reference: sagitta/chord = 0.08. */
.overline-arc {
  /* Hidden without JS, shown with it. It must NOT be display:none while the
     script runs — clientWidth would be 0 and the layout could never measure
     itself, so it would never reveal itself either. */
  display: none;
  --overline-span: 0.666; /* fraction of the frame the run should span */
  width: 100%;
  margin: 1.2em auto 0.4em;
  overflow: visible;
  font-size: 0.72em;
  /* Tracking is set by textLength/lengthAdjust in main.js, not here. */
  text-transform: uppercase;
  fill: var(--gold);
}

.js .overline-arc { display: block; }
.js .overline { display: none; }

/* The no-JS fallback, and what remains if the arc layout ever fails. */
.overline {
  font-size: 0.62em;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  margin: 1.2em 0 0.4em;
}

.names {
  font-size: 2.35em;
  line-height: 1.12;
  font-weight: 400;
  color: var(--bronze);
  margin: 0;
}

.amp { font-size: 1.5em; color: var(--bronze); margin: 0.42em 0; line-height: 1; }

.invite-line { font-size: 1.03em; color: var(--bronze); margin: 1em 0 0; line-height: 1.55; }

.date {
  font-size: 1.17em;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  margin: 0.9em 0 0;
  white-space: nowrap;
}
.date sup { font-size: 0.62em; letter-spacing: 0; }
.date .sep { color: var(--gold-soft); margin: 0 0.15em; }

/* Revive 80 has a small x-height and thin strokes, so it needs to run
   noticeably larger than the body face to read at the same visual weight. */
.script {
  font-family: 'Revive 80', 'Brush Script MT', cursive;
  font-size: 1.95em;
  line-height: 1;
  margin: 0;
}

.heading {
  font-size: 2.07em;
  font-weight: 400;
  color: var(--bronze);
  margin: 0 0 0.15em;
  line-height: 1.2;
}

.caps {
  font-size: 1.17em;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--bronze);
  margin: 0;
  line-height: 1.5;
}

.time { font-size: 1.1em; margin: 0.5em 0 0; }
.body-text { font-size: 1em; margin: 0; }

.spacer { width: 62%; margin: 0.7em auto; }
.spacer--heart { width: 34%; margin: 0.35em auto 0.7em; }

/* ---------- sections ---------- */

.hero { position: relative; padding-top: 3%; }
/* The width must stay explicit. This <img> carries srcset + sizes, and with
   width:auto the browser density-corrects the intrinsic size so the element
   resolves to the `sizes` value instead of anything CSS says — the chandelier
   came out 74px wide rather than 33%. An explicit width plus max-height still
   clamps proportionally: when the derived height exceeds max-height, the used
   width is recomputed from the intrinsic ratio. */
.hero__chandelier {
  width: 33%;
  height: auto;
  margin: 0 auto;
  position: relative;
  z-index: 2;
}

/* Measured off the reference: the arch spans x137–1303 of the 1440 card (81%),
   and its band reads #D9D7CA against the #FAF8F0 page. The raw asset is
   #827D6C, so it needs to sit at roughly 28% opacity to match that wash. */
/* The arch must sit BEHIND the floral border, so it cannot live inside
   .content — that has z-index 3 and position:relative, which creates a
   stacking context and forces everything inside it above the border caps at
   z-index 2. It is a direct child of .frame instead, at z-index 0:
   arch (0) < side vines (1) < border caps (2) < content (3).
   That is also what lets it run to the screen edges and tuck under the roses. */
.frame__arch {
  position: absolute;
  top: 0; left: 50%;
  /* The reference arch is taller than the raw asset: it spans y40–y1046 across
     a 1152px width (h/w 0.87), where top arch.png is 0.65. The design stretched
     it vertically by about a third, which is what elongates the scallop lobes
     and carries the legs down past the names. */
  /* The arch peak is not at the centre of its own image: it sits at 807.5 of
     1629px, i.e. 49.57%. Centring the bounding box therefore puts the peak
     ~6px left of frame centre at desktop, and the chandelier — which hangs on
     the true centre — reads as offset. Translating by the peak's own fraction
     instead of a flat 50% lands the peak dead centre at every width. */
  transform: translateX(-49.57%) scaleY(1.35);
  transform-origin: 50% 0;
  /* 116% of .hero lands the arch at 81% of the frame, matching the reference.
     max-width:none is required — the global img rule caps images at 100% of
     their container, which silently clamped this back to 100% and made the
     width value here do nothing at all. */
  width: 100%;
  max-width: none;
  opacity: var(--arch-opacity);
  z-index: 0;
  pointer-events: none;
}
.hero__text { position: relative; z-index: 3; padding-top: 4%; }

.sec { margin-top: 3.2em; }
.sec--tight { margin-top: 1.6em; }

.portraits {
  display: grid;
  grid-template-columns: 1fr 1.9fr 1fr;
  align-items: center;
  gap: 4%;
  margin-top: 2.6em;
}
/* The images sit inside <picture>, so a direct-child selector on .portraits
   would not match them — selectors walk the DOM, not the box tree. */
.portraits > picture > img { width: 100%; }
.portraits__mid { min-width: 0; }
.portraits__mid .script { font-size: 1.55em; }
.portraits__mid .spacer { width: 100%; }

/* Full-bleed to both screen edges, as in the reference. .content carries 15%
   padding each side, so its box is 70% of the frame: 100/70 = 142.857% wide,
   pulled left by 15/70 = 21.4286% to reach x=0. The vines then overlap it,
   which is exactly how the printed reference reads. */
.bleed {
  width: 142.857%;
  margin-left: -21.4286%;
  max-width: none;
}

/* ---------- no vines behind the Hilton band ----------
   Because that illustration is full-bleed on phones it covers the whole width
   of the frame, and the side vines show through its transparent watercolour
   edges — roses and olive sprigs landing on top of the palms. The printed
   reference does not do this: the border simply stops above the picture and
   picks up again below it.

   .content is at z-index 3 and the vines at z-index 1, so a cream band painted
   from in here hides them for exactly this stretch. z-index:-1 keeps it behind
   the picture while staying inside .content's own stacking context, so it is
   still above the border rather than under it.

   :has() rather than .split--reverse, so this follows the picture if the
   sections are ever reordered. It is already relied on for .attending. */
.split__media:has(.bleed) { position: relative; }

.split__media:has(.bleed)::before {
  content: '';
  position: absolute;
  z-index: -1;
  /* The same full-bleed geometry as .bleed: .content pads 15% a side, so its
     box is 70% of the frame — hence 100/70 wide, pulled back by 15/70. */
  left: -21.4286%;
  width: 142.857%;
  /* Reaches past the picture so the fade below is finished before the picture
     starts, and no vine is left half-covered against its edge. */
  top: -1.6em;
  bottom: -1.6em;
  background: var(--cream);
  /* Fades out instead of stopping dead, so a vine dissolves over ~1.4em rather
     than being sliced through the middle of a leaf. */
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 1.4em, #000 calc(100% - 1.4em), transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 1.4em, #000 calc(100% - 1.4em), transparent);
}

/* ---------- the layered entrance scene ----------
   Layers stack with inset:0 because every source layer shares one canvas.
   The first child establishes height; the rest are absolutely positioned. */

.scene {
  position: relative;
  aspect-ratio: 2896 / 2864;   /* the shared source canvas, after union crop */
}
.scene__layer { position: absolute; inset: 0; width: 100%; height: 100%; }
.scene--flat img { width: 100%; }

/* The layered scene is the default everywhere, because the trees only sway if
   they are separate elements — on a phone the flat image left them dead still.
   The flat single image survives as the reduced-motion fallback, where nine
   requests really would buy nothing; the swap is at the foot of this file.
   loading="lazy" plus display:none means the hidden set is never fetched. */
.scene { display: block; }
.scene--flat { display: none; }

/* ---------- buttons ---------- */

.actions {
  display: flex;
  gap: 0.6em;
  justify-content: center;
  flex-wrap: wrap;
  margin-top: 1.4em;
}

.btn {
  font-family: inherit;
  font-size: 0.92em;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bronze);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 0.62em 1.5em;
  cursor: pointer;
  text-indent: 0.14em;
  text-decoration: none;
  display: inline-block;
  transition: background .2s, border-color .2s, color .2s;
}
.btn:hover, .btn:focus-visible { background: var(--bronze); border-color: var(--bronze); color: var(--cream); }

/* ---------- RSVP ---------- */

.rsvp { margin: 0 auto; max-width: 34em; }

/* "kindly confirm your attendance" is far longer than the original wording, so
   the pill has to be allowed to wrap rather than run past the content edge. */
.rsvp__legend {
  font-family: 'Revive 80', cursive;
  font-size: 1.75em;
  color: var(--gold);
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 0.4em 1.1em 0.5em;
  display: inline-block;
  max-width: 100%;
  line-height: 1.25;
  margin-bottom: 1.5em;
}

.field { margin-bottom: 0.9em; text-align: left; }

.field > label, .field-label {
  display: block;
  font-size: 0.78em;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold-soft);
  margin-bottom: 0.3em;
}

.name-row { display: flex; gap: 0.5em; align-items: center; margin-bottom: 0.5em; }

input[type="text"] {
  flex: 1;
  min-width: 0;
  font-family: inherit;
  font-size: 1.05em;
  color: var(--bronze);
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--rule);
  padding: 0.35em 0.1em;
  outline: none;
}
input[type="text"]:focus { border-bottom-color: var(--bronze); }
input[type="text"]::placeholder { color: #DCCDB6; }

.btn-remove {
  background: none; border: none;
  color: var(--gold-soft);
  font-family: inherit; font-size: 1.3em; line-height: 1;
  cursor: pointer; padding: 0 0.3em;
}
.btn-remove:hover { color: var(--bronze); }

.btn-add {
  background: none; border: none;
  font-family: inherit; font-size: 0.92em;
  color: var(--gold);
  cursor: pointer; padding: 0.4em 0;
}
.btn-add:hover { color: var(--bronze); }

.attending { display: flex; gap: 0.6em; margin: 1.2em 0; }
.attending label {
  flex: 1;
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 0.5em 0;
  font-size: 0.86em;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--gold);
  cursor: pointer;
  text-align: center;
  text-indent: 0.1em;
  transition: background .2s, color .2s, border-color .2s;
}
.attending input { position: absolute; opacity: 0; pointer-events: none; }
.attending label:has(input:checked) { background: var(--bronze); border-color: var(--bronze); color: var(--cream); }
.attending label:focus-within { border-color: var(--bronze); }

.btn-submit {
  width: 100%;
  font-family: inherit;
  font-size: 1.05em;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--cream);
  background: var(--bronze);
  border: none;
  border-radius: 999px;
  padding: 0.75em 0;
  cursor: pointer;
  text-indent: 0.2em;
  margin-top: 0.4em;
}
.btn-submit:hover { background: var(--bronze-dark); }
.btn-submit[disabled] { opacity: .55; cursor: default; }

.hp { position: absolute; left: -9999px; }

.form-msg { margin-top: 1em; font-size: 0.95em; min-height: 1.6em; }
.form-msg[data-state="error"] { color: #9B4A3A; }

.footer-note { margin: 2.4em 0 0; }

/* ---------- COUNTDOWN START ----------
   Self-contained. Commenting out the HTML block removes it entirely:
   no shared rules, no layout dependency, and initCountdown() no-ops. */

#countdown { margin-top: 2.4em; }
#countdown .cd-grid { display: flex; justify-content: center; gap: 6%; }
#countdown .cd-unit { min-width: 3.4em; }
#countdown .cd-num {
  font-size: 1.9em;
  color: var(--bronze);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
#countdown .cd-label {
  font-size: 0.66em;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold-soft);
  margin-top: 0.4em;
}

/* ---------- COUNTDOWN END ---------- */

/* ---------- desktop reflow ---------- */

@media (min-width: 900px) {
  :root { --base: clamp(19px, 1.62vw, 23.5px); }

  .content { padding: 0 11% 19%; }

  .hero { padding-top: 1.5%; }
  /* .hero sits inside .content's 11% padding, so it is 78% of the frame.
     104% of that lands the arch at ~81% of the frame, matching the reference. */
  /* On a wide screen the width rule alone made the chandelier 517px tall —
     55% of a 940px viewport — which is most of why the fold fell above the
     baby photos. svh is the toolbars-visible height, i.e. the first-open case. */
  /* On a wide screen the width rule alone made the chandelier 517px tall — 55%
     of a 940px viewport — which is most of why the fold landed above the baby
     photos. Capping max-height instead squashes the image, because the browser
     clamps the height without recomputing the width. So cap the WIDTH by
     viewport height, converted through the intrinsic ratio (446/641 = 0.696):
     30svh of height is 20.9svh of width. Height then follows undistorted.
     svh is the toolbars-visible height, i.e. exactly the first-open case. */
  .hero__chandelier { width: min(32%, 20.9svh); }
  .frame__arch { width: 100%; }
  .hero__text { padding-top: 1%; }
  .names { font-size: 3.05em; }
  .overline { letter-spacing: 0.46em; }
  .overline, .overline-arc { font-size: 0.86em; }
  .overline-arc { --overline-span: 0.60; margin-top: 4.5em; }
  .overline { margin-top: 4.5em; }
  .invite-line { font-size: 1.06em; }
  .date { font-size: 1.24em; }
  .heading { font-size: 2.3em; }

  #countdown { margin-top: 2em; }
  #countdown .cd-num { font-size: 2.2em; }
  #countdown .cd-grid { gap: 4.5%; }

  .split {
    display: grid;
    grid-template-columns: 1.15fr 1fr;
    align-items: center;
    gap: 5%;
    text-align: left;
    margin-top: 4.5em;
  }
  .split--reverse { grid-template-columns: 1fr 1.15fr; }
  .split--reverse .split__media { order: 2; }
  .split--reverse .split__text { order: 1; text-align: right; }

  .split .spacer { margin-left: 0; }
  .split--reverse .spacer { margin-left: auto; margin-right: 0; }
  .split .actions { justify-content: flex-start; }
  .split--reverse .actions { justify-content: flex-end; }

  /* Here the picture sits inside its own grid column, nowhere near the vines,
     so there is nothing to hide — and the full-bleed geometry above would not
     line up with the frame edges from inside a column anyway. */
  .bleed { width: 100%; margin-left: 0; }
  .split__media:has(.bleed)::before { display: none; }

  .portraits { max-width: 56%; margin: 3.4em auto 0; }

  /* Both buttons have to share the narrower text column of the split. */
  .btn { font-size: 0.84em; letter-spacing: 0.1em; padding: 0.6em 1.2em; }

  .rsvp { max-width: 30em; }
  .rsvp__legend { font-size: 2em; padding: 0.4em 1.4em 0.5em; }
  .footer-note { margin-top: 3em; }
}

/* Above the frame's native width the art would have to upscale, so the page
   stops growing and sits centred on cream. That is handled entirely by
   --frame-max on .page — no padding override belongs here, and an earlier one
   silently reset the bottom padding that keeps the closing text off the roses. */

/* ================= MOTION =================
   Everything here animates transform or opacity only — the two properties the
   compositor can handle on its own thread, without laying out or repainting.
   Anything else (width, top, background-position, filter) would drop an old
   phone to single-digit frame rates while it scrolls.

   Two separate off switches:
     · prefers-reduced-motion  — the block at the very bottom of this file
     · no JavaScript           — nothing here is required for the page to read;
                                 the reveal and the petals are added by script,
                                 so without it everything is simply present. */

/* ---------- hero entrance ----------
   Runs on load. Kept under 1.1s end to end: the names are a candidate for the
   Largest Contentful Paint, and an element still fading up is not counted as
   painted, so a long entrance would show up directly as a worse LCP score. */

@keyframes rise-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

/* Fades only. The arch carries a permanent translateX/scaleY that positions it
   against the frame — animating transform here would wipe that out and throw
   the arch across the page. */
@keyframes arch-in {
  from { opacity: 0; }
  to   { opacity: var(--arch-opacity); }
}

@keyframes chandelier-in {
  from { opacity: 0; transform: translateY(-16px); }
  to   { opacity: 1; transform: none; }
}

/* The candles catching. Very shallow — at full strength it reads as a flicker
   or a fault, not as light. */
@keyframes shimmer {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.87; }
}

.js .hero__chandelier {
  /* Two animations, and the order matters: where both are live, the later one
     in the list wins the property. chandelier-in holds its end state with
     `both`, and shimmer has no fill, so before shimmer's delay elapses the
     entrance wins and afterwards the shimmer does. That hand-off is why this
     needs no JS and no wrapper element. */
  animation:
    chandelier-in 1s cubic-bezier(.22,.61,.36,1) both,
    shimmer 8s ease-in-out 1.2s infinite;
}

.js .frame__arch { animation: arch-in 1.6s ease-out both; }

.js .hero__text > * { animation: rise-in .7s cubic-bezier(.22,.61,.36,1) both; }
.js .hero__text > :nth-child(2) { animation-delay: .06s; }
.js .hero__text > :nth-child(3) { animation-delay: .12s; }
.js .hero__text > :nth-child(4) { animation-delay: .18s; }
.js .hero__text > :nth-child(5) { animation-delay: .24s; }
.js .hero__text > :nth-child(6) { animation-delay: .30s; }
.js .hero__text > :nth-child(7) { animation-delay: .36s; }

/* ---------- scroll reveal ----------
   The hiding rule is gated on .reveal-ready, which main.js adds to <html> only
   once it knows an IntersectionObserver exists to take the class back off
   again. Hiding these in plain CSS would leave the whole page blank on any
   browser that cannot observe them. */

.reveal-ready [data-reveal] {
  opacity: 0;
  transform: translateY(20px);
}

.reveal-ready [data-reveal].is-in {
  opacity: 1;
  transform: none;
  transition: opacity .85s cubic-bezier(.22,.61,.36,1),
              transform .85s cubic-bezier(.22,.61,.36,1);
}

/* In a two-column split the picture leads and the words follow, rather than
   both arriving at once. */
.reveal-ready .split__text[data-reveal].is-in { transition-delay: .12s; }

/* ---------- drifting petals ----------
   Built by main.js and appended to <body>, so they are absent under
   reduced-motion and without JS instead of being hidden after the fact.

   position:fixed keeps them falling for the whole scroll rather than only
   across one section. Nesting fall inside drift gives each petal two
   independent periods, which is what stops eight of them looking like one
   repeating loop. */

.petals {
  position: fixed;
  inset: 0;
  z-index: 5;
  overflow: hidden;
  pointer-events: none;   /* never intercepts a tap on the form beneath */
}

.petal {
  position: absolute;
  top: 0;
  will-change: transform;
  animation: petal-fall linear infinite;
}

.petal i {
  display: block;
  width: 100%;
  height: 100%;
  /* Asymmetric radii give the teardrop of a real petal; a plain ellipse reads
     as confetti. */
  border-radius: 100% 0 65% 65%;
  background: linear-gradient(150deg, var(--petal-light), var(--petal));
  opacity: 0.45;
  will-change: transform;
  animation: petal-drift ease-in-out infinite alternate;
}

@keyframes petal-fall {
  from { transform: translate3d(0, -12vh, 0); }
  to   { transform: translate3d(0, 112vh, 0); }
}

@keyframes petal-drift {
  from { transform: translateX(-14px) rotate(-40deg); }
  to   { transform: translateX(14px) rotate(40deg); }
}

/* ---------- the trees ----------
   Each layer is one tree alone on a transparent copy of the full 2896×2864
   canvas, so rotating the layer rotates the tree — but only if it turns about
   that tree's own trunk.

   A single transform-origin of 50% 100% was wrong for all six. No tree stands
   at the centre: the palms and outer olives sit at 6%, 13%, 88% and 93% of the
   width. Turning about a point up to 1275px to one side converts most of the
   rotation into a vertical bob, which is why the movement read as almost
   nothing however large the angle got. The origins below are measured from
   each layer's own alpha channel — horizontal centre of the lowest 8% of its
   visible pixels (the trunk) and the bottom of its bounding box (the ground).

   Amplitude is per tree rather than shared. The angle needed for a given
   amount of crown movement depends on how far the crown is above the pivot,
   and that ranges from 586px on the small olives to 1934px on the left palm.
   Smaller trees also genuinely move more in a breeze, so they get more still.
   Each keyframe runs from -angle to +angle, i.e. the full swing is twice the
   number below. */

@keyframes sway {
  from { transform: rotate(calc(var(--sway) * -1)); }
  to   { transform: rotate(var(--sway)); }
}

.js .scene__layer[data-sway] {
  animation: sway ease-in-out infinite alternate;
  /* Held still until the scene is actually on screen. The guest spends most of
     the page far away from it, and six continuously animating composited
     layers are not free on an old phone. main.js adds .scene--live. */
  animation-play-state: paused;
}

.js .scene--live .scene__layer[data-sway] { animation-play-state: running; }

/* Deliberately mismatched periods — none a whole multiple of another — so the
   six never fall into step and start reading as one mechanical loop. */
.js .scene__layer[data-sway="palm-left"] {
  transform-origin: 6.1% 91.1%;  --sway: 1.5deg;  animation-duration: 7.3s;
}
.js .scene__layer[data-sway="palm-right"] {
  transform-origin: 92.9% 90%;   --sway: -1.5deg; animation-duration: 8.9s;
}
.js .scene__layer[data-sway="olive-1"] {
  transform-origin: 12.7% 91.7%; --sway: 2.4deg;  animation-duration: 6.7s;
}
.js .scene__layer[data-sway="olive-4"] {
  transform-origin: 87.7% 92%;   --sway: -2.4deg; animation-duration: 9.7s;
}
.js .scene__layer[data-sway="olive-2"] {
  transform-origin: 32.5% 79.1%; --sway: 3.2deg;  animation-duration: 8.3s;
}
.js .scene__layer[data-sway="olive-3"] {
  transform-origin: 68.1% 79%;   --sway: -3.2deg; animation-duration: 7.9s;
}

/* ================= END MOTION ================= */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    /* The delays have to go too, and this is easy to miss. Collapsing only the
       durations still leaves the hero's six staggered delays intact, so the
       names would arrive in 60ms steps over a third of a second — a sequence,
       which is the thing being opted out of. A negative delay starts each
       animation already past its end, so the final state applies on frame one. */
    animation-delay: -.01ms !important;
    transition-delay: 0s !important;
  }

  /* Nothing is going to move, so there is no reason to fetch nine layers to
     build a picture one flat image already is. Because the layered set is
     loading="lazy", hiding it here means those requests are never made. */
  .scene { display: none; }
  .scene--flat { display: block; }
}
