/* ── WALK THE FOREST — animated role-screen scene ──
   Keyframes ported verbatim from the design prototype ("Walk the Forest
   v2.dc.html"); everything else (scene container, pins, panel, controls,
   role-screen bubbles) is new, written to this app's own conventions.
   See walk-forest.js for the JS that renders into #wf-scene. */

/* ── idle-motion grammar ──
   Every ambient loop below shares --wf-beat and its harmonics (tokens.css)
   for its duration, and --ease-idle for oscillating motion, so a
   frame-energy autocorrelation across the scene resolves to one dominant
   period instead of the 20 distinct durations across 4 easing families
   this file had before. Where two elements previously differed in
   duration for a *reason* (parallax depth, or one glow counter-phasing
   another), that reason is preserved by keeping them on the SAME harmonic
   and varying animation-delay instead — variety comes from phase, never
   from picking a different rung of the scale. The few exceptions that
   stay off-grammar on purpose (a locomotion cycle, a one-shot transition,
   a deliberately eye-catching ping) are labelled as such at their
   declaration, not silently left looking like an oversight. */
@keyframes wfBob { 0%,100% { transform:translateY(0) rotate(-0.25deg); } 50% { transform:translateY(-1.1%) rotate(0.25deg); } }
@keyframes wfPing { 0% { transform:scale(.62); opacity:.75; } 100% { transform:scale(2.15); opacity:0; } }
@keyframes wfMote { 0% { transform:translate3d(0,0,0); opacity:0; } 20% { opacity:.7; } 100% { transform:translate3d(70px,-38px,0); opacity:0; } }
/* A falling leaf tumbles, it doesn't just drop — rotation alongside the
   fall reads as "leaf" rather than "falling object." One-way travel like
   wfMote/wfSmoke (linear, opacity ramp at both ends hides the wrap), not
   an oscillation. All three .wf-leaf instances share this one keyframe,
   so every leaf drifts the same direction as it falls — a small, honest
   piece of "one wind vector" (the idle-motion grammar's own coherence
   rule): not a claim that the whole scene's wind is fully reconciled yet
   (.wf-drift and .wf-drift2 still lean opposite ways, a pre-existing
   quirk this doesn't attempt to fix), just that these three leaves don't
   contradict each other. */
@keyframes wfLeafFall {
  0%   { transform:translate(0,0) rotate(-8deg); opacity:0; }
  10%  { opacity:.55; }
  55%  { transform:translate(22px,72px) rotate(120deg); }
  88%  { opacity:.3; }
  100% { transform:translate(38px,136px) rotate(255deg); opacity:0; }
}
/* Event class (per the scene's own admission rule), not ambient idle —
   gated to appear only at the two stations whose own text is literally
   about noticing things overhead (senses: "find five things you can
   see"; soundscape: "mark where sounds come from" — see
   test/idle-scene-audit.md's Part E for the full reasoning), and only
   while no detail panel is open. --wf-beat-1200 (78s) reused as-is, not
   a new token — most of each cycle the ring is off-frame/invisible
   (0-91%), so it crosses roughly once a minute rather than continuously,
   which is what makes it a rare thing to notice rather than a tic.
   ease-in-out (not linear, unlike every other one-way-travel loop above)
   is deliberate: a glide that eases in and out of its crossing reads as
   a living thing choosing to move, not a particle drifting at constant
   velocity. */
@keyframes wfBirdCross {
  0%, 91% { transform:translate(0,0); opacity:0; }
  93%     { opacity:.55; }
  95.5%   { transform:translate(45vw,-3vh); opacity:.55; }
  98%     { transform:translate(80vw,-1vh); opacity:.35; }
  100%    { transform:translate(94vw,1vh); opacity:0; }
}
@keyframes wfSway { 0%,100% { transform:rotate(-0.55deg); } 50% { transform:rotate(0.55deg); } }
.wf-bob { animation: wfBob var(--wf-beat-100) var(--ease-idle) infinite; }
/* Named exception (armed-pin ping): deliberately allowed to draw the eye
   to an interactive target, per the scene's own admission rule for motion
   — everything else here is subordinate to foreground content, this is
   the one interaction cue that's supposed to be noticed. Left off the
   shared beat on purpose so it doesn't blend into the ambient rhythm. */
.wf-ping { transform-box: fill-box; transform-origin: center; animation: wfPing 4.6s ease-out infinite; }
/* One-way travel, not an oscillation — linear velocity is correct here
   (constant drift), and the wrap is hidden by the keyframe's own opacity
   ramp at both ends rather than by easing. 19s was already almost exactly
   this rung (--wf-beat-300, 19.5s). */
.wf-mote { animation: wfMote var(--wf-beat-300) linear infinite; }
.wf-leaf { opacity:0; animation: wfLeafFall var(--wf-beat-300) linear infinite; }
/* Base opacity 0 on the class itself (not an inline starting value, the
   way .wf-mote uses) is deliberate: under reduced motion (disabled
   below), a mote frozen at its own static opacity still reads fine as
   ambient dust, but a leaf frozen mid-tumble at a random angle reads as
   broken, not restful — so it should go fully absent, not freeze. */
.wf-bird { opacity:0; animation: wfBirdCross var(--wf-beat-1200) ease-in-out infinite; }
.wf-bird-gate {
  position:absolute; opacity:0; pointer-events:none;
  transition: opacity .6s var(--ease-idle);
}
#wf-scene[data-wf-station="senses"]:not(.wf-panel-open) .wf-bird-gate,
#wf-scene[data-wf-station="soundscape"]:not(.wf-panel-open) .wf-bird-gate {
  opacity:1;
}
.wf-sway { transform-box: fill-box; transform-origin: bottom center; animation: wfSway var(--wf-beat-200) var(--ease-idle) infinite; }
/* Locomotion, not ambient idle — tied to the walker's own travel speed
   (only runs while WF.mode is 'move'), so it's exempt from the shared
   beat by design, the same way the admission rule separates "locomotion"
   from "ambient idle" as different motion classes. */
@keyframes wfThigh { 0%,100% { transform:rotate(12deg); } 50% { transform:rotate(-12deg); } }
.wfThighA { transform-box: fill-box; transform-origin: top center; animation: wfThigh 1.9s ease-in-out infinite; }
.wfThighB { transform-box: fill-box; transform-origin: top center; animation: wfThigh 1.9s ease-in-out infinite reverse; }
@keyframes wfShin { 0% { transform:rotate(-5deg); } 22% { transform:rotate(-5deg); } 44% { transform:rotate(28deg); } 62% { transform:rotate(6deg); } 100% { transform:rotate(-5deg); } }
.wfShinA { transform-box: fill-box; transform-origin: top center; animation: wfShin 1.9s ease-in-out infinite; }
.wfShinB { transform-box: fill-box; transform-origin: top center; animation: wfShin 1.9s ease-in-out infinite reverse; }
@keyframes wfFlick { 0%,100% { transform:scale(1,1); opacity:.95; } 40% { transform:scale(.9,1.24); opacity:1; } 70% { transform:scale(1.08,.88); opacity:.82; } }
.wfFlick { transform-box: fill-box; transform-origin: bottom center; animation: wfFlick var(--wf-beat-050) var(--ease-idle) infinite; }
@keyframes wfSmoke { 0% { opacity:0; transform:translateY(0) scale(.6); } 30% { opacity:.4; } 100% { opacity:0; transform:translateY(-42px) scale(1.7); } }
/* One-way travel (rising smoke) — linear, same reasoning as wf-mote. */
.wfSmoke { transform-box: fill-box; transform-origin: center; animation: wfSmoke var(--wf-beat-100) linear infinite; }
@keyframes wfHang { 0%,100% { transform:rotate(-2.6deg); } 50% { transform:rotate(2.6deg); } }
.wfHang { transform-box: fill-box; transform-origin: top center; animation: wfHang var(--wf-beat-150) var(--ease-idle) infinite; }
@keyframes wfBreath { 0%,100% { transform:scale(.94); opacity:.5; } 50% { transform:scale(1.14); opacity:.85; } }
.wfBreath { transform-box: fill-box; transform-origin: center; animation: wfBreath var(--wf-beat-150) var(--ease-idle) infinite; }
@keyframes wfLift { 0%,100% { transform:translateY(0); } 50% { transform:translateY(-7px); } }
.wfLift { transform-box: fill-box; animation: wfLift var(--wf-beat-100) var(--ease-idle) infinite; }
@keyframes wfPeg { 0%,100% { transform:rotate(-3deg); } 50% { transform:rotate(3deg); } }
.wfPeg { transform-box: fill-box; transform-origin: top center; animation: wfPeg var(--wf-beat-100) var(--ease-idle) infinite; }
/* A log still being adjusted into place (sofa) — slower and smaller than
   wfPeg's rocking, which reads as a freely-hanging object, not a settling
   one. Also reused (see .wf-carry-settle below) as the carry pose's own
   idle tell in place of a gesture swing. */
@keyframes wfSettle { 0%,100% { transform:rotate(-1deg); } 50% { transform:rotate(1deg); } }
.wfSettle { transform-box: fill-box; transform-origin: center; animation: wfSettle var(--wf-beat-150) var(--ease-idle) infinite; }
/* A tarp's near corner catching a breath of wind (bivouac) — a corner
   flutter, not a whole-object sway like wfHang, so it skews rather than
   rotates about a center. */
@keyframes wfFlap { 0%,100% { transform:skewX(0) rotate(0deg); } 50% { transform:skewX(-2deg) rotate(-1.2deg); } }
.wfFlap { transform-box: fill-box; transform-origin: top right; animation: wfFlap var(--wf-beat-100) var(--ease-idle) infinite; }
/* A quieter wfBreath for spots that want a bare hint of life rather than
   a visible pulse (checkin's sign board) — same shape, damped amplitude.
   Always paired with .wfBreath in markup; overriding just animation-name
   here (not the full shorthand) lets wfBreath's own duration/easing carry
   over normally. */
@keyframes wfBreathSoft { 0%,100% { transform:scale(.98); opacity:.4; } 50% { transform:scale(1.04); opacity:.55; } }
.wfBreath.wf-breath-soft { animation-name: wfBreathSoft; }
@media (prefers-reduced-motion: reduce) {
  /* Not covered by the shared .wfBreath{animation:none} block further
     down, despite an earlier comment here claiming it was: that block's
     shorthand reset sets animation-name back to 'none', but this rule's
     own animation-name longhand has higher specificity (two classes, 0,2,0)
     and wins regardless of which one is later in source order — verified:
     the element was left at a ~0.00001s duration, technically still
     playState 'running', not the clean stop every other class here gets.
     Same cascade-order failure class as .wf-sun/.wf-pin-chip below (this
     codebase's own comments record hitting it twice already); placed
     after its own class's declaration for the same reason those two are.
       Matches the two-class selector exactly (0,2,0), not just
     .wf-breath-soft alone (0,1,0) — a first attempt at this fix used the
     lower-specificity selector and it silently lost to the very rule it
     was meant to override, the same mistake this comment is describing. */
  .wfBreath.wf-breath-soft { animation-name: none; }
}
/* Cast-figure gestures (placing a stone, pointing at a tree, lifting a
   pole, ...) — one shared class reused everywhere a figure's arm gets a
   small looping rotation, per-instance character coming entirely from
   animation-duration/delay set inline (walk-forest.js castGesture()),
   the same idiom naming's staggered tree-sway already uses, not from a
   separate keyframe per station.
     Deliberately NOT transform-box:fill-box, unlike every other animated
   class in this file — the pivot needs to be the figure's actual
   shoulder point (an absolute coordinate in the scene's own SVG user
   space, recomputed every frame as the figure moves), not a box derived
   from the limb path's own bounding box, which for a diagonal arm
   segment doesn't sit at any of its corners. Leaving transform-box at
   its SVG default (view-box) makes the inline transform-origin px value
   walk-forest.js sets resolve directly in that same coordinate space. */
@keyframes wfGesture { 0%,100% { transform:rotate(-9deg); } 50% { transform:rotate(9deg); } }
/* Duration is the shared beat's fastest rung — per-instance variety comes
   entirely from animation-delay (walk-forest.js WF_CAST), never from a
   different duration. Previously ten cast entries each carried their own
   inline animation-duration (1.9s–4.2s, all different) — the exact
   "variety from tempo instead of phase" failure this grammar exists to
   fix; see test/motion-dossier.md for the full per-station account. */
.wfGesture { animation: wfGesture var(--wf-beat-050) var(--ease-idle) infinite; }
/* The Sit Spot's rings actually ripple outward now rather than pulsing in
   place — the station is about attention spreading from where you settle,
   and the panel illustration (VISUAL.sitspot) draws exactly that: a
   dashed ring radiating from each seated figure. Three copies at staggered
   delays (set inline per ring in walk-forest.js) make one travelling
   wave instead of three rings breathing together. */
@keyframes wfRipple {
  0%   { transform: scale(.32); opacity: 0; }
  12%  { opacity: .6; }
  100% { transform: scale(1.3); opacity: 0; }
}
.wfRipple { transform-box: fill-box; transform-origin: center; animation: wfRipple var(--wf-beat-100) ease-out infinite; }
/* Build a Tiny World: each piece drops in, holds while the world stands
   finished, then the cycle restarts — the same element-by-element
   assembly the panel illustration animates (VISUAL.tinyworld reveals
   pond, bridge, moss hill, tree, path, hut, garden in sequence). Per-piece
   animation-delay (inline, walk-forest.js) is what turns one keyframe
   into that sequence. */
@keyframes wfPlace {
  0%   { opacity: 0; transform: translateY(-7px) scale(.55); }
  7%   { opacity: 1; transform: translateY(0) scale(1); }
  86%  { opacity: 1; transform: translateY(0) scale(1); }
  100% { opacity: 0; transform: translateY(-7px) scale(.55); }
}
/* Duration retimed 16s → 13s (--wf-beat-200); walk-forest.js's place()
   delay formula for this station was updated to match (13 - n*1.3, was
   16 - n*1.6) so the six pieces keep the same proportional stagger
   within the shorter cycle. */
.wfPlace { transform-box: fill-box; transform-origin: center; animation: wfPlace var(--wf-beat-200) var(--ease-idle) infinite; }
@keyframes wfDrift { 0%,100% { transform:translateX(0); } 50% { transform:translateX(3.5%); } }
.wf-drift { animation: wfDrift var(--wf-beat-1200) var(--ease-idle) infinite; }
/* Two cloud layers, deliberately NOT the same duration: the speed
   difference is the parallax depth cue (this layer reads as nearer than
   .wf-drift, so it should appear to move faster) — collapsing both onto
   one harmonic would flatten that depth read into one layer. Still on
   the grammar's own scale (a --wf-beat-1400 rung, added specifically for
   this one deliberate case) rather than an arbitrary number. */
@keyframes wfDrift2 { 0%,100% { transform:translateX(0); } 50% { transform:translateX(-2.6%); } }
.wf-drift2 { animation: wfDrift2 var(--wf-beat-1400) var(--ease-idle) infinite; }
/* Two light-shaft glows, meant to breathe out of phase with each other —
   previously different DURATIONS (12s/15.5s) that drifted in and out of
   alignment over several minutes rather than staying reliably
   counter-phased. Same duration now; the counter-phase comes from
   animation-delay alone (a half-period offset), which is what "variety
   from phase, not tempo" means in practice for a pair that's supposed to
   stay in a fixed relationship to each other. */
@keyframes wfGlow { 0%,100% { opacity:.09; } 50% { opacity:.24; } }
.wf-glow { animation: wfGlow var(--wf-beat-200) var(--ease-idle) infinite; }
@keyframes wfGlow2 { 0%,100% { opacity:.06; } 50% { opacity:.16; } }
.wf-glow2 { animation: wfGlow2 var(--wf-beat-200) var(--ease-idle) infinite calc(var(--wf-beat-200) / 2); }
/* The funder-logo "sun" (walk-forest.js) drifts a few px on a slow, loose
   loop rather than sitting pinned in place — its own transform, not the
   positioning wrapper's, so this doesn't fight the wrapper's inline
   left:50%/translateX(-50%) centering (a CSS animation targeting the same
   property on the same element would win the cascade and cancel it). */
@keyframes wfSunDrift {
  0%   { transform: translate(0, 0); }
  25%  { transform: translate(6px, -5px); }
  50%  { transform: translate(-4px, 3px); }
  75%  { transform: translate(3px, 6px); }
  100% { transform: translate(0, 0); }
}
.wf-sun { animation: wfSunDrift var(--wf-beat-600) var(--ease-idle) infinite; }
@media (prefers-reduced-motion: reduce) {
  /* Not in the shared disable block below — that block sits before most
     of this file's later rules in source order, so an override placed
     there for a class defined further down would lose the cascade and
     silently do nothing (the same bug already hit once this session, on
     .wf-pin-chip's own reduced-motion override). */
  .wf-sun { animation: none; }
}
/* One-shot, unlike everything else above: the arrival caption chip reads
   for a moment, then drifts up and away like a released balloon rather
   than sitting there occupying the scene indefinitely. Plays once because
   the chip <div> is only ever freshly inserted into the DOM on arrival
   (wfRender() doesn't repaint again until the camera next moves — see
   wfStep() in walk-forest.js) — forwards keeps it gone (opacity:0) for
   the rest of that hold, even though showChip likely still reads true. */
@keyframes wfChipFloat {
  0%, 28% { opacity: 1; transform: translateY(0) translateX(0) rotate(0deg); }
  100% { opacity: 0; transform: translateY(-56px) translateX(8px) rotate(3deg); }
}
@media (prefers-reduced-motion: reduce) {
  .wf-bob,.wf-ping,.wf-mote,.wf-sway,.wfThighA,.wfThighB,.wfShinA,.wfShinB,
  .wfFlick,.wfSmoke,.wfHang,.wfBreath,.wfLift,.wfPeg,.wf-drift,.wf-drift2,
  .wf-glow,.wf-glow2,.wfSettle,.wfFlap,.wfGesture,.wfRipple,.wfPlace,
  .wf-leaf,.wf-bird { animation: none; }
}

/* ── scene container ──
   Capped/fluid height rather than raw 100vh: this app embeds in an
   auto-height iframe (see README "Embedding on the Odoo site") — 100vh of
   an auto-sizing iframe is circular. Uses the site's own --vh custom
   property (see ui-behaviors.js) for the same reason it's used elsewhere:
   more reliable than raw vh units on mobile browser chrome. */
.wf-scene {
  /* Two-layer UI: this is the backdrop — a persistent, always-functional
     scene (pins/panel/controls, never stripped down) behind the real
     site, which is the foreground "menu, with function inside" (every
     screen, not just one — see .container below). One button
     (#wf-toggle-btn) switches it on/off; it never takes the page over —
     z-index stays under .container's own z-index:1 (styles-base.css) so
     the header and every screen's content sit above it exactly as
     before. wf-scene--visible marks the switched-on state; index.html
     ships it in the markup (the scene starts on — see wfSetOn(true) at
     the foot of walk-forest.js) so there is no frame without it, and
     wfSetOn() takes it from there. */
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  /* The same sky and ground bands wfRender() draws as two absolutely
     positioned <div>s (walk-forest.js), restated here as one gradient so
     they are already on screen at the very first paint — before any of
     this site's ~500KB of scripts has run, let alone walk-forest.js,
     which loads near the end of that chain.
       Without this the page painted plain browser white for a couple of
     hundred milliseconds and then cut hard to the full forest scene, which
     is the load flash. The stops are the JS bands' own values: sky runs
     0→47% (#E7EEE4 → #DCE6DE at 58% of the band, i.e. 27.3% → #D3E0D6),
     ground runs 47%→100% (#B9BA9C → #A9AA8E at 38% of the band, i.e.
     67.1% → #9B9C79). Keep the two in step if either is ever retuned. */
  background: linear-gradient(180deg,
    #E7EEE4 0%,
    #DCE6DE 27.3%,
    #D3E0D6 47%,
    #B9BA9C 47%,
    #A9AA8E 67.1%,
    #9B9C79 100%);
  cursor: default;
  -webkit-user-select: none;
  user-select: none;
  display: none;
}
.wf-scene.wf-scene--visible { display: block; }
.wf-scene svg text { user-select: none; }

/* The open stop panel renders into #wf-panel-root (a body-level sibling of
   #wf-scene — see index.html), not inside #wf-scene itself. z-index on
   .wf-panel alone couldn't out-rank .container — it's only ranked against
   other #wf-scene children, since z-index on a descendant can never escape
   its own ancestor's stacking context, and #wf-scene has to stay under
   .container (z-index 0 vs 1) so the ambient/crisp backdrop never covers
   the header. Giving the panel its own root sidesteps that: #wf-panel-root
   sits above .container (z-index 2) without dragging the rest of the
   scene's full-bleed layers up with it. pointer-events:none by default so
   it's a no-op when no panel is open; the panel itself re-enables them
   (below). Still well under every real modal/overlay in the app (search
   dialog 2500, run mode 2000+, etc. — see the z-index survey across
   styles-*.css). */
#wf-panel-root {
  position: fixed;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}

/* Blurred only once you're actually inside functional content (any
   screen but the entry/role landing pair) — see wfSetDeepFromScreen()
   in walk-forest.js, called from router.js on every navigation. Crisp
   by default so the landing screens read as a clear hero; the panel (a
   sibling of .wf-blur-layer, not inside it — see wfPanelHTML() in
   walk-forest.js) always stays crisp regardless, since its real content
   needs to be legible the moment it's open. */
.wf-blur-layer { position: absolute; inset: 0; }
body.wf-scene-deep .wf-blur-layer { filter: blur(3px); }

/* top clears the site header rather than starting at the viewport's top
   edge: the header now spans the full viewport width (styles-base.css),
   so its title sits at x≈28px — exactly where this chip used to be, and
   the two overlapped as unreadable stacked text. Same reasoning as the
   ≤768px override below, just for the desktop header's ~101px height. */
.wf-title-chip {
  position: absolute; left: 14px; top: 112px;
  display: flex; flex-direction: column; gap: 3px;
  padding: 8px 14px 9px; border-radius: 14px;
  background: rgba(251,249,244,0.82);
  pointer-events: none; z-index: 400;
}
.wf-title-chip-main { font: 600 15px/1.2 'Montserrat', sans-serif; color: #14302A; letter-spacing: -0.01em; }
.wf-title-chip-sub { font: 400 10.5px/1.3 'Open Sans', sans-serif; letter-spacing: .1em; text-transform: uppercase; color: #4D6359; }
@media (max-width: 768px) {
  /* #wf-scene is a fixed viewport-relative layer (see index.html), so on
     narrow widths — where the site's own persistent header wraps to
     ~150px tall — this needs to duck below it explicitly rather than
     assuming it starts below the header the way it did when nested
     inside #role-screen. 220px, not ~150px, because this same breakpoint
     (matched to styles-responsive.css's own ≤768px "phone density pass")
     also puts `body { zoom: 0.75 }` on everything below it, including this
     element — so a raw offset has to target 150/0.75 ≈ 200px to land at
     the header's real 150px rendered height, plus a bit of clearance. */
  .wf-title-chip { top: 220px; }
}

/* Trail stop pins */
.wf-pin-btn {
  position: absolute;
  display: flex; align-items: center; justify-content: center;
  border: none; background: none; padding: 0; border-radius: 999px;
  cursor: pointer; transition: transform .18s;
}
.wf-pin-btn:hover, .wf-pin-btn:focus-visible { transform: scale(1.09); }
.wf-pin-btn:focus-visible { outline: 2px solid #234A3E; outline-offset: 2px; }
.wf-pin-disc {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px; border: 2px solid #FBF9F4; background: #FBF9F4;
  box-shadow: 0 4px 12px rgba(20,48,42,0.18);
  transition: border-color .18s;
}
.wf-pin-chip {
  padding: 9px 12px; background: #FBF9F4; border: 1px solid #C8D8D0;
  border-radius: 10px; box-shadow: 0 6px 18px rgba(20,48,42,0.14);
  pointer-events: none;
  animation: wfChipFloat 3.6s ease-in forwards;
}
/* Placed here, not in the shared reduced-motion block up top (styles-walk-
   forest.css:41-45) — that block sits before every class it disables, but
   .wf-pin-chip's own animation is declared below it in source order, so
   putting the override there too would lose the cascade (later same-
   specificity rule wins) and silently do nothing. */
@media (prefers-reduced-motion: reduce) {
  .wf-pin-chip { animation: none; }
}
.wf-pin-chip-name { font: 600 12.5px/1.3 'Montserrat', sans-serif; color: #14302A; }
.wf-pin-chip-sub { margin-top: 2px; font: 400 10px/1.3 'Open Sans', sans-serif; color: #7C8E85; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Controls (back / restart / next / calm) */
.wf-controls { position: absolute; display: flex; align-items: center; gap: 10px; z-index: 400; }
.wf-ctrl-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: 999px;
  border: 1px solid #C8D8D0; background: #FFFFFF; color: #4D6359;
  cursor: pointer; box-shadow: 0 1px 2px rgba(20,48,42,0.06);
  transition: border-color .15s, color .15s;
}
.wf-ctrl-btn:hover, .wf-ctrl-btn:focus-visible { border-color: #7FA396; color: #234A3E; }
.wf-status-pill {
  font: 400 10.5px/1.4 'Open Sans', sans-serif; color: #4D6359;
  background: rgba(251,249,244,0.82); border-radius: 999px; padding: 6px 12px;
  white-space: nowrap;
}

.wf-rail {
  position: absolute; display: flex; align-items: flex-end; height: 12px;
  padding: 8px 12px; background: rgba(251,249,244,0.82); border-radius: 999px;
  z-index: 400;
}
/* Base button reset — every other visual property (width/height/color/
   margin) is written per-item as inline style in wfSyncRail(), computed
   in wfComputeFrame(); this class only supplies what a <button> needs
   that isn't already in that inline string. */
.wf-rail-btn {
  display: block; border: none; padding: 0; cursor: pointer;
  box-shadow: 0 0 0 3px transparent; transition: box-shadow .15s, height .25s;
}
.wf-rail-btn:hover, .wf-rail-btn:focus-visible { box-shadow: 0 0 0 3px rgba(35,74,62,0.25); }
.wf-rail-btn:focus-visible { outline: none; }

.wf-list-link {
  position: absolute; z-index: 400;
  font: 400 10.5px/1.4 'Open Sans', sans-serif; color: #4D6359;
  text-decoration: none; border-bottom: 1px dotted #7FA396;
  background: rgba(251,249,244,0.82); padding: 4px 7px; border-radius: 4px;
}
.wf-list-link:hover { color: #B8552E; }

/* Detail panel */
.wf-panel {
  position: absolute; display: flex; flex-direction: column;
  background: #FBF9F4; z-index: 500;
  pointer-events: auto;
}
.wf-panel--side {
  right: 0; top: 0; bottom: 0; width: min(430px, 42%);
  box-shadow: -12px 0 32px rgba(20,48,42,0.14);
}
.wf-panel--sheet {
  left: 0; right: 0; bottom: 0; max-height: 78%;
  border-top-left-radius: 16px; border-top-right-radius: 16px;
  box-shadow: 0 -12px 32px rgba(20,48,42,0.18);
}
.wf-panel-close {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; margin: -8px -8px 0 0;
  border: none; background: none; color: #7C8E85;
  font: 300 22px/1 'Open Sans', sans-serif; cursor: pointer; border-radius: 6px;
}
.wf-panel-close:hover, .wf-panel-close:focus-visible { color: #1B2A24; background: #F4F1EA; }
.wf-tag-chip {
  font: 500 9.5px/1 'Open Sans', sans-serif; letter-spacing: .08em; text-transform: uppercase;
  color: #4D6359; background: #F4F1EA; border-radius: 999px; padding: 7px 11px;
}
.wf-panel-label { font: 600 9.5px/1 'Open Sans', sans-serif; letter-spacing: .13em; text-transform: uppercase; color: #7FA396; }
.wf-panel-text { margin-top: 6px; font: 300 13.5px/1.62 'Open Sans', sans-serif; color: #1B2A24; }
.wf-panel-text-example {
  margin-top: 8px; padding: 12px 14px; background: #F4F1EA; border-radius: 10px;
  font: 300 italic 13.5px/1.6 'Open Sans', sans-serif; color: #234A3E;
}
.wf-panel-foot {
  padding: 12px 22px 18px; font: 400 10.5px/1.5 'Open Sans', sans-serif; color: #7C8E85;
}

/* Session strip — progression (add-to-session) + export, collapsed by
   default: the "clever trick" is keeping the real Session Builder state
   (pbSession/pbToggleInSession) and the real export pipeline
   (exportRunPDF/exportRunPNG) reachable from inside the scene's own panel
   without duplicating either, via a one-line summary that expands. */
.wf-session-strip { margin: 4px 22px 0; border-top: 1px solid #EDEAE0; }
.wf-session-toggle {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; padding: 12px 0; border: none; background: none; cursor: pointer;
  font: 500 11.5px/1.3 'Open Sans', sans-serif; color: #234A3E; text-align: left;
}
.wf-session-toggle:hover, .wf-session-toggle:focus-visible { color: #B8552E; }
.wf-session-drawer { display: flex; flex-direction: column; gap: 8px; padding-bottom: 12px; }
.wf-session-add {
  align-self: flex-start; padding: 7px 14px; border-radius: 999px;
  border: 1px solid #7FA396; background: #FFFFFF; color: #234A3E;
  font: 500 11px/1 'Open Sans', sans-serif; cursor: pointer;
}
.wf-session-add.in-pbSession { background: #234A3E; border-color: #234A3E; color: #FFFFFF; }
.wf-session-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.wf-session-export {
  padding: 7px 12px; border-radius: 8px; border: 1px solid #C8D8D0; background: #FFFFFF;
  color: #4D6359; font: 600 10.5px/1 'Open Sans', sans-serif; letter-spacing: .04em;
  cursor: pointer;
}
.wf-session-export:hover:not(:disabled), .wf-session-export:focus-visible:not(:disabled) { border-color: #7FA396; color: #234A3E; }
.wf-session-export:disabled { opacity: .4; cursor: not-allowed; }
.wf-session-open {
  font: 500 10.5px/1 'Open Sans', sans-serif; color: #4D6359; text-decoration: none;
  border-bottom: 1px dotted #7FA396; padding-bottom: 1px;
}
.wf-session-open:hover { color: #B8552E; }

/* ── global on/off toggle ──
   One small button (#wf-toggle-btn, in the header) — off is "clean
   mode" (no scene at all); on brings the backdrop up behind the site,
   which stays exactly as functional as it always was: header, nav,
   search, mode switch, language bar all keep working untouched (see
   wfSetOn()/wfToggleGlobal() in walk-forest.js). */
.wf-toggle-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0;
  border-radius: 999px; border: 1px solid var(--forest-mist);
  background: var(--paper-pure); color: var(--ink-soft);
  cursor: pointer; transition: border-color .15s, color .15s, background .15s;
}
.wf-toggle-btn:hover, .wf-toggle-btn:focus-visible { border-color: var(--forest-soft); color: var(--forest-deep); }
.wf-toggle-btn[aria-pressed="true"] { background: var(--forest-ink); border-color: var(--forest-ink); color: #FFFFFF; }

/* Once the backdrop is on, every screen's cards go translucent (same
   token-override trick as before: --paper/--paper-card/--paper-pure
   feed dozens of existing card styles across every stylesheet, so
   overriding them here is one change instead of dozens), and the
   header's muted text tokens darken — --ink-soft/--ink-faint default to
   fairly light grays, fine over solid paper but too low-contrast once
   there's a busy scene behind them instead. */
body.wf-scene-on {
  --paper:      rgba(244,241,234,0.88);
  --paper-card: rgba(251,249,244,0.88);
  --paper-pure: rgba(255,255,255,0.92);
  --ink-soft:   #14302A;
  --ink-faint:  #234A3E;
}
