/* ==========================================================================
   naturfilms — the fun bits
   Rule of the house: motion never covers a photograph. Critters live in
   margins, dividers and the footer. Everything degrades to nothing under
   prefers-reduced-motion.
   ========================================================================== */

/* ==========================================================================
   Atmosphere — the whole site sits in mist
   ==========================================================================
   Three fixed bands of very soft gradient drifting at different speeds behind
   the content. Fixed rather than per-section so the fog reads as one body of
   air the page moves through, instead of a decoration repeated on each block.
   Pure transform/opacity on a promoted layer, so it costs a compositor pass
   and nothing else.
   -------------------------------------------------------------------------- */

.mist {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  overflow: hidden;
  contain: strict;
}

.mist-band {
  position: absolute;
  left: -30%;
  width: 160%;
  height: 60vh;
  will-change: transform, opacity;
  background: radial-gradient(
    60% 100% at 50% 50%,
    color-mix(in oklab, var(--c-mist, #9fb0c4) 22%, transparent) 0%,
    color-mix(in oklab, var(--c-mist, #9fb0c4) 9%, transparent) 42%,
    transparent 72%);
  filter: blur(28px);
  opacity: 0;
  animation: mist-drift var(--dur, 58s) ease-in-out var(--delay, 0s) infinite;
}

.mist-band--a { top: 4%;  height: 52vh; --dur: 62s; --delay: -8s; }
.mist-band--b { top: 34%; height: 68vh; --dur: 84s; --delay: -30s; opacity: .8; }
.mist-band--c { top: 66%; height: 58vh; --dur: 74s; --delay: -52s; }

@keyframes mist-drift {
  0%   { transform: translate3d(-6%, 4%, 0) scaleX(1);    opacity: 0; }
  18%  { opacity: .55; }
  50%  { transform: translate3d(6%, -3%, 0) scaleX(1.15); opacity: .8; }
  82%  { opacity: .45; }
  100% { transform: translate3d(-6%, 4%, 0) scaleX(1);    opacity: 0; }
}

[data-theme="dark"] { --c-mist: #7e9bd0; }
[data-theme="light"] { --c-mist: #ffffff; }
:root:not([data-theme]) { --c-mist: #7e9bd0; }
@media (prefers-color-scheme: light) { :root:not([data-theme]) { --c-mist: #ffffff; } }

/* ---- Grain ---------------------------------------------------------------
   The reference frame is a high-ISO exposure at blue hour, and the noise is
   part of why it reads as photographic rather than rendered. A single tiling
   SVG turbulence, fixed and inert, gives the whole page the same tooth.
   -------------------------------------------------------------------------- */

body::after {
  content: "";
  position: fixed;
  inset: 0;                 /* fixed and static — no need to oversize it */
  z-index: 3;
  pointer-events: none;
  opacity: .16;
  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='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.55'/%3E%3C/svg%3E");
  mix-blend-mode: overlay;
}
[data-theme="light"] body::after { opacity: .08; }

/* Haze at the foot of every section, so blocks dissolve into one another
   instead of stacking with hard seams. */
.section::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 140px;
  pointer-events: none;
  background: linear-gradient(to top,
    color-mix(in oklab, var(--c-bg) 55%, transparent), transparent);
}

/* Content sits above the fog. The header is deliberately not in this list: it
   keeps its own --z-header layer, because at z-index 2 it ties with main and
   loses on document order — which leaves the nav dropdown painting behind the
   hero. It's already above .mist (z-index 1) either way. */
main, .site-footer { position: relative; z-index: 2; }

body.calm .mist,
body.calm .mist-band { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .mist-band { animation: none; opacity: .38; }
}

/* ---- Fog through the field-notes ticker -----------------------------------
   The footer sits above .mist, so the quote band would otherwise be the one
   strip of the site with no weather in it. Same feTurbulence cloud masses as
   the intro curtain (see initFogDefs), only slower and thinner, drifting the
   other way to the quotes so the two never read as one sheet.
   -------------------------------------------------------------------------- */

.ticker-fog {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  opacity: .5;
}

.ticker-fog-layer {
  position: absolute;
  /* Far bigger than the band in every direction: the filter region has hard
     edges of its own, and a cloud the height of a 40px strip is a smudge. */
  left: -60%; right: -60%;
  top: -130%; height: 360%;
  will-change: transform;
  animation: ticker-fog-drift
    calc(88s + var(--f) * 41s) ease-in-out calc(var(--f) * -29s) infinite alternate;
}
/* Second mass runs against the first, so they pass through one another. */
.ticker-fog-layer:nth-child(even) { animation-direction: alternate-reverse; opacity: .75; }

@keyframes ticker-fog-drift {
  from { transform: translate3d(-12%, 3%, 0) scale(1.06); }
  to   { transform: translate3d(12%, -3%, 0) scale(1.16); }
}

/* ---- Ridge divider ------------------------------------------------------ */

.ridge-divider {
  position: relative;
  height: clamp(70px, 9vw, 120px);
  overflow: hidden;
  pointer-events: none;
  contain: strict;
  margin-block: calc(var(--sp-6) * -1) 0;
}
.ridge-divider svg { width: 100%; height: 100%; display: block; }
.ridge-divider .rd {
  fill: currentColor;
  will-change: transform;
  transition: transform 900ms var(--ease-out);
}
/* Atmospheric perspective: the further back, the more air in front of it. */
.ridge-divider .rd--far  { color: color-mix(in oklab, var(--c-bg) 84%, var(--c-second)); }
.ridge-divider .rd--mid  { color: color-mix(in oklab, var(--c-bg) 91%, var(--c-second)); }
.ridge-divider .rd--near { color: color-mix(in oklab, var(--c-bg) 96%, var(--c-second-deep)); }

/* ---- Scroll reveal ------------------------------------------------------ */

.reveal {
  opacity: 0;
  transform: translateY(30px);
  transition: opacity 820ms var(--ease-out), transform 820ms var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}
.reveal.is-in { opacity: 1; transform: none; }

.reveal--left  { transform: translateX(-34px); }
.reveal--right { transform: translateX(34px); }
.reveal--scale { transform: scale(.94); }
.reveal--left.is-in, .reveal--right.is-in, .reveal--scale.is-in { transform: none; }

/* Word-by-word rise for headings */
.reveal-words .word {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
}
.reveal-words .word > span {
  display: inline-block;
  transform: translateY(105%);
  transition: transform 780ms var(--ease-out);
  transition-delay: calc(var(--w, 0) * 42ms);
}
.reveal-words.is-in .word > span { transform: none; }

/* ---- Cursor glow (desktop, fine pointer only) --------------------------- */

/* No mix-blend-mode here on purpose: blending a fixed, full-size element
   against the page forces the compositor to re-rasterize everything beneath
   it on every frame. A plain low-alpha radial looks the same and stays on
   its own GPU layer. */
.cursor-glow {
  position: fixed;
  top: 0; left: 0;
  width: 380px; height: 380px;
  margin: -190px 0 0 -190px;
  border-radius: 50%;
  pointer-events: none;
  z-index: var(--z-cursor);
  background: radial-gradient(circle, var(--c-glow) 0%, transparent 62%);
  opacity: 0;
  transition: opacity 500ms var(--ease-out), width 380ms var(--ease-out), height 380ms var(--ease-out);
  will-change: transform;
  contain: strict;
}
body.has-cursor-glow .cursor-glow { opacity: 1; }
body.cursor-hot .cursor-glow { width: 520px; height: 520px; }

@media (hover: none), (pointer: coarse) {
  .cursor-glow { display: none; }
}

/* ---- Magnetic elements -------------------------------------------------- */

.magnetic {
  will-change: transform;
  transition: transform 420ms var(--ease-out);
}

/* ---- Card tilt ---------------------------------------------------------- */

.tilt {
  transform: perspective(1100px)
             rotateX(var(--tilt-x, 0deg))
             rotateY(var(--tilt-y, 0deg))
             translateZ(0);
  transition: transform 520ms var(--ease-out);
}
.tilt.is-tilting { transition: transform 90ms linear; }

/* Moving specular sheen following the pointer.
   Also blend-mode-free — with one of these per card, `mix-blend-mode` alone
   dropped the gallery to single-digit FPS. */
.tilt-sheen {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--t-mid) var(--ease-out), visibility var(--t-mid);
  background: radial-gradient(
    420px circle at var(--mx, 50%) var(--my, 50%),
    rgba(255, 255, 255, 0.16), transparent 46%);
  z-index: 2;
}
.photo-card:hover .tilt-sheen { opacity: 1; visibility: visible; }

/* ---- Critters ----------------------------------------------------------- */

.critter-lane {
  position: relative;
  height: 74px;
  overflow: hidden;
  pointer-events: none;
  z-index: var(--z-critters);
  /* These animate forever. Containment keeps their repaints inside the lane
     instead of invalidating the whole document every frame. */
  contain: strict;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.critter-lane::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 14px;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--c-line-str), transparent);
}

.critter {
  position: absolute;
  bottom: 14px;
  left: 0;
  color: var(--c-text-mute);
  opacity: .55;
  transform: translateX(-140px);
  will-change: transform;
}
.critter svg { width: var(--critter-w, 52px); height: auto; }

.critter.walk { animation: critter-cross var(--dur, 26s) linear var(--delay, 0s) infinite; }
.critter.walk.reverse {
  animation-name: critter-cross-rev;
  transform: translateX(calc(100vw + 140px)) scaleX(-1);
}

@keyframes critter-cross {
  from { transform: translateX(-140px); }
  to   { transform: translateX(calc(100vw + 140px)); }
}
@keyframes critter-cross-rev {
  from { transform: translateX(calc(100vw + 140px)) scaleX(-1); }
  to   { transform: translateX(-140px) scaleX(-1); }
}

/* little bob so they don't glide like ghosts */
.critter .body { animation: critter-bob 620ms var(--ease-in-out) infinite alternate; }
@keyframes critter-bob {
  from { transform: translateY(0) rotate(-.6deg); }
  to   { transform: translateY(-3px) rotate(.6deg); }
}
.critter .leg-a { animation: leg 400ms linear infinite alternate; transform-origin: top center; }
.critter .leg-b { animation: leg 400ms linear infinite alternate-reverse; transform-origin: top center; }
@keyframes leg {
  from { transform: rotate(-19deg); }
  to   { transform: rotate(19deg); }
}

/* Birds drifting across the hero — high up, out of the way */
.bird-flock {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 1;
}
.bird {
  position: absolute;
  color: rgba(255,255,255,.30);
  animation: bird-fly var(--dur, 40s) linear var(--delay, 0s) infinite;
  will-change: transform;
}
.bird svg { width: var(--bird-w, 26px); height: auto; }
.bird .wing {
  transform-origin: 50% 60%;
  animation: flap var(--flap, 520ms) var(--ease-in-out) infinite alternate;
}
@keyframes flap {
  from { transform: rotate(-16deg) scaleY(1); }
  to   { transform: rotate(15deg) scaleY(.82); }
}
@keyframes bird-fly {
  from { transform: translate(-14vw, 0); }
  to   { transform: translate(118vw, -9vh); }
}

/* ---- Floating dust / pollen in the hero --------------------------------- */

.motes { position: absolute; inset: 0; pointer-events: none; overflow: hidden; z-index: 1; }
.mote {
  position: absolute;
  border-radius: 50%;
  background: var(--c-accent-soft);
  opacity: .0;
  animation: mote-drift var(--dur, 22s) linear var(--delay, 0s) infinite;
  will-change: transform, opacity;
}
@keyframes mote-drift {
  0%   { transform: translate3d(0, 20px, 0) scale(.6); opacity: 0; }
  12%  { opacity: .5; }
  88%  { opacity: .35; }
  100% { transform: translate3d(var(--dx, 40px), -70vh, 0) scale(1.1); opacity: 0; }
}

/* ---- Ridge parallax ----------------------------------------------------- */

.ridges {
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  pointer-events: none;
  z-index: 2;
}
.ridge {
  position: absolute;
  left: -6%; right: -6%;
  bottom: 0;
  will-change: transform;
  transform: translate3d(0, var(--ry, 0px), 0);
}
.ridge svg { width: 100%; height: auto; display: block; }
.ridge--far  { color: color-mix(in oklab, var(--c-bg) 78%, var(--c-second-deep)); opacity: .55; }
.ridge--mid  { color: color-mix(in oklab, var(--c-bg) 88%, var(--c-second-deep)); opacity: .75; }
.ridge--near { color: var(--c-bg); }

/* ---- Section divider: drifting topographic contour ----------------------
   Two copies of the same contour sit side by side in a track twice the
   container's width; sliding the track by exactly -50% loops seamlessly.
   The animation is a pure compositor transform on a promoted layer, so the
   contour rasterises once instead of per frame.
   -------------------------------------------------------------------------- */

.topo-divider {
  position: relative;
  height: 60px;
  overflow: hidden;
  pointer-events: none;
  color: var(--c-line-str);
  contain: strict;
  opacity: .7;
}
.topo-track {
  display: flex;
  width: 200%;
  height: 100%;
  will-change: transform;
  animation: topo-slide 70s linear infinite;
}
.topo-track svg { flex: 0 0 50%; height: 100%; }
@keyframes topo-slide {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}

/* ==========================================================================
   Arrival — the intro curtain
   ========================================================================== */

.intro {
  position: fixed;
  inset: 0;
  z-index: 300;
  display: grid;
  place-items: center;
  background: var(--c-bg-sink);
  overflow: hidden;
  /* The curtain's own fade runs last and long — the mist parts first, and the
     fade only mops up whatever haze is left. */
  transition: opacity 1100ms var(--ease-out) 320ms;
}
.intro.is-leaving {
  opacity: 0;
  pointer-events: none;
}

/* Cold pre-dawn air behind everything. */
.intro-sky {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(120% 80% at 50% 92%, color-mix(in oklab, var(--c-accent) 16%, transparent), transparent 60%),
    radial-gradient(90% 60% at 50% 20%, color-mix(in oklab, var(--c-second) 18%, transparent), transparent 70%);
  opacity: 0;
  animation: intro-sky 4200ms var(--ease-out) both;
}
@keyframes intro-sky {
  0% { opacity: 0; }
  40% { opacity: 1; }
  100% { opacity: 1; }
}

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

/* Ridges rise into place. */
.intro-ridges {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 42vh;
  z-index: 1;
}
.intro-ridges svg { width: 100%; height: 100%; }
/* The range surfaces slowly, far layers first — distance arrives before the
   foreground does, the way a valley clears from the top down. */
.intro-ridges .i-ridge {
  fill: currentColor;
  transform: translateY(30%);
  animation: ridge-rise 2600ms var(--ease-out) both;
}
.intro-ridges .i-ridge--far  { color: color-mix(in oklab, var(--c-bg-sink) 72%, var(--c-second)); animation-delay: 400ms; }
.intro-ridges .i-ridge--mid  { color: color-mix(in oklab, var(--c-bg-sink) 84%, var(--c-second)); animation-delay: 750ms; }
.intro-ridges .i-ridge--near { color: var(--c-bg-sink); animation-delay: 1100ms; }
@keyframes ridge-rise { to { transform: translateY(0); } }

/* ---- The mist -----------------------------------------------------------
   Cloud masses stacked over the whole curtain. They drift in, thicken, and
   then — on leaving — slide apart to the edges while the ridge and the
   wordmark stay put behind them, so the page underneath is revealed rather
   than faded to.

   The texture is not drawn: each layer is filled by an feTurbulence
   fractalNoise filter (see initFogDefs in motion.js), which is the primitive
   the SVG spec provides for simulating clouds and smoke. Gradients could only
   ever give smooth blobs or stripes; fractal noise gives actual cloud, with
   holes and wisps and no direction to it.

   The noise is rasterised once. Everything after that is transform and
   opacity, so the whole sequence stays on the compositor.
   ------------------------------------------------------------------------ */

.intro-fog {
  position: absolute;
  inset: -10%;
  z-index: 4;
  pointer-events: none;
}

.intro-fog-layer {
  position: absolute;
  /* Oversized, so the filter region's own edges are always off-screen. */
  left: -40%; right: -40%;
  height: 90%;
  top: calc(var(--f) * 8% - 12%);
  color: var(--c-mist);                /* feFlood reads this */
  opacity: 0;
  will-change: transform, opacity;
  animation:
    intro-fog-in 2400ms var(--ease-out) calc(var(--f) * 150ms) both,
    intro-fog-drift calc(52s + var(--f) * 13s) linear calc(var(--f) * -11s) infinite alternate;
  transition:
    transform 1500ms cubic-bezier(.22, .61, .36, 1) calc(var(--f) * 60ms),
    opacity 1100ms var(--ease-out) calc(var(--f) * 60ms);
}
/* Alternate masses drift the other way, so they slide through one another
   instead of moving as a single sheet. */
.intro-fog-layer:nth-child(even) { animation-direction: normal, alternate-reverse; }

/* The colour the fog is flooded with. Setting it in CSS rather than inside the
   filter means one definition serves both themes. */
#nf-fog-defs feFlood { flood-color: var(--c-mist); flood-opacity: .3; }

@keyframes intro-fog-in {
  from { opacity: 0; transform: translate3d(0, 10%, 0) scale(1.18); }
  to   { opacity: .6; transform: none; }
}

/* Fog moves like weather, not like a conveyor: a minute to cross the frame,
   swelling slightly as it goes. */
@keyframes intro-fog-drift {
  from { transform: translate3d(-6%, 1.5%, 0) scale(1.05); }
  to   { transform: translate3d(6%, -1.5%, 0) scale(1.13); }
}

/* Leaving: odd masses clear left, even masses clear right, all of them lifting
   and expanding at once. This is the "opening through the mist". */
.intro.is-leaving .intro-fog-layer {
  animation-play-state: paused;
  opacity: 0;
  transform: translate3d(calc(var(--f) * 10% - 34%), -14%, 0) scale(1.55);
}
.intro.is-leaving .intro-fog-layer:nth-child(even) {
  transform: translate3d(calc(34% + var(--f) * 10%), -10%, 0) scale(1.55);
}


/* One fast front that rakes across as the curtain opens — the gust that
   finally shifts the cloud. */
.intro-sweep {
  position: absolute;
  inset: -20% -60%;
  z-index: 5;
  pointer-events: none;
  background: linear-gradient(100deg,
    transparent 34%,
    color-mix(in oklab, var(--c-mist) 40%, transparent) 50%,
    transparent 66%);
  filter: blur(26px);
  opacity: 0;
}
.intro.is-leaving .intro-sweep { animation: intro-sweep-through 1150ms var(--ease-out) both; }
@keyframes intro-sweep-through {
  0%   { opacity: 0; transform: translate3d(-42%, 0, 0); }
  28%  { opacity: 1; }
  100% { opacity: 0; transform: translate3d(42%, 0, 0); }
}

/* Two dense half-curtains that draw back from the centre line. They carry the
   "opening" read; the fog banks alone only ever look like a dissolve. */
.intro-part {
  position: absolute;
  top: -10%; bottom: -10%;
  width: 62%;
  z-index: 5;
  pointer-events: none;
  opacity: 0;
  filter: blur(34px);
  animation: intro-part-in 1600ms var(--ease-out) 200ms both;
  transition:
    transform 1500ms cubic-bezier(.3, .7, .2, 1),
    opacity 1200ms var(--ease-out) 160ms;
}
.intro-part--l {
  left: -12%;
  background: linear-gradient(to right,
    color-mix(in oklab, var(--c-mist) 22%, transparent) 30%,
    transparent 100%);
}
.intro-part--r {
  right: -12%;
  background: linear-gradient(to left,
    color-mix(in oklab, var(--c-mist) 22%, transparent) 30%,
    transparent 100%);
}
@keyframes intro-part-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.intro.is-leaving .intro-part { opacity: 0; }
.intro.is-leaving .intro-part--l { transform: translate3d(-88%, 0, 0); }
.intro.is-leaving .intro-part--r { transform: translate3d(88%, 0, 0); }

/* The wordmark condenses out of the fog rather than fading in over it, so it
   sits above the banks and sharpens instead of arriving. */
.intro-word {
  position: relative;
  z-index: 6;
  text-align: center;
  opacity: 0;
  animation: intro-word-condense 2200ms var(--ease-out) 1500ms both;
  transition: opacity 700ms var(--ease-out), filter 900ms var(--ease-out);
}
@keyframes intro-word-condense {
  from { opacity: 0; filter: blur(22px); transform: scale(1.06); }
  60%  { opacity: 1; }
  to   { opacity: 1; filter: blur(0); transform: none; }
}
.intro.is-leaving .intro-word { opacity: 0; filter: blur(14px); }
.intro-name {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(1.9rem, 6vw, 3.4rem);
  letter-spacing: 0.01em;
  color: var(--c-text);
}
.intro-tag {
  display: block;
  margin-top: var(--sp-3);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--c-text-mute);
}

.intro-skip {
  position: absolute;
  z-index: 4;
  right: var(--gutter);
  bottom: var(--sp-5);
  padding: 0.6em 1.2em;
  border: 1px solid var(--c-line);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--c-text-mute);
  opacity: 0;
  /* Appears early — a long arrival needs a visible way out of it. */
  animation: fade-rise 700ms var(--ease-out) 900ms both;
  transition: color var(--t-fast), border-color var(--t-fast);
}
.intro-skip:hover { color: var(--c-accent); border-color: var(--c-accent); }

/* Hold the page still underneath, then let it settle in. */
body.intro-running { overflow: hidden; }
body.intro-running main { opacity: 0; }
body.intro-done main { animation: settle 900ms var(--ease-out) both; }
@keyframes settle {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}

/* ---- Konami easter egg: the stampede ------------------------------------ */

.stampede {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 90;
  overflow: hidden;
}
.stampede .critter {
  bottom: var(--b, 10vh);
  opacity: .9;
  color: var(--c-accent);
  animation: critter-cross var(--dur, 3.2s) linear var(--delay, 0s) 1 both;
}

.egg-toast {
  position: fixed;
  left: 50%;
  bottom: var(--sp-6);
  translate: -50% 0;
  z-index: 95;
  padding: var(--sp-4) var(--sp-6);
  border-radius: var(--r-pill);
  background: var(--c-accent);
  color: #14100a;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  box-shadow: var(--shadow-lg);
  animation: toast-pop 3600ms var(--ease-spring) both;
}
@keyframes toast-pop {
  0%   { transform: translateY(140%) scale(.85); opacity: 0; }
  12%  { transform: none; opacity: 1; }
  85%  { transform: none; opacity: 1; }
  100% { transform: translateY(140%) scale(.9); opacity: 0; }
}

/* ---- Mist page transition -----------------------------------------------
   Same weather as the arrival curtain. Fog closes over the page, the swap
   happens under it, and it parts again on the far side — so leaving one view
   and arriving at the next is a single continuous movement.

   The resting state is *parted*. `.is-closed` brings the halves together;
   `.is-open` returns to rest and fades the whole thing out.
   -------------------------------------------------------------------------- */

.veil {
  position: fixed;
  inset: 0;
  z-index: 200;
  pointer-events: none;
  visibility: hidden;
}
.veil.is-closed, .veil.is-open { visibility: visible; }
/* Nothing left to composite once it's cleared. */
.veil.is-open { transition: visibility 0s linear 1100ms; }

/* Base scrim, so the page underneath is genuinely obscured at the peak
   rather than merely hazy. */
.veil::before {
  content: "";
  position: absolute;
  inset: 0;
  background: color-mix(in oklab, var(--c-bg-sink) 78%, transparent);
  opacity: 0;
  transition: opacity 520ms var(--ease-out);
}
.veil.is-closed::before { opacity: 1; }
.veil.is-open::before { opacity: 0; transition-duration: 900ms; }

/* Two half-curtains drawing together and back apart. */
.veil-half {
  position: absolute;
  top: -10%; bottom: -10%;
  width: 62%;
  filter: blur(30px);
  will-change: transform;
  transition: transform 820ms cubic-bezier(.3, .7, .2, 1);
}
.veil-half--l {
  left: -12%;
  background: linear-gradient(to right,
    color-mix(in oklab, var(--c-mist) 30%, transparent) 30%, transparent 100%);
  transform: translate3d(-92%, 0, 0);
}
.veil-half--r {
  right: -12%;
  background: linear-gradient(to left,
    color-mix(in oklab, var(--c-mist) 30%, transparent) 30%, transparent 100%);
  transform: translate3d(92%, 0, 0);
}
.veil.is-closed .veil-half { transform: none; }

/* Cloud masses that thicken over the whole frame, then lift. Same
   fractal-noise filters as the arrival curtain — one texture for the whole
   site, so a transition and the intro are obviously the same weather. */
.veil-bank {
  position: absolute;
  left: -40%; right: -40%;
  height: 96%;
  top: calc(var(--f) * 12% - 16%);
  color: var(--c-mist);
  opacity: 0;
  transform: translate3d(0, 8%, 0) scale(1.04);
  will-change: transform, opacity;
  transition:
    opacity 560ms var(--ease-out) calc(var(--f) * 45ms),
    transform 860ms var(--ease-out);
}
.veil.is-closed .veil-bank { opacity: .9; transform: none; }
.veil.is-closed .veil-bank:nth-child(even) { transform: scale(1.08); }
.veil.is-open .veil-bank {
  opacity: 0;
  transform: translate3d(calc(var(--f) * 14% - 20%), -14%, 0) scale(1.4);
  transition-duration: 900ms, 1050ms;
}
.veil.is-open .veil-bank:nth-child(even) {
  transform: translate3d(calc(20% + var(--f) * 14%), -10%, 0) scale(1.4);
}

/* The gust that clears it, matching the intro's parting sweep. */
.veil-sweep {
  position: absolute;
  inset: -20% -60%;
  pointer-events: none;
  background: linear-gradient(100deg,
    transparent 34%,
    color-mix(in oklab, var(--c-mist) 36%, transparent) 50%,
    transparent 66%);
  filter: blur(24px);
  opacity: 0;
}
.veil.is-open .veil-sweep { animation: veil-sweep-through 1000ms var(--ease-out) both; }
@keyframes veil-sweep-through {
  0%   { opacity: 0; transform: translate3d(-40%, 0, 0); }
  30%  { opacity: 1; }
  100% { opacity: 0; transform: translate3d(40%, 0, 0); }
}

/* ---- Scroll progress hairline ------------------------------------------- */

.scroll-progress {
  position: fixed;
  top: 0; left: 0;
  height: 2px;
  width: 100%;
  transform-origin: left;
  transform: scaleX(var(--p, 0));
  background: linear-gradient(90deg, var(--c-accent), var(--c-second));
  z-index: calc(var(--z-header) + 1);
  pointer-events: none;
  transition: transform 90ms linear;
}

/* ---- Image loading shimmer ---------------------------------------------- */

.photo-card-frame::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, transparent 25%, rgba(255,255,255,.06) 45%, transparent 65%);
  transform: translateX(-100%);
  animation: shimmer 1900ms var(--ease-in-out) infinite;
  pointer-events: none;
}
.photo-card.is-loaded .photo-card-frame::after { display: none; }
@keyframes shimmer { to { transform: translateX(100%); } }

/* ---- Reduced motion: switch it all off ---------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .reveal, .gallery-item, .hero-headline .line > span, .reveal-words .word > span {
    opacity: 1 !important;
    transform: none !important;
  }
  /* The contour itself stays (it reads as structure); only its drift stops. */
  .critter-lane, .bird-flock, .motes, .stampede, .cursor-glow,
  .scroll-hint-line, .ticker { display: none !important; }
  .topo-track { animation: none !important; width: 100% !important; }
  .topo-track svg:last-child { display: none; }
  .hero-slide { opacity: 0; }
  .hero-slide.is-active { opacity: 1; }
  .photo-card img { opacity: 1; transform: none; }
  .photo-card-frame::after { display: none; }
}

/* Manual off-switch — the "calm mode" toggle in the header */
body.calm .critter-lane,
body.calm .bird-flock,
body.calm .motes,
body.calm .cursor-glow,
body.calm .topo-track,
body.calm .ticker-track,
body.calm .ticker-fog-layer { animation: none; }
body.calm .critter-lane,
body.calm .bird-flock,
body.calm .motes,
body.calm .cursor-glow { display: none; }
body.calm .tilt { transform: none !important; }
