/* ==========================================================================
   MCPA — motion and scene layer

   Two jobs:
   1. The land-release and risk-education diagrams. Both used to be drawn for
      the light theme and animated by GSAP timelines keyed to individual
      element ids, so any edit to the artwork broke the animation. They are
      now plain SVG whose motion is entirely CSS, gated on a single class
      (.on for a land-release stage, .fig-on for a risk-education figure).
      Redraw the artwork freely; the animation follows the class, not the ids.
   2. Transitions: first paint, section entrance, and the wipe used when a
      navigation link jumps between sections.

   Every animation is inside a prefers-reduced-motion guard at the end.
   ========================================================================== */

:root {
  --mo-fast: .38s;
  --mo-mid: .7s;
  --mo-slow: 1.1s;
  --mo-ease: cubic-bezier(.22,.61,.36,1);
  --mo-ease-out: cubic-bezier(.16,1,.3,1);
}

/* ==========================================================================
   1. Land release stage
   ========================================================================== */
.lr-svg { width: 100%; height: 100%; display: block; overflow: visible; }

/* ---- the ground, shared by all five stages ----
   Drawn once and never re-animated, so the plot stays put while the stages
   change over it. The two slab faces differ by about 40% of their lightness,
   which is what makes the block turn a corner rather than read as an
   outline. */
.lr-plain { fill: url(#lrPlain); }
.lr-face  { fill: url(#lrGround); }
.lr-slab-left  { fill: #141917; }
.lr-slab-right { fill: #1D2422; }
.lr-mesh path { fill: none; stroke: rgba(255,255,255,.055); stroke-width: 1; }

.scene {
  opacity: 0;
  visibility: hidden;
  transition: opacity .5s var(--mo-ease), visibility 0s linear .5s;
}
.scene.on {
  opacity: 1;
  visibility: visible;
  transition: opacity .5s var(--mo-ease), visibility 0s linear 0s;
}

/* ---- the state of the ground ----
   A wash laid over the shared surface rather than a fill replacing it, so the
   gradient and the tile mesh stay readable underneath at every stage. */
.lr-tint { stroke: none; }
.lr-tint-suspect  { fill: rgba(255,95,58,.16); }
.lr-tint-released { fill: rgba(52,206,138,.13); }

.lr-chip {
  font-family: var(--f-mono); font-size: 11px; letter-spacing: .16em;
  fill: var(--muted); text-transform: uppercase;
}
.lr-chip-ok { fill: var(--clay-deep); }
.lr-chip-warn { fill: var(--hazard); font-size: 9.5px; letter-spacing: .13em; }

.lr-mark path { fill: var(--hazard); }
.lr-mark-shadow { fill: rgba(0,0,0,.42); }
.lr-mark { opacity: 0; }

/* ---- scene 1: non-technical survey ---- */
.lr-outline {
  fill: none; stroke: var(--hazard); stroke-width: 2;
  stroke-dasharray: 8 7;
}
.lr-node-ring { fill: none; stroke: var(--clay); stroke-width: 1.5; opacity: .5; }
.lr-hut { fill: rgba(52,206,138,.22); stroke: var(--clay); stroke-width: 1.3; stroke-linejoin: round; }
.lr-thread {
  fill: none; stroke: var(--clay-deep); stroke-width: 1.4;
  stroke-dasharray: 3 5; opacity: .75;
}

/* ---- scene 2: technical survey ---- */
.lr-lane { fill: rgba(52,206,138,.20); stroke: rgba(52,206,138,.38); stroke-width: 1; }
.lr-confirmed { fill: rgba(255,95,58,.26); stroke: var(--hazard); stroke-width: 1.6; }

/* ---- scene 3: clearance ---- */
.lr-cell { fill: rgba(255,95,58,.20); stroke: rgba(255,95,58,.26); stroke-width: .8; }
.lr-sweep { fill: url(#lrSweepGrad); }

/* ---- scene 4: quality assurance ---- */
.lr-sample path { fill: rgba(52,206,138,.10); stroke: var(--clay); stroke-width: 1.6; }
.lr-tick { fill: none; stroke: #fff; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
.lr-scan { stroke: var(--clay); stroke-width: 2.5; opacity: .6; fill: none; stroke-linecap: round; }

/* ---- scene 5: handover ---- */
.lr-furrow { stroke: rgba(52,206,138,.34); stroke-width: 1.6; fill: none; }
.lr-wheat path { stroke: #9CB268; stroke-width: 2; fill: none; stroke-linecap: round; }
.lr-wheat circle { fill: #E0B368; }
.lr-road { stroke: #6E6659; stroke-width: 5.5; stroke-linecap: round; fill: none; }
.lr-flag path { stroke: var(--clay); stroke-width: 2.4; fill: none; }
.lr-flag path:last-child { fill: var(--clay); }

/* ==========================================================================
   2. Risk education figures
   ========================================================================== */
.eo-svg { width: 100%; height: auto; display: block; overflow: visible; }

.eo-plane { fill: rgba(255,255,255,.04); stroke: var(--line-strong); stroke-width: 1.2; }
.eo-grid { stroke: var(--line); stroke-width: 1; }
/* None of these carry a static `opacity: 0` any more. The entrance animations
   use `animation-fill-mode: both`, which already holds the `from` state
   through the delay — so the hiding is the animation's job. Doing it twice
   meant that if the observer never ran, or the visitor asked for reduced
   motion, the element stayed invisible permanently instead of simply being
   there. Artwork should degrade to "drawn", never to "absent". */
.eo-risk { fill: var(--hazard-wash); stroke: var(--hazard); stroke-width: 1.3; }
.eo-person circle { fill: var(--clay); }
.eo-person path { fill: none; stroke: var(--clay); stroke-width: 3; stroke-linecap: round; }
.eo-link { fill: none; stroke: var(--clay-deep); stroke-width: 1.4; stroke-dasharray: 3 5; }
.eo-tag {
  font-family: var(--f-mono); font-size: 10px; letter-spacing: .15em;
  fill: var(--muted); text-transform: uppercase;
}

.eo-hub circle { fill: none; stroke: var(--clay); stroke-width: 1.8; }
.eo-hub path { stroke: var(--clay); stroke-width: 1.8; stroke-linecap: round; }
.eo-ray { fill: none; stroke: var(--clay-deep); stroke-width: 1.5; }
.eo-card rect { fill: rgba(255,255,255,.045); stroke: var(--line-strong); stroke-width: 1; }
.eo-card text { font-family: var(--f-mono); font-size: 11px; letter-spacing: .13em; fill: var(--ink-soft); }

/* Buildings, and the routes between them. Roofs and openings take the brand
   green so the eye reads "a place people use"; the walls stay neutral. */
.eo-struct rect { fill: rgba(255,255,255,.06); stroke: var(--line-strong); stroke-width: 1.1; }
.eo-struct path { fill: rgba(52,206,138,.16); stroke: var(--clay); stroke-width: 1.2; }
.eo-struct circle { fill: rgba(52,206,138,.10); stroke: var(--clay); stroke-width: 1.2; }
.eo-struct-water .eo-drop { fill: rgba(52,206,138,.18); stroke: var(--clay-deep); stroke-width: 1.3; }
/* Distance, done with weight rather than with perspective. */
.eo-far { opacity: .5; }
.eo-route { fill: none; stroke: var(--clay-deep); stroke-width: 1.6; stroke-dasharray: 5 6; stroke-linecap: round; }

.eo-glyph path { fill: none; stroke: var(--clay-deep); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.eo-glyph rect { fill: none; stroke: var(--clay-deep); stroke-width: 1.5; }
.eo-glyph circle { fill: var(--clay-deep); stroke: none; }

.eo-board rect { fill: rgba(255,255,255,.05); stroke: var(--line-strong); stroke-width: 1.2; }
.eo-board path { fill: none; stroke: var(--clay-deep); stroke-width: 2; stroke-linecap: round; }
.eo-track { fill: none; stroke: var(--clay-deep); stroke-width: 1.6; stroke-dasharray: 6 7; stroke-linecap: round; }
.eo-veh rect { fill: rgba(52,206,138,.32); stroke: var(--clay); stroke-width: 1.2; }
.eo-veh circle { fill: var(--clay); }

.eo-pupil circle { fill: var(--clay-deep); }
.eo-pupil path { fill: none; stroke: var(--clay-deep); stroke-width: 2.6; stroke-linecap: round; }

.eo-axis { stroke: var(--line-strong); stroke-width: 1.2; }
/* The origin has to sit on the axis or the bars grow from the wrong baseline.
   It is 152 because the axis moved up to make room for the feedback loop. */
.eo-bar { fill: rgba(52,206,138,.3); stroke: rgba(52,206,138,.55); stroke-width: 1; transform-origin: 50% 152px; }
.eo-trend { fill: none; stroke: var(--clay); stroke-width: 2.2; stroke-linecap: round; }
/* Amber, not green: the loop is the process correcting itself, not an outcome
   delivered — and it has to be legible as a separate idea from the series. */
.eo-loop { fill: none; stroke: var(--sand); stroke-width: 1.6; stroke-dasharray: 4 5; }
.eo-loop-head path { fill: var(--sand); }
.eo-tag-loop { font-size: 9px; letter-spacing: .1em; }

/* ==========================================================================
   3. Transitions
   ========================================================================== */

/* First paint: a single panel lifts off the page rather than a spinner. */
.page-veil {
  position: fixed; inset: 0; z-index: 300;
  background: var(--paper);
  transform-origin: 50% 100%;
  pointer-events: none;
}
.page-veil::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
  background: linear-gradient(90deg, transparent, var(--clay), transparent);
  transform: scaleX(0);
}

/* Section-to-section wipe, used when a nav link jumps the page. */
.nav-wipe {
  position: fixed; inset: 0; z-index: 250;
  background: var(--paper-2);
  transform: scaleY(0); transform-origin: 50% 100%;
  pointer-events: none;
  will-change: transform;
}
.nav-wipe.is-in  { animation: wipe-in .42s var(--mo-ease-out) forwards; }
.nav-wipe.is-out { animation: wipe-out .52s var(--mo-ease-out) forwards; }

@keyframes wipe-in  { from { transform: scaleY(0); transform-origin: 50% 100%; }
                      to   { transform: scaleY(1); transform-origin: 50% 100%; } }
@keyframes wipe-out { from { transform: scaleY(1); transform-origin: 50% 0; }
                      to   { transform: scaleY(0); transform-origin: 50% 0; } }

/* ==========================================================================
   4. The animations themselves — only for visitors who accept motion
   ========================================================================== */
@media (prefers-reduced-motion: no-preference) {

  /* ---- first paint ---- */
  .page-veil { animation: veil-lift 1s var(--mo-ease-out) .15s forwards; }
  .page-veil::after { animation: veil-rule .5s var(--mo-ease) forwards; }
  @keyframes veil-rule { to { transform: scaleX(1); } }
  @keyframes veil-lift { to { transform: scaleY(0); } }

  /* Hero content arrives behind the veil, staggered. */
  .hero-kicker, .hero h1, .hero-lede, .hero-actions, .hero-proof, .field-hud {
    animation: rise-in .9s var(--mo-ease-out) backwards;
  }
  .hero-kicker { animation-delay: .70s; }
  .hero h1     { animation-delay: .78s; }
  .hero-lede   { animation-delay: .90s; }
  .hero-actions{ animation-delay: 1.00s; }
  .hero-proof  { animation-delay: 1.10s; }
  .field-hud   { animation-delay: 1.25s; }
  /* No `to`, deliberately: the end state resolves from each element's own
   computed style, which is what lets one keyframe serve a hero paragraph and
   a readout panel that has to come to rest at translateY(-56%). SVG figure
   parts need the opposite guarantee and use fig-rise below. */
@keyframes rise-in { from { opacity: 0; transform: translateY(26px); } }

/* Same movement, but landing on a stated end state. Figure parts sit inside
   diagrams where the resting position must be exactly "no transform, fully
   opaque" — and where an implicit end state resolved from the element's own
   style meant anything holding a pre-animation `opacity: 0` animated from 0
   to 0 and never appeared at all. That is what had happened to the message
   format cards, the seated pupils and the figure groups. */
@keyframes fig-rise { from { opacity: 0; transform: translateY(26px); } to { opacity: 1; transform: none; } }

  /* ---- section entrances ---- */
  /* A section head resolves as a unit: rule, then label, then heading, then
     the standfirst. Every section on the page carries one of these, which is
     what makes this the single beat the whole document shares — before it,
     some sections arrived and others were simply already there.

     `fig-rise`, not `rise-in`: these land on a stated end state. rise-in
     resolves its end state from the element's own computed style, which is
     right for the hero and wrong here. */
  .sec-head .eyebrow::before { transform-origin: left; }
  .reveal.in .eyebrow::before { animation: rule-draw .5s var(--mo-ease) both; }
  @keyframes rule-draw { from { transform: scaleX(0); } to { transform: scaleX(1); } }

  .sec-head.reveal.in > .chapter-label,
  .sec-head.reveal.in > .eyebrow { animation: fig-rise .55s var(--mo-ease-out) .04s both; }
  .sec-head.reveal.in > h2 { animation: fig-rise .65s var(--mo-ease-out) .15s both; }
  /* The label is also a <p>, so it has to be excluded by name or it would be
     matched twice and take the standfirst's later delay. */
  .sec-head.reveal.in > p:not(.chapter-label):not(.eyebrow) {
    animation: fig-rise .65s var(--mo-ease-out) .27s both;
  }

  /* Cards in a grid stagger by position rather than all at once. */
  .svc-grid .reveal.in, .impact-grid .reveal.in, .news-grid .reveal.in,
  .story-grid .reveal.in, .field-grid .field-item, .eore-track .eore-panel {
    animation: rise-in .8s var(--mo-ease-out) backwards;
  }
  .field-grid .field-item:nth-child(2), .eore-track .eore-panel:nth-child(2) { animation-delay: .08s; }
  .field-grid .field-item:nth-child(3), .eore-track .eore-panel:nth-child(3) { animation-delay: .16s; }
  .field-grid .field-item:nth-child(4), .eore-track .eore-panel:nth-child(4) { animation-delay: .24s; }
  .field-grid .field-item:nth-child(5) { animation-delay: .32s; }
  .field-grid .field-item:nth-child(6) { animation-delay: .40s; }

  /* ---- one stagger, for any group that arrives as a band ----
     The page had two different ways of animating a group of cards: mark every
     card .reveal and let the observer catch each one, or mark the container
     and have the whole block appear at once. The first gives a rhythm, the
     second does not, and which one a section got was an accident of when it
     was written — partners had eleven cards arriving simultaneously while
     services had eight arriving in sequence.

     .stagger is the second one done properly: the container is what scrolls
     in, and its children follow it in order. It suits a band that fits on one
     screen. Tall grids keep per-card reveals, because a stagger fired from the
     top of a three-row grid would run its last row while it is still below
     the fold. */
  .reveal.stagger.in > * { animation: fig-rise .6s var(--mo-ease-out) both; }
  .reveal.stagger.in > :nth-child(1)  { animation-delay: .06s; }
  .reveal.stagger.in > :nth-child(2)  { animation-delay: .13s; }
  .reveal.stagger.in > :nth-child(3)  { animation-delay: .20s; }
  .reveal.stagger.in > :nth-child(4)  { animation-delay: .27s; }
  .reveal.stagger.in > :nth-child(5)  { animation-delay: .34s; }
  .reveal.stagger.in > :nth-child(6)  { animation-delay: .41s; }
  .reveal.stagger.in > :nth-child(7)  { animation-delay: .48s; }
  .reveal.stagger.in > :nth-child(8)  { animation-delay: .55s; }
  .reveal.stagger.in > :nth-child(9)  { animation-delay: .62s; }
  .reveal.stagger.in > :nth-child(10) { animation-delay: .69s; }
  .reveal.stagger.in > :nth-child(n+11) { animation-delay: .76s; }

  /* ---- the coverage map draws itself ----
     Border first, then the provinces fill in behind it, then the markers land.
     The order is the argument: this is the country, these are its provinces,
     this is where we worked. The whole panel is a .reveal and therefore
     already hidden until it scrolls in, so the pre-states below can never
     flash — and outside this media query none of them exist at all. */
  .geo-panel .afg-outline { stroke-dasharray: var(--len, 2800); }
  .geo-panel.in .afg-outline { animation: border-draw 1.5s var(--mo-ease) both; }
  @keyframes border-draw { from { stroke-dashoffset: var(--len, 2800); }
                           to   { stroke-dashoffset: 0; } }

  /* Staggered by --i, which gen-map.py writes in west-to-east order, so the
     fill sweeps across the country rather than blinking on at once. */
  .geo-panel.in .prov-cell {
    animation: fade-in .5s var(--mo-ease) calc(.45s + var(--i, 0) * .018s) both;
  }
  .geo-panel.in .prov-frontier { animation: fade-in .6s var(--mo-ease) 1.15s both; }
  .geo-panel.in .prov-marker {
    animation: marker-drop .45s var(--mo-ease-out) calc(1.2s + var(--i, 0) * .014s) both;
  }
  @keyframes marker-drop { from { opacity: 0; transform: translateY(-9px); }
                           to   { opacity: 1; transform: none; } }

  /* ---- land release scenes ----
     The plot is isometric now, so every movement that used to run along a
     screen axis has to run along a GRID axis instead: one step of a column is
     (+15.5, +7.9), one step of a row is (-15.5, +7.9). A sweep that still
     travelled straight across the screen would visibly slide off the ground.
     The distances below are those two vectors multiplied out, and they are
     the only magic numbers in this block. */
  /* Scene 1 */
  .lr-outline { stroke-dasharray: var(--len, 1000); }
  .scene-1.on .lr-outline { animation: dash-draw 1.4s var(--mo-ease) both; }
  @keyframes dash-draw { from { stroke-dashoffset: var(--len, 1000); } to { stroke-dashoffset: 0; } }
  .scene-1.on .lr-node-ring { animation: node-pulse 2.4s var(--mo-ease) var(--d, 0s) infinite; }
  @keyframes node-pulse { 0% { r: 14; opacity: .7; } 70% { r: 26; opacity: 0; } 100% { r: 26; opacity: 0; } }
  .scene-1.on .lr-hut { animation: fig-rise .5s var(--mo-ease-out) var(--d, 0s) both; }
  .scene-1.on .lr-thread { stroke-dasharray: 4 6; animation: thread-run 1.6s linear var(--d, 0s) infinite; }
  @keyframes thread-run { to { stroke-dashoffset: -40; } }
  .scene-1.on .lr-mark { animation: mark-pop .5s var(--mo-ease-out) var(--d, 0s) both; }
  @keyframes mark-pop { from { opacity: 0; transform: translateY(-8px) scale(.7); } to { opacity: 1; transform: none; } }
  .lr-mark { transform-box: fill-box; transform-origin: 50% 100%; }

  /* Scene 2. A lane is cut along its own length — two rows back, then
     forward — rather than scaled up from a screen-horizontal edge. */
  .scene-2.on .lr-lane { animation: lane-cut .8s var(--mo-ease-out) var(--d, 0s) both; }
  @keyframes lane-cut { from { transform: translate(31px, -15.8px); opacity: 0; }
                        to   { transform: none; opacity: 1; } }
  .scene-2.on .lr-confirmed { animation: box-in .7s var(--mo-ease-out) 1s both; transform-box: fill-box; transform-origin: 50% 50%; }
  @keyframes box-in { from { opacity: 0; transform: scale(.86); } to { opacity: 1; transform: none; } }
  .scene-2.on .lr-mark-live { animation: mark-pop .5s var(--mo-ease-out) var(--d, 0s) both, mark-blink 1.3s steps(1) 1.7s infinite; }
  @keyframes mark-blink { 0%, 60% { opacity: 1; } 61%, 100% { opacity: .35; } }

  /* Scene 3 */
  .scene-3.on .lr-cell { animation: cell-clear .7s var(--mo-ease) var(--d, 0s) both; }
  @keyframes cell-clear {
    0%   { fill: rgba(255,95,58,.20); stroke: rgba(255,95,58,.26); }
    55%  { fill: rgba(52,206,138,.50); stroke: rgba(52,206,138,.72); }
    100% { fill: rgba(52,206,138,.16); stroke: rgba(52,206,138,.30); }
  }
  /* Six columns of the confirmed area: 6 x (15.5, 7.9). */
  .scene-3.on .lr-sweep { animation: sweep-run 3.4s var(--mo-ease) infinite; }
  @keyframes sweep-run { from { transform: translate(0, 0); opacity: 0; }
                         10% { opacity: 1; }
                         90% { opacity: 1; }
                         to  { transform: translate(93px, 47.4px); opacity: 0; } }
  .scene-3.on .lr-mark-gone { animation: mark-pop .4s var(--mo-ease-out) both, mark-out .6s var(--mo-ease) var(--d, 0s) forwards; }
  @keyframes mark-out { to { opacity: 0; transform: translateY(-16px) scale(.5); } }

  /* Scene 4. The QA line crosses the whole plot: 12 x (15.5, 7.9). */
  .scene-4.on .lr-sample { animation: box-in .55s var(--mo-ease-out) var(--d, 0s) both; transform-box: fill-box; transform-origin: 50% 50%; }
  .scene-4.on .lr-tick { stroke-dasharray: 40; animation: tick-draw .45s var(--mo-ease) calc(var(--d, 0s) + .3s) both; }
  @keyframes tick-draw { from { stroke-dashoffset: 40; } to { stroke-dashoffset: 0; } }
  .scene-4.on .lr-scan { animation: scan-run 3s var(--mo-ease) infinite; }
  @keyframes scan-run { from { transform: translate(0, 0); opacity: 0; }
                        15% { opacity: .7; } 85% { opacity: .7; }
                        to { transform: translate(186px, 94.8px); opacity: 0; } }

  /* Scene 5 */
  .scene-5.on .lr-furrow { stroke-dasharray: var(--len, 330); animation: dash-draw-s .8s var(--mo-ease) var(--d, 0s) both; }
  @keyframes dash-draw-s { from { stroke-dashoffset: var(--len, 330); } to { stroke-dashoffset: 0; } }
  .scene-5.on .lr-wheat { animation: grow-up .6s var(--mo-ease-out) var(--d, 0s) both; transform-box: fill-box; transform-origin: 50% 100%; }
  @keyframes grow-up { from { opacity: 0; transform: scaleY(0); } to { opacity: 1; transform: scaleY(1); } }
  /* 296px of path, measured from the two grid endpoints. */
  .scene-5.on .lr-road { --len: 300; stroke-dasharray: 300; animation: dash-draw-s 1s var(--mo-ease) .2s both; }
  .scene-5.on .lr-flag { animation: mark-pop .6s var(--mo-ease-out) 1.2s both; transform-box: fill-box; transform-origin: 0 100%; }

  /* ---- risk education figures ---- */
  .fig-on .eo-risk { animation: risk-pulse 2.6s var(--mo-ease) var(--d, 0s) infinite; transform-box: fill-box; transform-origin: 50% 50%; }
  @keyframes risk-pulse { 0% { opacity: 0; transform: scale(.7); } 35% { opacity: 1; transform: scale(1); }
                          75% { opacity: .45; transform: scale(1.08); } 100% { opacity: 0; transform: scale(.7); } }
  .fig-on .eo-person { animation: fig-rise .6s var(--mo-ease-out) var(--d, 0s) both; }
  .fig-on .eo-link { stroke-dasharray: 4 6; animation: thread-run 1.4s linear infinite; }

  .fig-on .eo-ray { stroke-dasharray: 220; animation: dash-draw-s .9s var(--mo-ease) var(--d, 0s) both; }
  .fig-on .eo-card { animation: fig-rise .6s var(--mo-ease-out) var(--d, 0s) both; }

  .fig-on .eo-wave { animation: wave-out 2.4s var(--mo-ease) var(--d, 0s) infinite; transform-box: fill-box; transform-origin: 50% 100%; }
  @keyframes wave-out { 0% { opacity: 0; transform: scale(.5); } 30% { opacity: .8; } 100% { opacity: 0; transform: scale(1.25); } }
  .fig-on .eo-pupil { animation: fig-rise .55s var(--mo-ease-out) var(--d, 0s) both; }

  .fig-on .eo-bar { animation: bar-grow .8s var(--mo-ease-out) var(--d, 0s) both; }
  @keyframes bar-grow { from { transform: scaleY(0); } to { transform: scaleY(1); } }
  .fig-on .eo-trend { stroke-dasharray: 360; animation: dash-draw-s 1.1s var(--mo-ease) .5s both; }

  /* Routes and tracks march, the way the walked-route figure in the problem
     section does — same idea, so the same motion. */
  .fig-on .eo-route, .fig-on .eo-track { animation: thread-run 1.7s linear infinite; }
  .fig-on .eo-struct { animation: fig-rise .7s var(--mo-ease-out) var(--d, 0s) both; }
  .fig-on .eo-board  { animation: fig-rise .7s var(--mo-ease-out) both; }
  .fig-on .eo-veh    { animation: fig-rise .6s var(--mo-ease-out) .85s both; }
  /* Reversed, because this one runs back to the start of the series. */
  .fig-on .eo-loop      { animation: thread-run 2.4s linear infinite reverse; }
  .fig-on .eo-loop-head { animation: fade-in .5s var(--mo-ease) 1.3s both; }

  /* ---- the route past a suspected area ----
     Read in order: the route is walked every day (the dashes march), two
     hazards are marked on it, and only then does the diversion draw itself
     toward report, educate, clear. Static, the drawing stated all three at
     once and so stated none of them. */
  .problem-route.fig-on .route-line { animation: route-walk 1.9s linear infinite; }
  @keyframes route-walk { to { stroke-dashoffset: -34; } }   /* two 9+8 dashes */

  .problem-route.fig-on .route-marker circle {
    transform-box: fill-box; transform-origin: 50% 50%;
    animation: route-mark-in .5s var(--mo-ease-out) var(--d, 0s) both,
               route-mark-pulse 2.6s var(--mo-ease) calc(var(--d, 0s) + .5s) infinite;
  }
  .problem-route.fig-on .route-marker path {
    animation: fade-in .45s var(--mo-ease) calc(var(--d, 0s) + .18s) both;
  }
  .problem-route.fig-on .route-marker-b { --d: .22s; }
  @keyframes route-mark-in { from { transform: scale(.2); opacity: 0; } to { transform: scale(1); opacity: 1; } }
  @keyframes route-mark-pulse { 0%, 100% { stroke-width: 2; opacity: 1; } 50% { stroke-width: 5; opacity: .68; } }

  .problem-route.fig-on .route-diversion {
    stroke-dasharray: 530;
    animation: dash-draw-route 1.25s var(--mo-ease) .85s both;
  }
  @keyframes dash-draw-route { from { stroke-dashoffset: 530; } to { stroke-dashoffset: 0; } }

  .problem-route.fig-on .route-arrow {
    /* Grows out of the base toward the tip, so it lands as the line arrives.
       The class sits on the path, not on the <g> that positions it: a CSS
       transform on that <g> would replace its transform attribute and throw
       the arrowhead to the origin. */
    transform-box: fill-box; transform-origin: 0% 50%;
    animation: route-mark-in .4s var(--mo-ease-out) 2s both;
  }

  .problem-route.fig-on text { animation: fade-in .6s var(--mo-ease) var(--d, 0s) both; }
  .problem-route.fig-on text:nth-of-type(2) { --d: .3s; }
  .problem-route.fig-on text:nth-of-type(3) { --d: 2.1s; }
  @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
}

/* ==========================================================================
   5. Reduced motion — everything resolves to its finished state
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  .page-veil, .nav-wipe { display: none; }
  .lr-mark, .eo-person, .eo-card, .eo-pupil, .eo-risk, .eo-wave { opacity: 1; }
  .scene, .scene.on { transition: none; }
}
