/* ============================================================================
   Proof — motion layer                                    /assets/motion.css
   Opt-in. Add `extraCss: /assets/motion.css` to a page's front matter, then put
   the class on the element. No JS anywhere in this file.
   House rules: instrumentation, not marketing. Short durations, monotonic
   easing (no overshoot), transform/opacity/clip only, nothing that reflows
   after first paint, nothing that delays reading. Every animation carries
   fill-mode `both` so it settles in its final state, and every animation is
   neutralised in the reduced-motion block at the foot of the file.
   ========================================================================== */

/* 1. .rise — element lifts 10px and fades in as it scrolls into view.
   Scroll-driven, so it needs no observer. Browsers without view() never see
   the hidden start state: it exists only inside the @supports guard.
   Stagger siblings with --i.   <div class="card rise" style="--i:2">        */
@supports (animation-timeline: view()){
  .rise{
    animation:m-rise .5s cubic-bezier(.22,.61,.36,1) both;
    animation-delay:calc(var(--i,0) * 60ms);
    animation-timeline:view();
    /* a progress-based timeline ignores the time delay above, so the same
       stagger is carried by the range offset */
    animation-range:entry calc(4% + var(--i,0) * 4%) cover calc(30% + var(--i,0) * 4%);
  }
}
@keyframes m-rise{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}

/* 2. .scan — one thin cyan sweep across the top hairline of a panel: "this
   surface was checked". Purely decorative, ends invisible.
   <div class="panel scan">                                                  */
.scan{position:relative;}
.scan::after{content:"";position:absolute;left:0;right:0;top:0;height:1px;
  pointer-events:none;
  background:linear-gradient(90deg,transparent,var(--cyan),transparent);
  background-size:36% 100%;background-repeat:no-repeat;background-position:-45% 0;
  animation:m-scan 1.15s cubic-bezier(.3,0,.4,1) both;}
@keyframes m-scan{
  0%{background-position:-45% 0;opacity:0}
  14%,86%{opacity:1}
  100%{background-position:145% 0;opacity:0}}

/* 3. .stamp — a verdict badge settling into place: 4% scale-down, no bounce,
   with a hairline ring that contracts and fades out. Sits on top of .chip for
   the border, or standalone. Colour with .ok / .warn / .bad.
   <span class="chip cy stamp ok">VERIFIED</span>                            */
.stamp{position:relative;--sc:var(--dim);
  animation:m-stamp .34s cubic-bezier(.2,.7,.4,1) both;}
.stamp::after{content:"";position:absolute;inset:-1px;border:1px solid var(--sc);
  border-radius:2px;pointer-events:none;animation:m-ring .5s ease-out both;}
.stamp.ok{--sc:var(--cyan);color:var(--link);}
.stamp.warn{--sc:var(--amber);color:var(--amber-ink);}
.stamp.bad{--sc:var(--red);color:var(--red-ink);}
@keyframes m-stamp{from{opacity:0;transform:scale(1.04)}to{opacity:1;transform:none}}
@keyframes m-ring{from{opacity:.85;transform:scale(1.3)}to{opacity:0;transform:none}}

/* 4. .tick — a monospace counter row whose cells fade in left to right, for a
   number that should read as measured. One element per cell; eight cells are
   pre-staggered, past that set --i inline. Total run stays under 700ms.
   <span class="tick"><span>1</span><span>2</span><span>3</span></span>      */
.tick>*{animation:m-fade .26s linear both;animation-delay:calc(var(--i,0) * 55ms);}
.tick>:nth-child(2){--i:1}.tick>:nth-child(3){--i:2}.tick>:nth-child(4){--i:3}
.tick>:nth-child(5){--i:4}.tick>:nth-child(6){--i:5}.tick>:nth-child(7){--i:6}
.tick>:nth-child(8){--i:7}
@keyframes m-fade{from{opacity:0}to{opacity:1}}

/* 5. .wire — draws an SVG connector on between two figures. Put
   pathLength="100" on the path so the dash maths needs no measurement, or
   override --len with the real length. Stagger with --i.
   <path class="wire" pathLength="100" d="M0 0H120" style="--i:1"/>          */
.wire{stroke-dasharray:var(--len,100);stroke-dashoffset:var(--len,100);
  animation:m-wire .9s cubic-bezier(.3,0,.3,1) both;
  animation-delay:calc(var(--i,0) * 120ms);}
@keyframes m-wire{to{stroke-dashoffset:0}}

/* 6. .pulse-dot — breathing dot for a live / standing-by readout. Cyan unless
   --sc says otherwise. It runs forever, so use one per view.
   <span class="pulse-dot" aria-hidden="true"></span>CONTINUOUS AUDIT        */
.pulse-dot{display:inline-block;width:6px;height:6px;border-radius:50%;
  background:var(--sc,var(--cyan));vertical-align:1px;margin-right:8px;
  animation:m-pulse 2.4s ease-in-out infinite;}
@keyframes m-pulse{
  0%,100%{opacity:1;box-shadow:0 0 0 0 rgba(14,157,184,.45)}
  50%{opacity:.5;box-shadow:0 0 0 4px rgba(14,157,184,0)}}

/* 7. .grow — a bar or column expanding from nothing to its measured size, for
   one requirement fanning out into many obligations. The final width comes
   from --w, so the box is its final size at first paint and only the scale
   animates; height from --h; colour from --sc. Keep the label in a sibling.
   <div class="grow" style="--w:64%;--sc:var(--violet)"></div>               */
.grow{width:var(--w,100%);height:var(--h,6px);background:var(--sc,var(--cyan));
  border-radius:1px;transform-origin:left center;
  animation:m-grow .7s cubic-bezier(.25,.6,.35,1) both;
  animation-delay:calc(var(--i,0) * 90ms);}
@keyframes m-grow{from{transform:scaleX(0)}to{transform:scaleX(1)}}

/* .wire and .grow prefer to fire when they scroll into view. Under a
   progress-based timeline the time delays above are ignored, so --i moves the
   range instead.                                                            */
@supports (animation-timeline: view()){
  .scan::after{animation-timeline:view();animation-range:entry 10% cover 30%;}
  .wire,.grow{animation-timeline:view();
    animation-range:entry calc(8% + var(--i,0) * 6%) cover calc(38% + var(--i,0) * 6%);}
}

/* 8. .type — reveals one short line of code behind a block caret. The reveal
   is a clip, so the box never changes size; padding-right reserves the caret
   cell up front. Reveal 1.05s, caret blinks three times and goes. Use it on a
   single short line only.   <code class="type">proof audit</code>           */
.type{display:inline-block;position:relative;white-space:pre;padding-right:.8em;
  animation:m-type 1.05s steps(22,end) both;}
.type::after{content:"";position:absolute;right:.15em;top:.16em;
  width:.5em;height:1em;background:var(--ink);
  animation:m-caret .42s linear 1.05s 3 both;}
@keyframes m-type{from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0 0 0)}}
@keyframes m-caret{0%,49%{opacity:1}50%,100%{opacity:0}}

/* ============================================================================
   KINETICS — the scroll-driven layer.                     /assets/motion.js
   The classes below are applied by motion.js and by nothing else, which is the
   whole safety argument: with JavaScript off none of them exist, and the page
   is already in the state they animate towards.

   Two conventions run through the block.

   `html.m-live`  motion.js is running. Present only once an IntersectionObserver
                  has actually been wired, so a browser without one is untouched.
   `.m-arm`       this element is below the fold and may safely be given a
                  pre-state. `.m-in` replaces it when the element arrives.
                  An element already on screen is marked `.m-in` immediately and
                  never moves, so nothing jumps and nothing shifts layout.

   Every pre-state here is a transform (or a transform of a decorative
   pseudo-element). No copy is ever behind an opacity, so an element that never
   receives `.m-in` — impossible in practice, cheap to guarantee — is still a
   fully readable, fully laid-out element that has been nudged ten pixels.
   ========================================================================== */

/* 9. SECTION TELEMETRY — a position readout in the chrome's micro-type. Built
   by motion.js, so the markup carries nothing. Bottom-left, where #hint sits on
   the hero; the footer is opaque and above it, so it never cuts into a link.
   The scale is a row of station ticks rather than a filled bar: it reports
   where the reader is, not how much is left.                                */
.m-tel{position:fixed;left:20px;bottom:16px;z-index:4;pointer-events:none;
  display:flex;align-items:center;gap:11px;max-width:min(440px,42vw);
  padding:7px 22px 7px 8px;
  font-family:var(--mono);font-size:10px;letter-spacing:.24em;
  text-transform:uppercase;color:var(--dim);
  /* the readout is fixed and copy scrolls under it, so it carries the same
     fade-to-page backdrop #mark uses in the opposite corner */
  background:linear-gradient(270deg,rgba(250,251,252,0),rgba(250,251,252,.92) 26%);
  opacity:0;transition:opacity .4s linear;}
.m-tel.on{opacity:1;}
.m-tel-s{display:flex;align-items:flex-end;gap:3px;height:9px;}
.m-tel-s i{display:block;width:1px;height:3px;background:rgba(90,101,114,.30);
  transition:height .3s cubic-bezier(.22,.61,.36,1),background-color .3s linear;}
.m-tel-s i.on{height:9px;background:var(--cyan);}
.m-tel-n{color:var(--ink2);letter-spacing:.16em;}
.m-tel-n:empty{display:none;}   /* a section that declares no number */
.m-tel-t{color:var(--dim);letter-spacing:.18em;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
/* the nav wraps below ~1100px and the chrome row goes with it */
@media (max-width:1100px){ .m-tel{display:none;} }

/* 10. THE NUMERAL SPINE REACTS. The oversized gutter numerals are one
   instrument, not eleven decorations: once tracking starts they all recede and
   the section under the reader carries the stroke. Colour and transform only —
   the stroke width never changes, so no glyph metric moves.                 */
.numeral{transition:color .45s linear,-webkit-text-stroke-color .45s linear,
  transform .55s cubic-bezier(.22,.61,.36,1);}
html.m-track .numeral{-webkit-text-stroke-color:rgba(90,101,114,.11);}
html.m-track .numeral.is-live{
  -webkit-text-stroke-color:rgba(124,92,214,.42);
  color:rgba(124,92,214,.06);
  transform:translateY(-10px) scale(1.34);}

/* The numeral is position:absolute, pointer-events:none and user-select:none, so
   it is out of flow in every sense: scaling it cannot reflow, cannot reorder, and
   cannot create a scrollable overflow. That is what makes it the one element that
   can carry a large gesture safely. It grows from its own left edge so it opens
   into the gutter rather than across the column, and the stroke fades as it grows
   so a bigger glyph is not a louder one.                                       */
html.m-track .numeral{transform-origin:left top;}

/* Below the point where the gutter exists, the numerals sit much nearer the text
   column, so the same gesture would cross the copy. It is dropped, not shrunk.  */
@media (max-width:1100px){
  html.m-track .numeral.is-live{transform:translateY(-3px) scale(1.02);}
}

/* 11. RULES THAT DRAW THEMSELVES. Each of these sits on top of a hairline that
   proof.css already draws as a border, so the rule is present at first paint
   and what animates is an overlay scaled from one end. scaleX only; no width
   is ever animated and nothing reflows.                                     */
.sec.alt.m-seam::after{content:"";position:absolute;left:0;right:0;top:-1px;
  height:1px;pointer-events:none;transform-origin:0 50%;
  background:linear-gradient(90deg,rgba(14,157,184,.5),rgba(90,101,114,.26) 46%,
    rgba(90,101,114,0));
  transition:transform .95s cubic-bezier(.25,.6,.35,1);}
.sec.alt.m-seam.rtl::after{transform-origin:100% 50%;
  background:linear-gradient(270deg,rgba(14,157,184,.5),rgba(90,101,114,.26) 46%,
    rgba(90,101,114,0));}
html.m-live .sec.alt.m-seam.m-arm:not(.m-in)::after{transform:scaleX(0);}

/* The diagonal cross-rules keep their inline rotate: `scale` is an independent
   transform property, so animating it cannot clobber `transform`.
   The observed element is the .hair-x frame, not the 1px lines inside it. A line
   scaled to zero width has zero area, and IntersectionObserver reports a
   zero-area target as a ratio of 0 forever, so it would have armed and never
   drawn. The frame fills the band and always has area.                       */
.m-rulex i{transform-origin:0 0;
  transition:scale .9s cubic-bezier(.25,.6,.35,1);}
.m-rulex i:nth-child(2){transition-delay:.12s;}
.m-rulex i:nth-child(3){transition-delay:.24s;}
html.m-live .m-rulex.m-arm:not(.m-in) i{scale:0 1;}

/* the definition-row block: its top hairline draws in with the block */
.rows.m-ruled{position:relative;}
.rows.m-ruled::before{content:"";position:absolute;left:0;right:0;top:-1px;height:1px;
  pointer-events:none;transform-origin:0 50%;background:rgba(90,101,114,.30);
  transition:transform .7s cubic-bezier(.25,.6,.35,1);
  transition-delay:calc(var(--i,0) * 70ms);}
html.m-live .rows.m-ruled.m-arm:not(.m-in)::before{transform:scaleX(0);}

/* 12. A sticky figure's caption, once motion.js is swapping it per adjacent
   block. It must never wrap out of the figure frame, so it clips.           */
.fig-wrap .fig-cap.m-cap-live{right:12px;white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis;}

/* 13. PER-PAGE MOMENTS. One each, applied by slug in motion.js.            */

/* index · §11 — the seats table deals its rows in, alternating the tilt */
.m-deal{transition:transform .62s cubic-bezier(.22,.61,.36,1);
  transition-delay:calc(var(--i,0) * 65ms);}
html.m-live .m-deal.m-arm:not(.m-in){
  transform:translateY(16px) rotate(var(--rot,-.5deg));}

/* the engagement · §1 — the three ladder steps are laid down in order, top
   edge first, as if onto a light table. The translateZ is not decoration: a
   tilt on its own projects the near edge OUTWARD, and the first step of the
   ladder is a .bleed-r panel already touching the viewport edge, so the panel
   is pushed back further than the tilt brings it forward. The armed box is
   always smaller than the settled one, never wider. */
.m-lay{transform-origin:50% 0;
  transition:transform .6s cubic-bezier(.22,.61,.36,1);
  transition-delay:calc(var(--i,0) * 90ms);}
html.m-live .m-lay.m-arm:not(.m-in){
  transform:perspective(1200px) translateZ(-34px) rotateX(1.5deg) translateY(-6px);}

/* methodology · §2 — the limits table reveals bottom-up, because the page
   tells the reader to read it that way */
.m-up{transition:transform .55s cubic-bezier(.22,.61,.36,1);
  transition-delay:calc(var(--i,0) * 60ms);}
html.m-live .m-up.m-arm:not(.m-in){transform:translateY(12px);}

/* ai-native systems · §5 — fit and not-a-fit arrive mirrored */
.m-mirror{transition:transform .7s cubic-bezier(.22,.61,.36,1);}
@media (min-width:901px){
  html.m-live .m-mirror.l.m-arm:not(.m-in){transform:translateX(-22px);}
  html.m-live .m-mirror.r.m-arm:not(.m-in){transform:translateX(22px);}
}

/* tools · §2 — the engine figure tilts with scroll position. Scroll-linked, so
   it needs no observer; the scale keeps the rotated canvas covering its frame.
   Without view() support the canvas simply sits square.                      */
@supports (animation-timeline: view()){
  html.m-live .m-spin{animation:m-spin-t linear both;
    animation-timeline:view();animation-range:cover;}
}
@keyframes m-spin-t{
  from{transform:rotate(-1.5deg) scale(1.04)}
  to{transform:rotate(1.5deg) scale(1.04)}}

/* findings · the ledger — a chart-recorder pen drawing down the gutter at the
   reader's own rate. Decorative, aria-hidden, created by motion.js.          */
.m-trace{position:absolute;left:calc(var(--gut) * .42);top:5%;width:1px;height:90%;
  pointer-events:none;transform-origin:50% 0;
  background:linear-gradient(180deg,rgba(14,157,184,.55),rgba(14,157,184,.10));}
@supports (animation-timeline: view()){
  html.m-live .m-trace{animation:m-trace-t linear both;
    animation-timeline:view();animation-range:entry 24% exit 12%;}
}
@keyframes m-trace-t{from{transform:scaleY(0)}to{transform:scaleY(1)}}
@media (max-width:900px){ .m-trace{display:none;} }

/* trust — the audience lamp bank, in the chrome's status slot. The page answers
   three readers in turn and the lit lamp says which one is being answered.   */
.m-lamp{position:fixed;top:74px;right:56px;z-index:4;pointer-events:none;
  display:flex;gap:16px;align-items:center;}
.m-lamp b{display:flex;align-items:center;gap:7px;font-weight:400;
  font-size:9px;letter-spacing:.24em;text-transform:uppercase;color:var(--dim2);
  transition:color .4s linear;}
.m-lamp b::before{content:"";width:4px;height:4px;border-radius:50%;
  background:var(--line);
  transition:background-color .4s linear,box-shadow .4s linear;}
.m-lamp b.on{color:var(--dim);}
.m-lamp b.on::before{background:var(--cyan);box-shadow:0 0 0 3px rgba(14,157,184,.13);}
@media (max-width:1240px){ .m-lamp{display:none;} }

/* 404 — the numeral drifts and rotates with scroll position, which is the same
   drift the figure underneath it is about */
@supports (animation-timeline: view()){
  html.m-live .m-numdrift{animation:m-numdrift-t linear both;
    animation-timeline:view();animation-range:cover;}
}
@keyframes m-numdrift-t{
  from{transform:translateY(-12px) rotate(-.9deg)}
  to{transform:translateY(16px) rotate(.9deg)}}

/* --------------------------------------------------------------------------
   Reduced motion: no animation runs, and every class is left in the state a
   reader needs — text opaque, bars at full width, paths drawn, code line
   fully revealed, decorative sweeps and rings simply absent.

   The kinetic layer is doubly covered: motion.js does not arm anything under
   reduced motion, and every pre-state and transition below is switched off in
   case a class is applied anyway. The telemetry readout and the numeral
   tracking survive, because a readout that reports position is state rather
   than movement — they simply change value without a transition.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce){
  .rise,.tick>*,.stamp,.wire,.grow,.type,.pulse-dot{
    animation:none!important;opacity:1;transform:none;}
  .wire{stroke-dashoffset:0;}
  .type{clip-path:none;padding-right:0;}
  .scan::after,.stamp::after,.type::after{display:none;}

  .m-deal,.m-lay,.m-up,.m-mirror,.m-rulex,.m-spin,.m-numdrift,
  .numeral,.m-tel,.m-tel-s i,.m-lamp b,.m-lamp b::before,
  .sec.alt.m-seam::after,.rows.m-ruled::before,.m-trace{
    animation:none!important;transition:none!important;}
  html.m-live .m-deal,html.m-live .m-lay,html.m-live .m-up,
  html.m-live .m-mirror,html.m-live .m-spin,html.m-live .m-numdrift,
  html.m-track .numeral.is-live{transform:none!important;}
  html.m-live .m-rulex{scale:1 1!important;}
  html.m-live .sec.alt.m-seam::after,html.m-live .rows.m-ruled::before,
  html.m-live .m-trace{transform:none!important;}
}
