/* ===========================================================================
   Municorn design system · 4 · motion
   ---------------------------------------------------------------------------
   The point of motion is that the page builds itself: parts arrive in install
   order, then it is still.

   Two rules hold this file together.

   1. Every authored offset is multiplied by var(--motion). Calm mode sets
      --motion:0, which zeroes the movement and leaves the order and timing
      intact. Anything that loops or travels is switched off explicitly at the
      bottom of the file. js/*.js multiplies its own offsets by MU.motion.scale
      for the same reason.

   2. Nothing is invisible at rest without a guaranteed reveal, and each gate is
      set by the file that clears it. html.reveal is added by js/reveal.js, which
      owns .rise and .stag; html.anime is added by js/motion.js only once the
      animation library has actually loaded, so the parts js/build.js
      choreographs are never hidden by a library that never arrived. Neither is
      set in the boot script, so a blocked or broken request leaves the page
      complete and static. Pages that load no build script — class B — keep
      .rise and lose nothing.

   The OS reduced-motion flag resolves calm mode by default — see the boot script
   in src/design/head.ts. The footer toggle overrides it in either direction.
   =========================================================================== */

/* Registering the animated custom properties keeps interpolation numeric and
   lets the browser skip a full style recalc on change. */
@property --rw{syntax:'<number>';inherits:false;initial-value:1}
@property --ro{syntax:'<number>';inherits:false;initial-value:0}
@property --fill{syntax:'<number>';inherits:false;initial-value:1}

html.calm{--motion:0}

@keyframes fadein{from{opacity:0}to{opacity:1}}
@keyframes fade{
  from{opacity:0;transform:translateY(calc(9px * var(--motion)))}
  to{opacity:1;transform:none}
}
@keyframes slideIn{
  from{opacity:0;transform:translateX(calc(-12px * var(--motion)))}
  to{opacity:1;transform:none}
}

/* === hero =================================================================
   The masked lines rise; the description and the button follow.

   The second line is delayed by the same .14s it always was. It used to be
   selected as `.l.it`, the gold italic aside; both lines are the headline now,
   so it is the second child. The staging is identical. */
.hero h1 .l>i{animation:up 1.1s var(--ease-reveal) .15s both}
.hero h1 .l:nth-child(2)>i{animation-delay:.29s}
@keyframes up{
  from{transform:translateY(calc(112% * var(--motion)))}
  to{transform:translateY(0)}
}
.hero__mark{opacity:0;animation:fade .8s var(--ease-reveal) .1s both}

/* The wall builds BEHIND text that is already there.
   These two carried an .85s and a .95s delay, which are long enough that a
   capture of the hero showed a headline over an empty slab: the description
   and the buttons were still waiting on the course sequence. That the capture
   was an entrance state rather than a load benchmark does not change the
   answer — the first thing a reader is shown should be the whole of what the
   page says, with the wall arriving underneath it.

   So the sequence stays and the waiting goes. .hero__low is one frame behind
   the mark and .hero__desc a frame behind that, which is enough for the three
   to read as an order rather than a jump, and short enough that all of it is
   on screen inside a fifth of a second. The offsets are still the --motion
   ones in `fade` and `slideIn`, so calm mode still takes the movement and
   leaves the order. */
.hero__low{animation:fade .6s var(--ease-reveal) .12s both}
.hero__desc{opacity:0;animation:slideIn .6s var(--ease-reveal) .18s both}

/* === the wall goes up =====================================================
   Courses lay in from alternating sides, top down, with a brass flash as each
   joint sets. No library: it plays even if the CDN never answers.
   While the wall is going up the base is a dark bed rather than bare mortar —
   mortar beige would read as a pale flash on load. Once built the base is plain
   mortar, since the courses cover it completely and a second fixed copy of the
   wall underneath would fight the drift. */
.js .hero__bg::before{background-image:none;background-color:var(--bed)}
.js .hero__bg.built::before{background-image:none;background-color:var(--mortar)}
.crs--lay{animation:lay .72s var(--ease-set) var(--d) both}
.crs--lay::after{animation:setjoint .5s linear calc(var(--d) + .40s) both}
@keyframes lay{
  from{transform:translateX(calc(var(--from) * var(--motion)))}
  to{transform:translateX(0)}
}
@keyframes setjoint{0%{opacity:0}22%{opacity:.85}100%{opacity:0}}

/* Ambient loop: the course drift. Once the wall is up every course drifts a couple of
   pixels on its own long cycle, so the running bond shears and closes
   continuously. Compositor-only transforms, paused whenever the hero is off
   screen. The cycle starts and ends at zero so a course picks the drift up from
   exactly where the build left it, and courses desynchronise through differing
   durations and a short positive delay rather than a negative one, which would
   drop them mid-cycle. */
.crs--drift{animation:drift var(--dur) ease-in-out var(--del) infinite}
@keyframes drift{
  0%,100%{transform:translate3d(0,0,0)}
  25%{transform:translate3d(var(--amp),0,0)}
  75%{transform:translate3d(calc(var(--amp) * -1),0,0)}
}
.courses--paused .crs--drift{animation-play-state:paused}

/* === ticker ===============================================================
   A marquee, not a reveal: it is metadata, and it pauses on hover. */
.tick__t{animation:mq 42s linear infinite}
.tick:hover .tick__t,.tick--paused .tick__t{animation-play-state:paused}
@keyframes mq{from{transform:translateX(0)}to{transform:translateX(-50%)}}

/* === the exhibits ========================================================
   The typed question, the collapsing answer body, the growing chart bars and
   the linkmark that flashed the first evidence row have all gone from here.
   Every one of them was gated on data-wait / data-answered, and js/type.js was
   the only thing that ever set either. That file went when the typewriter
   framing was dropped from the cards; these rules outlived the behaviour and
   went on being served to every visitor. Nothing on any page matches them.
   ========================================================================= */

/* === the scenario switch ==================================================
   The card walks between its two runs on its own until a reader takes it over,
   and this is the only thing that says so. A panel that changes with no warning
   and no indication of when it will change again reads as a fault; the fill is
   what turns it into a control somebody can wait out or interrupt.

   js/plan.js owns the timing and the stopping. This owns only the drawing, and
   the duration here and DWELL there are one number in two files — the fill has
   to finish exactly when the switch happens or it is telling the reader
   something false. */
@keyframes planfill{from{transform:scaleX(0)}to{transform:scaleX(1)}}
.js .plan__tabs label.is-auto::after{content:'';position:absolute;left:0;right:0;bottom:0;
  height:2px;background:var(--brass);transform-origin:left center;
  animation:planfill 5.2s linear both}
/* Calm mode never starts it — js/plan.js returns early on MU.motion.reduce —
   so this is the belt to that braces: no fill can be drawn with --motion at 0. */
html.calm .plan__tabs label.is-auto::after{display:none}

/* === generic reveals ======================================================
   .rise for a block, .stag for its children in sequence. Both driven by the
   shared observer in js/reveal.js, both caught by its scroll sweep.

   GATED ON html.reveal, NOT html.js. These two rules hide whole sections of the
   page — the fork, both exhibits, the record, the people, the contact grid — and
   only js/reveal.js ever puts them back. html.js is set in the boot script
   before any of that has loaded, so gating on it meant one blocked request or
   one parse error left a visitor with a hero, a marquee and nothing else.
   js/reveal.js adds html.reveal as its first statement, so the start state
   cannot exist unless the file that clears it is already running. Same contract
   as html.anime below. */
html.reveal .rise{opacity:0;transform:translateY(calc(16px * var(--motion)));
  transition:opacity .75s var(--ease-reveal),transform .75s var(--ease-reveal)}
html.reveal .rise.in{opacity:1;transform:none}
html.reveal .stag>*{opacity:0;transform:translateY(calc(14px * var(--motion)));
  transition:opacity .65s var(--ease-reveal),transform .65s var(--ease-reveal)}
html.reveal .stag.in>*{opacity:1;transform:none}
/* 80ms between children put a six-row list through its whole sequence in about
   a second, which read as one movement rather than as parts arriving in order.
   Widened now that the reveal fires late enough for the sequence to be watched. */
html.reveal .stag.in>*:nth-child(2){transition-delay:.11s}
html.reveal .stag.in>*:nth-child(3){transition-delay:.22s}
html.reveal .stag.in>*:nth-child(4){transition-delay:.33s}
html.reveal .stag.in>*:nth-child(5){transition-delay:.44s}
html.reveal .stag.in>*:nth-child(6){transition-delay:.55s}

/* === start states for the choreographed parts =============================
   Gated on html.anime, not html.js: js/build.js is the only thing that animates
   these to rest, so if anime.js never loads they must never have been hidden.
   The offsets are written here as well as there so the first paint matches the
   first frame. */
.anime .win__lintel{opacity:0;transform:scaleX(calc(1 - .78 * var(--motion)));
  transform-origin:center}
.anime .win__casing{opacity:0;
  transform:translateY(calc(-9px * var(--motion))) scale(calc(1 - .015 * var(--motion)))}
.anime .win__glass img{--fill:0;transform:scale(calc(1 + .07 * var(--motion)))}
.anime .win__sill,.anime .win__apron{opacity:0;transform:scaleX(calc(1 - .82 * var(--motion)))}
.anime .win__cap{opacity:0;transform:translateY(calc(13px * var(--motion)))}

/* one-shot glint the moment a pane is glazed */
@keyframes glint{from{left:-70%}to{left:140%}}
.win--glint .win__glass::after{transition:none;animation:glint 1.05s cubic-bezier(.32,0,.2,1)}

/* === calm mode ============================================================
   --motion:0 has already removed every offset above. What remains is switching
   off the two ambient loops and the travelling highlights, and restoring a
   plain cross-fade where the movement was the whole reveal. */
html.calm .crs--drift{animation:none}
html.calm .tick__t{animation:none}
html.calm .win__glass::after{display:none}
html.calm .win--glint .win__glass::after{animation:none}
html.calm .crs--lay{animation:fadein .5s linear var(--d) both}
html.calm .crs--lay::after{animation:none}
html.calm .hero h1 .l>i{animation:fadein .7s linear .15s both}
