/* jonw.xyz — the riparian scene.
 *
 * Responsive model: a bottom-anchored 16:10 "stage" (container units) holds
 * the composition; viewport-wide "bleed" bands behind it extend sky/water/
 * bank color to the screen edges on aspect ratios the stage doesn't cover.
 * Essential actors live within 22–78 cqw so the 640px stage floor can crop
 * the edges on small portrait screens without losing anything that matters.
 */

:root {
  /* The stage ALWAYS spans the viewport width, so 1cqw == 1vw and the
   * foreground band's left/right banks land exactly on the screen edges.
   * (The old `min(100vw, 165svh)` cap silently shrank the stage whenever the
   * window was wide relative to its height, pulling the banks inboard.)
   * Height follows at 16:10 and is bottom-anchored — a short window crops the
   * canopy from the top, which reads as zooming in. */
  --stage-w: 100vw;
  --stage-h: calc(var(--stage-w) / 1.6);
  /* Nav scale knob. The signpost art, the carved label size and the tap
   * targets all derive from this one width, so making the nav legible on a
   * phone is a single number rather than three that can drift apart.
   * (cqw resolves at the use site — inside .stage — not here.) */
  --signpost-x: 4cqw;
  --signpost-w: 16cqw;
  --grade-ms: 0ms; /* html.mode-animating raises this to 4s for the dusk grade */
}
html.mode-animating { --grade-ms: 4000ms; }

/* Boot veil: a plain sky-coloured panel laid OVER the scene, faded out once
 * the art has rasterised (see revealWhenReady in scene.js).
 * Deliberately not done by animating opacity on .backdrop/.stage: those are
 * huge subtrees carrying filters, and compositing them for an opacity
 * transition made Firefox re-rasterise the heavy tree mid-fade — which is the
 * partial-raster flash this is meant to hide. Animating one solid panel keeps
 * the art on the normal paint path. Skipped entirely without JS. */
.boot-veil {
  position: fixed;
  inset: 0;
  z-index: 200;
  pointer-events: none;
  background: linear-gradient(#e9dcbc, #d6d6b2 72%);
}
[data-mode="night"] .boot-veil { background: linear-gradient(#232b3d, #4a5570 72%); }
html:not(.js) .boot-veil { display: none; }
html.scene-ready .boot-veil { opacity: 0; transition: opacity .5s ease; }

.scene {
  position: fixed;
  inset: 0;
  overflow: hidden;
  background: var(--sky-lo);
  transition: background-color var(--grade-ms) linear;
}

/* gradients can't transition — the two sky washes cross-fade instead
 * (values mirror --sky-hi/--sky-lo in base.css) */
.bleed-sky { position: absolute; inset: 0; transition: opacity var(--grade-ms) linear; }
.bleed-sky--day   { background: linear-gradient(#e9dcbc, #d6d6b2 72%); opacity: var(--day-decor); }
.bleed-sky--night { background: linear-gradient(#232b3d, #4a5570 72%); opacity: var(--night-decor); }
/* the creature IS the cursor — everywhere, links and buttons included */
html.cursor-hidden,
html.cursor-hidden a,
html.cursor-hidden button { cursor: none; }

/* themed elements ease during the 4 s grade (no-op when --grade-ms is 0) */
.scene [fill], .bleed, .plank-label {
  transition:
    fill var(--grade-ms) linear,
    background-color var(--grade-ms) linear,
    color var(--grade-ms) linear,
    opacity var(--grade-ms) linear,
    filter var(--grade-ms) linear;
}

/* ---- bleed bands ---------------------------------------------------- */

.bleed { position: absolute; left: 0; right: 0; bottom: 0; z-index: 1; }
/* Fills the strip between the plate's bottom edge and the foreground band's
 * art (the band's top is irregular, so a gap opens mid-scene). Uses the
 * plate's own bottom-row colour and the SAME grade as the plate, so it tracks
 * it in both modes — driving it from --water instead showed as a navy stripe
 * across the water at night. */
.bleed--water {
  /* the plate's opaque art ends at ~.422 of --stage-w (its box bottom is .4273
   * less a 1.16% margin). Overlap that, but never rise so high it shows
   * through the plate's knocked-out sky. Tracks --stage-w, not vw, because the
   * plate does — pinned to vw it rode up to 56% of the plate in portrait
   * instead of 97%, straight into the sky it must stay below. */
  top: calc(var(--stage-w) * .415);
  height: auto;
  background: #c8d5c7;   /* the plate's on-screen bottom water (the traced
                          * SVG quantises colour, so sample the render, not
                          * the pre-trace PNG) */
  filter: var(--plant-grade);
  transition: filter var(--grade-ms) linear;
}
/* NOTE: the old .bleed--farbank strip is gone. The wide plate carries its own
 * rolling-hill silhouette and spans the viewport, so the strip added nothing —
 * and it showed through the plate's knocked-out sky as a flat line across the
 * hills. Don't reintroduce a bleed band above the waterline. */

/* sky decor (sun / moon+stars) cross-fades with the grade */
.sky-decor { position: absolute; inset: 0; pointer-events: none; }
.sky-decor .day-decor,
.sky-decor .night-decor { transition: opacity var(--grade-ms) linear; }
.sky-decor .day-decor   { opacity: var(--day-decor); }
/* .moon's geometry is inline on the element (see index.html) so it is correct
 * even before this sheet applies; it reads --moon-x/-y/-w, which only the
 * portrait block sets. Nothing to declare here. */
.sky-decor .night-decor { opacity: var(--night-decor); }

/* ---- stage ---------------------------------------------------------- */

.stage {
  position: absolute;
  bottom: 0;
  left: 50%;
  translate: -50% 0;
  width: var(--stage-w);
  aspect-ratio: 16 / 10;
  container-type: size;
  z-index: 2;
}

.layer { position: absolute; inset: 0; pointer-events: none; }
.layer--bgplants  { z-index: 2; filter: var(--plant-grade); }
.layer--waterfx   { z-index: 3; }
/* heron: behind the band and tree so it exits behind the trunk */
.layer--band      { z-index: 5; }
.layer--midplants { z-index: 6; filter: var(--plant-grade); transition: filter var(--grade-ms) linear; }
/* heron sits between the two cattail clusters: in front of the far one
 * (midplants), behind the near one by the signpost (fgplants) */
.layer--faractors { z-index: 7; }
/* no layer-level filter here: the props carry their own grades individually
 * (a parent filter would compound with the child's) */
.layer--props     { z-index: 8; }
.layer--lace      { z-index: 8; filter: var(--lace-grade); transition: filter var(--grade-ms) linear; }
.layer--actors    { z-index: 9; }
/* foot ripples: ABOVE the heron (so they cross its legs and read as the
 * water surface) but BELOW the tree — they were on top of it at z7 */
.layer--frontfx   { z-index: 7; }  /* after faractors in DOM → over the heron */
.layer--fgplants  { z-index: 10; filter: var(--plant-grade); transition: filter var(--grade-ms) linear; }
.layer--nav       { z-index: 11; pointer-events: auto; }
.layer--bgplants  { transition: filter var(--grade-ms) linear; }

.layer > svg { position: absolute; inset: 0; width: 100%; height: 100%; }

/* Plate sits at the VIEWPORT top, full width: the whole canopy arch shows and
 * stays put no matter the window height. Offsets cancel the traced canvas's
 * registration margins (0.5% sides, 1.16% top). Its lower water is hidden
 * behind the foreground band, which overlaps it on any window wider than
 * ~0.75x its height. */
.backdrop {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
  filter: var(--plant-grade);
  transition: filter var(--grade-ms) linear;
}
.backdrop img {
  position: absolute;
  /* Derived from --stage-w, not vw, and centred the same way .stage is: the
   * plate's far water and the stage's foreground band have to stay locked
   * together or they slide apart as the stage zooms. (The far-water glint drift
   * was exactly this bug.) At --stage-w:100vw these reproduce the previous
   * literal -0.7vw / -0.51vw / 101.02vw exactly. */
  top: calc(var(--stage-w) * -0.007);
  left: 50%;
  translate: -50% 0;
  width: calc(var(--stage-w) * 1.0102);
  height: auto;
  /* dissolve the plate's bottom edge into the water fill below it (same
   * colour), so its feathered alpha edge leaves no hairline */
  -webkit-mask-image: linear-gradient(180deg, #000 95%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 95%, transparent 100%);
}

/* ---- flip-book playback --------------------------------------------- */

/* imgs may sit below a motion wrapper (.flyer/.hop-y) — match descendants */
.fb { position: relative; overflow: hidden; }
.fb img { display: none; width: calc(var(--fb-n, 1) * 100%); }
.fb img.on { display: block; }
.fb.at-idle img[data-seq="idle"] { display: block; width: 100%; }

@keyframes fb-play { to { transform: translateX(-100%); } }
/* one-shot keyframes (fbo-N, stopping one frame early) are generated with
 * literal percentages in anim.gen.css */

/* All animation longhands (name/duration/steps/iteration/fill) come from
 * anim.gen.css as LITERAL values — no var()/calc() in animation properties,
 * so every engine plays them. play-loop/play-once are markers only. */
.fb img.play-rev { animation-direction: reverse; }

/* cattail instances: pure CSS loops; per-instance cycle length and phase are
 * inline animation-duration/-delay styles on the img (inline wins cascade) */
.sway { overflow: hidden; }
.sway img {
  display: block;
  width: calc(var(--fb-n) * 100%);
}
/* background clusters idle on a single neutral frame until their strip is
 * promoted — don't run the strip animation on the static frame */
.sway img[data-src] { animation: none !important; }

/* ---- actors --------------------------------------------------------- */

.actor { position: absolute; pointer-events: none; }
.actor .flyer { width: 100%; height: 100%; }   /* WAAPI flight/hop wrapper */

/* bottoms compensate for empty canvas below the art (grounded poses end at
 * ~69–70% of the shared frame box — measured from the .reg frames):
 * heron feet land at 15cqh (shallows), owl feet at 43.5cqh (snag top),
 * toad at ~12cqh (bank surface) */
/* heron mirrored to face into the scene (art faces right) — NOTE: step-3
 * transition work must compose this flip into the WAAPI flight transform */
.actor--heron { left: 47cqw; bottom: 1cqh; width: 14cqw; }
.actor--toad  { left: 79cqw; bottom: 2cqh; width: 7.5cqw; }
.actor--owl   { left: 80cqw; bottom: 24cqh; width: 9cqw; filter: var(--owl-grade); }

.actor.flying { z-index: 20; }
.actor.away { visibility: hidden !important; }

/* Boot arrival, pre-paint half: hold the cast off-stage from the head script
 * until scene.js hands over to .away (see index.html). Hiding every .actor is
 * the same set as the arriving cast — the off-mode ones are hidden anyway by
 * the rules below. Skipped under reduced motion, where nothing arrives and the
 * scene must render as the complete still-life. */
@media (prefers-reduced-motion: no-preference) {
  html.pre-arrival .actor { visibility: hidden; }
}

/* rest-state visibility per mode; JS lifts these with .in-scene during
 * transitions so an exiting creature stays visible after the mode flips */
[data-mode="day"] .actor--night:not(.in-scene) { visibility: hidden; }
[data-mode="night"] .actor--day:not(.in-scene) { visibility: hidden; }
:root:not([data-mode]) .actor--night { visibility: hidden; }
@media (prefers-color-scheme: dark) {
  :root:not([data-mode]) .actor--night { visibility: visible; }
  :root:not([data-mode]) .actor--day { visibility: hidden; }
}

/* toad hop: outer wrapper translates X, inner arcs Y (JS/WAAPI drives both) */
.actor--toad .hop-y { width: 100%; height: 100%; }
.actor--toad.faces-left .fb { transform: scaleX(-1); }

/* ---- plants --------------------------------------------------------- */

.plant { position: absolute; }
.plant.mirror { transform: scaleX(-1); }
/* animated cluster overlaying the band's baked cattails (mixed motion) */
.plant--fg-l  { left: 7cqw; bottom: 6.5cqh; width: 11cqw; }
.plant--bg-1  { left: 42cqw; bottom: 19.5cqh; width: 9cqw; }
.layer--bgplants .plant { filter: var(--bg-distance); }
/* static plants only — must NOT override `.sway img`, whose
 * width: calc(var(--fb-n) * 100%) is what clips a strip to one frame */
.plant:not(.sway) img { width: 100%; }

/* ---- lily-pad lace overlays ----------------------------------------- */

.lace { position: absolute; }
.lace img { width: 100%; }
/* feather the crop-rect edges into the pool (element + img masks compose) */
.lace--near {
  left: 52cqw; bottom: 2cqh; width: 30cqw;
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 14%, #000 90%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 14%, #000 90%, transparent 100%);
}
.lace--near img {
  -webkit-mask-image: linear-gradient(180deg, transparent 2%, #000 22%);
  mask-image: linear-gradient(180deg, transparent 2%, #000 22%);
}

/* ---- props (traced pipeline art) ------------------------------------ */

.prop { position: absolute; filter: var(--prop-grade); transition: filter var(--grade-ms) linear; }
.prop img { width: 100%; }
/* foreground band: the whole 1584×672 sheet traced as ONE piece — splitting it
 * left a seam (halves cleaned/traced independently diverge in water tone).
 * Spans the stage exactly so its raised left bank and rising right bank land
 * on the viewport edges (stage width == viewport width on laptops).
 * Measured profile (x% → top of art, % down the band):
 *   left bank 0–38% · pool 40–72% · right bank 73–100% */
.prop--fg {
  /* the traced canvas carries an 8px transparent registration margin (0.5% a
   * side), so 100cqw leaves the banks visibly inset — overscan to cancel it */
  left: -0.51cqw; bottom: -1cqh; width: 101.02cqw;
  /* scaled down (not translated) — vertical compression is the only way to
   * shrink the banks while still spanning the viewport, since spanning the
   * width pins the height at width/2.645 */
  transform: scaleY(.87);
  transform-origin: bottom;
}
/* tree rooted ON the land spit that runs from the right bank into the centre
 * along the bottom of the band (spans ~42–85% of the band width). Root flare
 * is 17–69% of the tree box, so the box is sized to land the flare there. */
.prop--tree     { left: 47cqw; bottom: -3cqh; width: 52cqw; }
/* signpost holds the left bank */
.prop--signpost { left: var(--signpost-x); bottom: 9cqh; width: var(--signpost-w); }
/* boulders/idle.svg is unused in this composition — the foreground band's own
 * pebbles and rocks fill that role; the band would hide it anyway */

/* ---- signpost nav (registered over the traced plank art, 456×888) ---- */

.signnav {
  position: absolute;
  left: var(--signpost-x); bottom: 9cqh;
  width: var(--signpost-w);
  height: calc(var(--signpost-w) * 1.95); /* 888/456 — matches the traced art box */
}
/* Labels are carved into the boards rather than set on them: a translucent
 * warm-dark ink lets the wood grain read through the strokes, a bright lip
 * below each letter catches the light, and a soft dark line above sits in the
 * groove. Real text (not images) — selectable, focusable, no extra assets. */
.signnav a {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 9%;
  text-decoration: none;
  /* carved/inscriptional faces first; Georgia elsewhere */
  font-family: Herculanum, Trattatello, Copperplate, Georgia, serif;
  /* .125 of the signpost width — the ratio the carved look was tuned at
   * (2cqw against a 16cqw post); scales with --signpost-w, not the stage, so
   * a bigger phone signpost gets proportionally bigger lettering */
  font-size: calc(var(--signpost-w) * .125);
  /* uppercase keeps the carved-signage look consistent where Herculanum
   * (macOS-only, capitals-only) isn't available and Georgia takes over */
  text-transform: uppercase;
  letter-spacing: .07em;
  white-space: nowrap;
  color: var(--plank-ink);
  text-shadow:
    0 .055em 0 var(--plank-carve-hi),
    0 -.03em .02em var(--plank-carve-lo);
  cursor: pointer;
  /* a touch of foreshortening so the lettering sits on the board's plane
   * instead of floating flat in front of it */
  transform: translate(-50%, -50%) perspective(420px) rotateY(-7deg);
}
/* underline on hover — scored into the board rather than a web underline:
 * text-shadow applies to decorations too, so it picks up the same carve
 * highlight as the letters. focus-visible matches it for keyboard users. */
.signnav a:hover,
.signnav a:focus-visible {
  color: var(--plank-ink-hover);
  text-decoration: underline;
  text-decoration-thickness: .06em;
  text-underline-offset: .2em;
}

/* Per-plank geometry, measured by isolating each board in the columns that
 * miss the upright (the post merges with every plank and wrecks a naive scan)
 * and least-squares fitting its centreline — max residual 3px over 260+
 * columns, so these are the real axes:
 *   About    centre 48.2%,13.0%  tilt +6.87deg  board 10.5-86.0%
 *   Projects centre 52.6%,25.2%  tilt +2.60deg  board 17.1-88.2%
 *   Photos   centre 53.5%,39.2%  tilt +6.01deg  board 19.3-87.7%
 * Each label is centred ON its board's midpoint via translate(-50%,-50%),
 * then rotated onto that board's axis. */
.signnav a:nth-child(1) {
  left: 48.2%; top: 12.94%; width: 75.4%;
  transform: translate(-50%, -50%) rotate(6.87deg) perspective(420px) rotateY(-7deg);
}
.signnav a:nth-child(2) {
  left: 52.6%; top: 25.19%; width: 71.1%;
  transform: translate(-50%, -50%) rotate(2.60deg) perspective(420px) rotateY(-7deg);
}
.signnav a:nth-child(3) {
  left: 53.5%; top: 39.18%; width: 68.4%;
  transform: translate(-50%, -50%) rotate(6.01deg) perspective(420px) rotateY(-7deg);
}

/* ---- water fx ------------------------------------------------------- */

.glint { animation: glint-drift 34s ease-in-out infinite alternate; }

/* Far-water glint. Sits between the plate (z 1) and the stage (z 2), so the
 * foreground band can occlude it — at wide-and-short aspects the plate's far
 * water is hidden behind the near bank, and the glint correctly goes with it.
 * All lengths are vw because the plate is width-scaled and top-anchored: 35vw
 * is plate-top + 0.822 of plate height, the spot it occupied back when stage
 * and plate happened to coincide at 16:10. Do not convert these to cqw. */
.farwater-fx { position: absolute; inset: 0; z-index: 1; pointer-events: none; overflow: hidden; }
.glint--far {
  position: absolute;
  /* --stage-w-relative, and horizontally measured from the CENTRE because the
   * plate is centred and may be wider than the viewport. Reproduces the
   * previous 35vw / 27.5vw / 15vw / .375vw exactly at --stage-w:100vw, and
   * holds the 0.8218-of-plate-height invariant at every aspect. */
  top: calc(var(--stage-w) * .35);
  left: calc(50% - var(--stage-w) * .225);
  width: calc(var(--stage-w) * .15);
  height: calc(var(--stage-w) * .00375);
  border-radius: calc(var(--stage-w) * .002);
  background: var(--water-glint);
  opacity: .6;
  /* its own keyframes: glint-drift drifts in cqw, and outside .stage there is
   * no container to resolve against — engines differ on that fallback */
  animation-name: glint-drift-far;
}
@keyframes glint-drift-far { from { transform: translateX(0); } to { transform: translateX(calc(var(--stage-w) * .025)); } }
.glint--2 { animation-duration: 46s; animation-delay: -12s; }
@keyframes glint-drift { from { transform: translateX(0); } to { transform: translateX(2.5cqw); } }

/* ripples: three variant groups swapped discretely (storybook, not tweened) */
.ripple-set g { opacity: 0; animation: ripple-cycle 2.4s step-end infinite; }
.ripple-set g:nth-child(2) { animation-delay: .8s; }
.ripple-set g:nth-child(3) { animation-delay: 1.6s; }
.ripple-set--slow g { animation-duration: 3.6s; }
.ripple-set--slow g:nth-child(2) { animation-delay: 1.2s; }
.ripple-set--slow g:nth-child(3) { animation-delay: 2.4s; }
@keyframes ripple-cycle {
  0%, 32.9% { opacity: 1; }
  33% , 100% { opacity: 0; }
}

/* ---- cursor creature ------------------------------------------------ */

.flyer-cursor {
  position: fixed;
  left: 0; top: 0;
  width: 96px; height: 81px;    /* ≈ dragonfly frame aspect */
  z-index: 100;                 /* topmost — it IS the cursor, above all UI */
  pointer-events: none;
  visibility: hidden;           /* JS reveals when active */
}
.flyer-cursor .spin { position: relative; width: 100%; height: 100%; transform-origin: 50% 50%; }
.flyer-cursor .swap { position: absolute; inset: 0; transition: opacity .6s linear; }
.flyer-cursor .swap.faded { opacity: 0; }
.firefly svg {
  width: 44px; height: 44px;
  position: absolute;
  left: 50%; top: 50%;
  translate: -50% -50%;
}

/* Coarse pointers: there is no cursor to embody, so the creature is just
 * another ambient animal — and at a fixed 96px it rendered 1.8x WIDER than the
 * heron on a phone, because it was the only thing in the scene measured in px
 * while everything else scales with the stage. Scale it from --stage-w too,
 * holding the ratio it has to the heron on desktop (96 / 201.6 = .476 of the
 * heron's 14cqw, hence .0667). The factors reproduce 96x81 and 44px exactly at
 * a 1440-wide stage; fine pointers keep the literal px and are untouched. */
@media (hover: none) {
  .flyer-cursor {
    width: calc(var(--stage-w) * .0667);
    height: calc(var(--stage-w) * .0563);
  }
  .firefly svg {
    width: calc(var(--stage-w) * .0306);
    height: calc(var(--stage-w) * .0306);
  }
}
.firefly .halo { animation: firefly-pulse var(--pulse, 2s) ease-in-out infinite; transform-origin: 50% 50%; }
@keyframes firefly-pulse {
  0%, 100% { opacity: .25; transform: scale(.8); }
  50% { opacity: 1; transform: scale(1.15); }
}

/* ---- UI ------------------------------------------------------------- */

.mode-toggle {
  position: fixed;
  top: 18px; right: 18px;
  z-index: 50;
  width: 44px; height: 44px;
  border: none;
  border-radius: 50%;
  background: var(--ui-face);
  color: var(--ui-icon);
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: background-color var(--grade-ms) linear, opacity .5s ease;
}
.mode-toggle img { width: 34px; height: 34px; }
.mode-toggle .icon-sun { display: none; }
[data-mode="night"] .mode-toggle .icon-sun { display: block; }
[data-mode="night"] .mode-toggle .icon-moon { display: none; }
.mode-toggle[disabled] { opacity: .6; }
.mode-toggle.pending { outline: 2px dashed var(--ui-icon); outline-offset: 2px; }
html:not(.js) .mode-toggle { display: none; } /* inert without JS */
/* The toggle sits above the scene but is visually part of it, and its day
 * glyph is a moon — on a slow load it was the one thing that appeared before
 * the art, reading as a stray moon on an empty sky. It fades in WITH the
 * scene, never ahead of it. (The veil covers it too; this is the belt to that
 * pair of braces, and it also covers the veil's own fade window.) */
html.js:not(.scene-ready) .mode-toggle { opacity: 0; }

/* ---- portrait ------------------------------------------------------- */

/* Everything scene-side derives from --stage-w, so a narrow screen shrank the
 * WHOLE composition: at 390px the stage was 244px tall (29% of the screen),
 * the nav 7.8px with 11px tap targets, and ~50% of the screen was flat bleed.
 * Here the stage is scaled off HEIGHT instead — which zooms the art and crops
 * the vista left/right — and the cast is re-anchored into what stays visible.
 *
 * NOTE THE DIRECTION: this only ever GROWS the stage (100vw is the floor).
 * The `min(100vw, 165svh)` cap the note at the top of this file warns about
 * did the opposite — it SHRANK the stage on wide windows and pulled the banks
 * inboard. svh, not vh, so the iOS URL bar hiding doesn't resize the scene.
 *
 * Landscape phones (844x390) stay on the desktop path by design: the stage is
 * already 527px tall there, so there is nothing to fix. */
@media (max-aspect-ratio: 1/1) {
  :root {
    /* 81svh is chosen against the plate/stage seam, not picked for looks: the
     * plate is top-anchored and the stage bottom-anchored, so below this they
     * pull apart and leave a dead band of flat bleed colour across the middle
     * (26% of the screen at 70svh). Closing the seam completely needs ~95svh,
     * but that narrows the visible window to 48cqw — too narrow for the
     * signpost to get its base onto the left bank at a legible size. 81svh is
     * the balance: a 15% seam, and 57cqw of window to compose in. */
    --stage-w: clamp(100vw, 81svh, 180vw);
    /* 28cqw puts the planks ~44px apart (they sit .264 of the post width
     * apart, and the post is .42 of its box from the left edge, so the base
     * still lands on the left bank at --signpost-x:14cqw) */
    --signpost-w: 28cqw;
    --signpost-x: 22cqw;
  }

  /* Re-anchored cast. These positions are constrained by the BAND ART, not
   * free: the band's terrain sits at fixed cqw positions, so an actor can only
   * move to somewhere its own footing still exists — left bank ends ~28cqw,
   * water runs ~28-66cqw, right land starts ~66cqw. The heron therefore stays
   * where it is (it must stand in water) and only the land-dwellers move. */
  /* Scale relationships, not just positions. Cropping to ~57cqw of window
   * while the signpost had to grow to 28cqw for legible tap targets left it
   * .58x the tree and 2.0x the heron — against .31x and 1.14x on desktop, i.e.
   * roughly twice as dominant as the composition intends. The others grow to
   * pull those ratios back.
   *
   * The grounded actors need their `bottom` re-derived, not just their width:
   * the art's feet sit ~70% down the frame box, so a box that grows from a
   * fixed bottom edge lifts the animal off the ground. Each bottom below keeps
   * feet where they were — see the per-actor arithmetic in the plan notes. */
  .actor--heron { left: 40cqw; width: 30cqw; bottom: -9.95cqh; }
  /* Toad dropped in portrait. The enlarged tree's roots take the right-hand
   * land it sat on, and the only other footing — the left bank — is across
   * open water, which its hop-in entrance would have to cross. JS reads this
   * `display:none` and skips its beats and schedules (see `shown` in
   * transition.js); nothing needs to be kept in sync by hand. */
  .actor--toad  { display: none; }
  /* The tree runs off the right edge on purpose: at this size its trunk sits
   * at 70-102cqw, so only the canopy and a sliver of trunk are in frame and it
   * never crowds the heron. Its base and the branch the owl used are both past
   * the crop — hence the owl move below. */
  .prop--tree   { left: 33cqw; width: 100cqw; }
  /* Owl relocated onto the ABOUT plank: the tree branch it perched on is now
   * off-frame, and the post cap is too narrow to read as contact. Nothing in
   * JS needs to know — the arrival flight measures live rects, so it lands
   * wherever this puts it.
   *
   * Vertical is set from the BIRD's feet, not the art's alpha bottom. The
   * perch frame has a branch/tuft pad baked under the feet running to .697 of
   * the box while the feet themselves end near .65, so aligning the alpha
   * bottom to a surface leaves the owl visibly hovering above it. The pad is
   * allowed to overlap the plank, which reads as contact rather than a float. */
  .actor--owl   { left: 33cqw; width: 19cqw; bottom: 73.3cqh; }

  /* scenery, not cast — dropping it buys room in the narrower window */
  .plant--bg-1 { display: none; }

  /* The heron grew to 22cqw here, so the creature has to grow with it or it
   * stops reading as one of the animals (it fell to .30x the heron). Same
   * .476 ratio the hover:none block above is built on. */
  .flyer-cursor { width: calc(var(--stage-w) * .144); height: calc(var(--stage-w) * .1215); }
  .firefly svg  { width: calc(var(--stage-w) * .066); height: calc(var(--stage-w) * .066); }

  /* The moon belongs in the PLATE's open sky, and the plate now scales and
   * centres with the stage. Left in vw/vh it stayed put while the sky grew
   * past it, and on a phone it ended up behind the plate's opaque water. */
  :root {
    --moon-x: calc(50% - var(--stage-w) * .24);
    --moon-y: calc(var(--stage-w) * .1435);
    --moon-w: calc(var(--stage-w) * .05);
  }
}

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

@media (prefers-reduced-motion: reduce) {
  .fb > img, .sway > img, .glint, .ripple-set g,
  .firefly .halo, .actor, .flyer-cursor {
    animation: none !important;
    transition: none !important;
  }
  .scene [fill], .bleed, .plank-label, .mode-toggle { transition: none !important; }
  .flyer-cursor { display: none; }
  html.cursor-hidden .scene { cursor: auto; }
}
