/* ==========================================================================
   ALLIN — fx.css
   Motion layer: scroll reveals, cursor, grain, marquee, and the hero
   ecosystem graph. Loaded after sections.css so it can lean on the tokens.

   Everything here is gated on `html.js`. Without the boot script the page
   renders fully visible and static — a failed script never blanks content.
   ========================================================================== */

/* ---------- Scroll reveal ------------------------------------------------- */
html.js [data-reveal]{
  opacity:0; transform:translateY(24px);
  transition:opacity .85s var(--ease-out) var(--reveal-delay,0ms),
             transform .85s var(--ease-out) var(--reveal-delay,0ms);
  will-change:opacity,transform;
}
html.js [data-reveal].is-revealed{opacity:1;transform:none;will-change:auto}
html.js [data-reveal-group] > *{
  opacity:0; transform:translateY(20px);
  transition:opacity .8s var(--ease-out) var(--reveal-delay,0ms),
             transform .8s var(--ease-out) var(--reveal-delay,0ms);
}
html.js [data-reveal-group].is-revealed > *{opacity:1;transform:none}

/* Character reveal. The chars only become inline-block once the split has
   run, so an un-split heading keeps normal text flow and normal wrapping. */
.split-word{display:inline-block;white-space:nowrap}
.split-char{display:inline-block;will-change:transform,opacity}
html.js [data-split].split-ready .split-char{
  opacity:0; transform:translateY(.55em) rotate(2deg);
  transition:opacity .7s var(--ease-out),transform .7s var(--ease-out);
}
html.js [data-split].split-ready.is-revealed .split-char{opacity:1;transform:none}

html.reduced-motion [data-reveal],
html.reduced-motion [data-reveal-group] > *,
html.reduced-motion [data-split].split-ready .split-char{
  opacity:1 !important;transform:none !important;transition:none !important;
}

/* ---------- Film grain ---------------------------------------------------- */
.grain{
  position:fixed; inset:0; width:100%; height:100%;
  pointer-events:none; z-index:9998;
  opacity:.035; mix-blend-mode:overlay;
}
[data-theme="light"] .grain{opacity:.02}

/* ---------- Cursor -------------------------------------------------------- */
/* Only mounted on fine pointers, and the native cursor is never hidden — a
   custom cursor that disappears over an iframe or a native control is worse
   than no custom cursor at all. */
.cursor{position:fixed;inset:0;pointer-events:none;z-index:9999;opacity:0;transition:opacity .3s}
.cursor.is-active{opacity:1}
.cursor__ring,.cursor__dot{position:absolute;top:0;left:0;border-radius:50%}
.cursor__ring{
  width:34px;height:34px;border:1px solid var(--gold);
  transition:width .28s var(--ease-out),height .28s var(--ease-out),
             background .28s var(--ease-out),border-color .28s;
}
.cursor__dot{width:5px;height:5px;background:var(--gold)}
.cursor.is-hovering .cursor__ring{
  width:52px;height:52px;background:rgba(123,92,255,.12);border-color:var(--gold-soft);
}

/* ---------- Scroll progress ----------------------------------------------- */
.scroll-progress{
  position:fixed;top:0;left:0;right:0;height:2px;z-index:9997;
  transform:scaleX(0);transform-origin:0 50%;
  background:linear-gradient(90deg,var(--gold-deep),var(--gold),var(--gold-soft));
}

/* ---------- Marquee ------------------------------------------------------- */
.marquee{overflow:hidden;position:relative;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 8%,#000 92%,transparent);
          mask-image:linear-gradient(90deg,transparent,#000 8%,#000 92%,transparent);
}
.marquee__track{display:flex;width:max-content;will-change:transform}

/* ==========================================================================
   Hero ecosystem graph
   One SVG, no JavaScript. Every wire is a static path plus a duplicate that
   carries a travelling dash; every leaf cycles its course names in place.
   ========================================================================== */
.hero--eco{
  position:relative;overflow:hidden;
  /* Tighter than the default hero: the graph is the payload, so the copy
     above it has to leave the fold before the first branch. */
  padding:calc(var(--nav-h) + clamp(1.4rem,3.5vw,2.6rem)) 0 clamp(2.5rem,5vw,4rem);
}
.hero--eco .hero-copy h1{font-size:clamp(2.2rem,1.5rem + 2.8vw,3.5rem);margin:.9rem 0 1rem}
.hero--eco .hero-copy .lead{margin-bottom:1.5rem}
.hero--eco .hero-copy{max-width:880px;margin-inline:auto;text-align:center}
.hero--eco .hero-copy .lead{margin-inline:auto;max-width:62ch}
.hero--eco .hero-cta{justify-content:center}

/* Soft brand light behind the graph, so the wires read as lit rather than drawn. */
.hero-aura{
  position:absolute;left:50%;top:8%;width:min(1100px,120vw);aspect-ratio:1.6;
  transform:translateX(-50%);pointer-events:none;z-index:0;
  background:radial-gradient(50% 50% at 50% 40%,rgba(123,92,255,.22),transparent 70%);
  filter:blur(10px);
}
.hero--eco .container{position:relative;z-index:1}

/* Width is capped below the container on purpose: the SVG keeps a fixed
   aspect ratio, so every extra pixel of width costs half a pixel of height and
   pushes the first branch past the fold. */
.eco{margin:clamp(1rem,2.6vw,1.9rem) auto 0;max-width:960px}
.eco__cap{
  font-family:var(--font-mono);font-size:var(--fs-xs);letter-spacing:.18em;
  text-transform:uppercase;color:var(--text-mute);
  display:flex;align-items:center;justify-content:center;gap:.9rem;margin-bottom:.75rem;
}
/* Hairlines either side of the caption, drawn in the same colour as the graph
   wires so the label reads as part of the diagram rather than a heading. */
.eco__cap::before,.eco__cap::after{
  content:"";height:1px;flex:0 1 110px;
  background:linear-gradient(90deg,transparent,var(--border));
}
.eco__cap::after{background:linear-gradient(90deg,var(--border),transparent)}
/* The graph keeps a readable minimum width and scrolls sideways on small
   screens. Scaling it down to fit a phone would shrink 12px labels to five. */
.eco__frame{overflow-x:auto;overflow-y:hidden;scrollbar-width:none;padding-bottom:.25rem}
.eco__frame::-webkit-scrollbar{display:none}
.eco__svg{display:block;width:100%;min-width:660px;height:auto;overflow:visible}

.eco__edge{stroke:var(--border);stroke-width:1.1}
/* pathLength="1" normalises every curve to the same length, so one dash
   pattern travels at the same visual speed along a short wire and a long one. */
.eco__spark{
  stroke:var(--gold-soft);stroke-width:1.8;stroke-linecap:round;
  stroke-dasharray:.18 1;stroke-dashoffset:.18;
  animation:eco-travel 2.8s linear infinite;
  animation-delay:calc(var(--i) * .18s);
}

.eco__node rect,.eco__root rect{
  fill:var(--surface);stroke:var(--border);stroke-width:1.2;
  transition:fill .3s var(--ease),stroke .3s var(--ease);
}
.eco__root rect{
  stroke:var(--gold);stroke-width:1.5;
  filter:drop-shadow(0 0 22px rgba(123,92,255,.35));
}
.eco__root-name{
  font-family:var(--font-head);font-size:23px;font-weight:700;
  fill:var(--heading);letter-spacing:.16em;
}
.eco__node--div rect{stroke:rgba(123,92,255,.36)}
.eco__label{font-family:var(--font-mono);font-size:13px;fill:var(--text)}
.eco__node--leaf .eco__label{font-size:12px}
.eco__kind{
  font-family:var(--font-mono);font-size:9px;letter-spacing:.12em;
  text-transform:uppercase;fill:var(--text-mute);
}

a.eco__node{cursor:pointer}
a.eco__node:hover rect{fill:var(--surface-2);stroke:var(--gold)}
a.eco__node:hover .eco__label{fill:var(--heading)}
a.eco__node:focus-visible{outline:none}
a.eco__node:focus-visible rect{stroke:var(--gold);stroke-width:2}

/* Course names share one slot per leaf and hand off to each other. Cycle
   lengths differ, but both loops are 9s, so the whole board stays in step. */
.eco__swap{
  opacity:0;animation-duration:9s;animation-timing-function:var(--ease-in-out);
  animation-iteration-count:infinite;
}
.eco__swap--3{animation-name:eco-swap-3;animation-delay:calc(var(--i) * 3s)}
.eco__swap--2{animation-name:eco-swap-2;animation-delay:calc(var(--i) * 4.5s)}

@keyframes eco-travel{from{stroke-dashoffset:.18}to{stroke-dashoffset:-1}}
@keyframes eco-swap-3{
  0%{opacity:0;transform:translateY(7px)}
  4%,28%{opacity:1;transform:none}
  33.33%,100%{opacity:0;transform:translateY(-7px)}
}
@keyframes eco-swap-2{
  0%{opacity:0;transform:translateY(7px)}
  5%,44%{opacity:1;transform:none}
  50%,100%{opacity:0;transform:translateY(-7px)}
}

html.reduced-motion .eco__spark{animation:none;opacity:0}
html.reduced-motion .eco__swap{animation:none;opacity:0}
html.reduced-motion .eco__swap[style*="--i: 0"]{opacity:1}

/* ---------- Hero stats ---------------------------------------------------- */
.hero-stats{
  display:flex;flex-wrap:wrap;justify-content:center;gap:.75rem;
  margin-top:clamp(1.5rem,4vw,2.5rem);
}
.hero-stats .hs{
  display:flex;align-items:center;gap:.7rem;text-align:left;
  padding:.7rem 1.1rem;border:1px solid var(--border);border-radius:999px;
  background:color-mix(in srgb,var(--surface) 80%,transparent);
  backdrop-filter:blur(8px);
}
.hero-stats .ico{display:grid;place-items:center;width:26px;height:26px;color:var(--gold)}
.hero-stats .ico svg{width:20px;height:20px}
.hero-stats .b-num{font-family:var(--font-head);font-weight:700;color:var(--heading);line-height:1.1}
.hero-stats .b-cap{font-size:var(--fs-xs);color:var(--text-mute)}

@media (max-width:640px){
  .eco__cap{margin-bottom:.6rem}
  .hero-stats .hs{padding:.6rem .9rem}
}

/* ==========================================================================
   Premium surface utilities
   ========================================================================== */

/* --- Border beam ----------------------------------------------------------
   A lit arc that travels the card's outline. Built from a conic gradient
   rotated by an @property angle — a plain custom property is a string to the
   animation engine and would jump from 0deg to 360deg instead of sweeping. */
@property --beam-angle{syntax:"<angle>";initial-value:0deg;inherits:false}

.beam,
.cat-card,.why-card,.prog-card,.vcard,.fac-card{position:relative;isolation:isolate}
.beam::after,
.cat-card::after,.why-card::after,.prog-card::after,.vcard::after,.fac-card::after{
  content:"";position:absolute;inset:0;border-radius:inherit;padding:1.4px;
  background:conic-gradient(from var(--beam-angle),
    transparent 0 62%, var(--gold-soft) 76%, var(--gold) 84%, transparent 92%);
  /* Two masks, subtracted: the fill is punched out and only the padding ring
     survives, so the gradient shows as a border rather than a wash. */
  -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
          mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;mask-composite:exclude;
  opacity:0;transition:opacity .45s var(--ease-out);pointer-events:none;
}
.beam:hover::after,.beam:focus-within::after,
.cat-card:hover::after,.cat-card:focus-visible::after,
.why-card:hover::after,.prog-card:hover::after,
.vcard:hover::after,.fac-card:hover::after{
  /* The spin only runs under the pointer. Left on permanently it would repaint
     a conic gradient every frame for every card on the page, visible or not. */
  opacity:1;animation:beam-spin 5s linear infinite;
}
@keyframes beam-spin{to{--beam-angle:360deg}}

/* --- Spotlight ------------------------------------------------------------
   Pointer position arrives as --mx/--my from initSpotlight; the highlight
   itself is CSS, so it composes with whatever hover transform the card has. */
[data-spotlight],
.step-card{position:relative;isolation:isolate}
[data-spotlight]::before,
.cat-card::before,.why-card::before,.vcard::before,.fac-card::before,.step-card::before{
  content:"";position:absolute;inset:0;border-radius:inherit;z-index:-1;
  background:radial-gradient(340px circle at var(--mx,50%) var(--my,0%),
    rgba(123,92,255,.16),transparent 62%);
  opacity:0;transition:opacity .4s var(--ease-out);pointer-events:none;
}
[data-spotlight]:hover::before,
.cat-card:hover::before,.why-card:hover::before,
.vcard:hover::before,.fac-card:hover::before,.step-card:hover::before{opacity:1}

/* --- Shiny text -----------------------------------------------------------
   A highlight sweeps the glyphs every few seconds. background-clip:text takes
   over the element's whole background, so this belongs on plain accent text,
   never on something that carries a pill or card fill. The base colour is held
   at partial alpha because opaque glyphs would sit on top of the sweep and
   hide it. */
.shiny{
  --shiny-width:7ch;
  display:inline-block;
  color:color-mix(in srgb,var(--gold) 76%,transparent);
  background:linear-gradient(100deg,transparent calc(50% - var(--shiny-width)/2),
    var(--gold-soft),transparent calc(50% + var(--shiny-width)/2)) no-repeat;
  background-size:calc(100% + var(--shiny-width) * 2) 100%;
  -webkit-background-clip:text;background-clip:text;
  animation:shiny-text 7s var(--ease-in-out) infinite;
}
@keyframes shiny-text{
  0%,86%,100%{background-position:calc(-1 * var(--shiny-width)) 0}
  30%,56%{background-position:calc(100% + var(--shiny-width)) 0}
}

html.reduced-motion .beam::after,
html.reduced-motion .cat-card::after,html.reduced-motion .why-card::after,
html.reduced-motion .prog-card::after,html.reduced-motion .vcard::after,
html.reduced-motion .fac-card::after,
html.reduced-motion .shiny{animation:none}
html.reduced-motion .shiny{background:none;color:var(--gold)}
html.reduced-motion .cat-card{transition:none}

/* --- Category filter swap --------------------------------------------------
   The tab script fades a card out, waits for the transition, then flips
   [hidden] and fades the new set in. Plain instant hidden-toggling read as a
   jump cut; this is the same opacity/translate pattern as scroll reveal, just
   driven by a click instead of the viewport. */
.cat-card.is-hiding{opacity:0;transform:translateY(6px);pointer-events:none}

/* --- Button shine ---------------------------------------------------------
   A single specular pass across the primary CTA on hover. The band is parked
   off the left edge and animates past the right, so it plays once per hover
   rather than looping in the corner of the user's eye. */
.btn-gold{position:relative;overflow:hidden;isolation:isolate}
.btn-gold::after{
  content:"";position:absolute;top:0;left:-140%;width:55%;height:100%;
  background:linear-gradient(100deg,transparent,rgba(255,255,255,.42),transparent);
  transform:skewX(-18deg);pointer-events:none;
}
.btn-gold:hover::after,.btn-gold:focus-visible::after{animation:btn-shine .8s var(--ease-out)}
@keyframes btn-shine{to{left:160%}}
html.reduced-motion .btn-gold::after{animation:none}

/* --- Cross-document page transitions --------------------------------------
   The site is 36 separate documents, so every nav click used to repaint the
   whole window white before the next page drew. This opts the whole site into
   the browser's own transition between documents: same-origin link clicks and
   back/forward now cross-fade instead of flashing.

   Progressive enhancement in the strictest sense — a browser without the API
   ignores the at-rule and navigates exactly as before. Both the outgoing and
   the incoming document must carry this rule, which they do: fx.css is loaded
   on every page.

   Easing is written out rather than referenced through --ease-* because these
   pseudo-elements live in a separate snapshot tree, and a token that fails to
   resolve there silently drops the whole animation. */
@view-transition{navigation:auto}

/* Elements that are identical on every page get their own snapshot so they
   hold still while the page body moves. Without a name they are captured as
   part of the root and cross-fade against themselves, which reads as a
   flicker on exactly the furniture that should feel nailed down. */
.nav{view-transition-name:allin-nav}
.scroll-progress{view-transition-name:allin-progress}

::view-transition-old(root){animation:vt-leave .26s cubic-bezier(.65,0,.35,1) both}
::view-transition-new(root){animation:vt-enter .42s cubic-bezier(.16,1,.3,1) both}
@keyframes vt-leave{to{opacity:0;transform:translateY(-10px)}}
@keyframes vt-enter{from{opacity:0;transform:translateY(14px)}}

/* Reduced motion keeps the transition — a cross-fade is calmer than a white
   flash — but drops the travel. */
@media (prefers-reduced-motion:reduce){
  ::view-transition-old(root){animation:vt-leave-flat .2s linear both}
  ::view-transition-new(root){animation:vt-enter-flat .2s linear both}
  @keyframes vt-leave-flat{to{opacity:0}}
  @keyframes vt-enter-flat{from{opacity:0}}
}

/* ==========================================================================
   Scroll storytelling
   A stage pins while a column of steps scrolls past it. The pin is native
   `position:sticky` — nothing here or in scrollytell.js touches the scroll
   itself. JS only publishes --scrolly-p (0..1) and .is-active/.is-past, and
   everything below is drawn from those two things.
   ========================================================================== */
.scrolly{
  display:grid;
  grid-template-columns:minmax(0,.9fr) minmax(0,1.1fr);
  gap:clamp(1.6rem,4vw,4rem);
  /* Load-bearing: a stretched grid item is exactly as tall as its row, which
     leaves position:sticky with zero distance to travel. `start` lets the
     stage keep its own height and slide inside the row instead. */
  align-items:start;
}

/* ---------- Pinned stage ---------- */
.scrolly__stage{
  position:sticky;
  /* Cleared past the fixed nav, or the panel title parks underneath it. */
  top:calc(var(--nav-h) + clamp(.8rem,3vw,2rem));
  align-self:start;
  border:1px solid var(--border);
  border-radius:var(--radius-lg);
  background:
    radial-gradient(120% 90% at 12% 0%,rgba(123,92,255,.14),transparent 62%),
    var(--surface);
  padding:clamp(1.5rem,3vw,2.4rem);
  box-shadow:var(--shadow);
  overflow:hidden;
}

/* All panels share one grid cell, so the stage is as tall as its tallest
   panel and never resizes as the story advances. Absolute positioning would
   have needed a hard-coded height to do the same job.

   The overlap is scoped to html.js through :where(), which adds no
   specificity of its own. Two things fall out of that: with the boot script
   missing the panels just stack and stay readable, and the stacked fallbacks
   further down win on source order instead of having to out-specify a gate.

   Inactive panels go visibility:hidden rather than opacity:0 alone, so the
   stack cannot swallow pointer events or hand a screen reader four panels at
   once. Nothing is lost by that: the stage is aria-hidden decoration and
   every line in it is a condensed echo of the track beside it. */
.scrolly__panels{display:grid;gap:1rem}
:where(html.js) .scrolly__panels{gap:0}
:where(html.js) .scrolly__panel{
  grid-area:1/1;
  opacity:0;visibility:hidden;transform:translateY(14px);
  /* Delayed visibility on the way out only, so the panel stays paintable for
     the length of its own fade instead of vanishing on frame one. */
  transition:opacity .5s var(--ease-out),transform .5s var(--ease-out),
             visibility 0s linear .5s;
}
:where(html.js) .scrolly__panel.is-active{
  opacity:1;visibility:visible;transform:none;
  transition:opacity .5s var(--ease-out),transform .5s var(--ease-out),
             visibility 0s;
}
.scrolly__num{
  display:block;font-family:var(--font-head);font-weight:700;line-height:1;
  font-size:clamp(3rem,8vw,4.6rem);
  /* The numeral is the thing that ties the stage to the numbered track, so it
     is drawn as a lit outline rather than body text. */
  color:transparent;
  background:linear-gradient(160deg,var(--gold-soft),var(--gold-deep));
  -webkit-background-clip:text;background-clip:text;
}
.scrolly__panel-title{
  font-family:var(--font-head);font-weight:700;color:var(--heading);
  font-size:var(--fs-lg);margin:.7rem 0 .5rem;
}
.scrolly__panel-line{color:var(--text-soft);font-size:var(--fs-base);max-width:34ch}

/* Progress meter. --scrolly-p arrives already damped from the rAF tick, so
   there is deliberately no transition here: a transition on top of per-frame
   values fights the scroll and lags behind the thumb. */
.scrolly__meter{
  margin-top:clamp(1.4rem,3vw,2rem);height:2px;border-radius:2px;
  background:var(--border-soft);overflow:hidden;
}
.scrolly__meter-fill{
  display:block;height:100%;transform-origin:0 50%;
  transform:scaleX(var(--scrolly-p,0));
  background:linear-gradient(90deg,var(--gold-deep),var(--gold),var(--gold-soft));
}

/* ---------- Scrolling track ---------- */
.scrolly__track{
  list-style:none;margin:0;position:relative;
  /* Lead-in. Without it the first step is already centred the moment the pin
     starts and hands over almost immediately, so panel one gets a fraction of
     the pinned time the other three get. Roughly a third of a step slot evens
     the four turns out. */
  padding:18vh 0 0;
}
/* Rail and its fill are two layers of the same line: the unlit groove is a
   static hairline, the lit part is that hairline scaled by progress. Both are
   faded out at the ends — the spine runs the whole column while the first and
   last dots sit a third of a screen inside it, and a hard-cut line reads as a
   stray border rather than as a thread running through the story. */
.scrolly__track::before,
.scrolly__track::after{
  content:"";position:absolute;left:6px;top:0;bottom:0;width:1px;
  -webkit-mask-image:linear-gradient(180deg,transparent,#000 14%,#000 86%,transparent);
          mask-image:linear-gradient(180deg,transparent,#000 14%,#000 86%,transparent);
}
.scrolly__track::before{background:var(--border)}
.scrolly__track::after{
  background:linear-gradient(180deg,var(--gold-deep),var(--gold-soft));
  transform-origin:50% 0;transform:scaleY(var(--scrolly-p,0));
}

.scrolly__step{
  /* The distance between steps IS the pin duration — shorten this and the
     stage un-pins before the last panel has been read. */
  min-height:60vh;
  /* Two columns rather than a dot positioned against the step box: the box is
     most of a screen tall, so an absolutely placed dot would sit at the top of
     an empty slot instead of beside the words it belongs to. */
  display:grid;
  grid-template-columns:13px minmax(0,1fr);
  column-gap:clamp(1rem,2vw,1.5rem);
  /* Centring the rows inside the tall slot is what keeps the active step near
     the middle of the screen for its whole turn instead of sliding up under
     the nav halfway through. */
  align-content:center;
  opacity:.4;transition:opacity .45s var(--ease-out);
}
.scrolly__step.is-past{opacity:.62}
.scrolly__step.is-active{opacity:1}
.scrolly__step-num{
  grid-column:1;grid-row:1;align-self:center;
  width:13px;height:13px;border-radius:50%;
  /* Opaque fill, because the dot sits on top of the rail rather than beside
     it — a transparent dot would show the hairline running straight through. */
  background:var(--bg);border:1px solid var(--border);
  transition:background .35s var(--ease-out),border-color .35s var(--ease-out),
             box-shadow .35s var(--ease-out);
}
.scrolly__step.is-past .scrolly__step-num{background:var(--gold-deep);border-color:var(--gold-deep)}
.scrolly__step.is-active .scrolly__step-num{
  background:var(--gold);border-color:var(--gold-soft);
  box-shadow:0 0 0 5px rgba(123,92,255,.18);
}
.scrolly__step-kicker{
  grid-column:2;grid-row:1;
  font-family:var(--font-mono);font-size:var(--fs-xs);
  letter-spacing:.18em;text-transform:uppercase;color:var(--text-mute);
}
.scrolly__step-title{grid-column:2;grid-row:2;font-size:var(--fs-md);color:var(--heading);margin:.35rem 0 .45rem}
.scrolly__step-text{grid-column:2;grid-row:3;color:var(--text-soft);font-size:var(--fs-base);max-width:46ch}

/* ---------- Stacked fallback ----------
   Two triggers, one outcome. Below 860px there is no room for a stage beside
   a track, and under reduced motion a pin is exactly the kind of motion that
   was asked not to happen — so both drop to a plain document: nothing pinned,
   the decorative stage gone, every step visible in order, no transitions, and a
   rail that reads full because there is no scroll position left to report. */
@media (max-width:860px){
  .scrolly{grid-template-columns:1fr}
  /* The stage is a decorative mirror: aria-hidden, and every line in it is a
     condensation of the step it is pinned beside. Side by side that reads as a
     caption; stacked one above the other it is the same four steps printed
     twice on a screen that has the least room for it. The track carries the
     real headings and the fuller copy, so the stage is what goes. */
  .scrolly__stage{display:none}
  /* The lead-in only exists to pace a pin that no longer happens. */
  .scrolly__track{padding-top:0}
  /* Two classes deep on purpose: this has to outrank .scrolly__step.is-past,
     which is itself two classes deep and would otherwise keep dimming rows
     that no longer have an active row to be behind. */
  .scrolly .scrolly__step{min-height:0;opacity:1;padding-block:.9rem}
  /* JS keeps publishing a real scroll position here, and an inline custom
     property outranks anything the stylesheet says. Neutralise the transform
     instead of arguing over the variable: an unscaled rail reads full, which
     is the honest state for a list with no travel left to report. */
  .scrolly__track::after{transform:none}
}

html.reduced-motion .scrolly{grid-template-columns:1fr;--scrolly-p:1}
/* Same call as the 860px block above, for the same reason: unpinned, the stage
   stops being a caption beside the track and becomes the four steps printed a
   second time. Reduced motion asks for less movement, not for more reading. */
html.reduced-motion .scrolly__stage{display:none}
html.reduced-motion .scrolly__track{padding-top:0}
html.reduced-motion .scrolly__step{
  min-height:0;opacity:1;transition:none;padding-block:.9rem;
}
html.reduced-motion .scrolly__step-num,
html.reduced-motion .scrolly__meter-fill,
html.reduced-motion .scrolly__track::after{transition:none}
html.reduced-motion .scrolly__meter-fill,
html.reduced-motion .scrolly__track::after{transform:none}

/* ── Spec sheet ─────────────────────────────────────────────────────────────
   Lab inventory on the AI & Robotics page. Deliberately reads like a parts
   list rather than a marketing grid: mono labels, hairline chips, no imagery.
   A parent scanning it should be able to price the kit themselves. */
.spec-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(280px,100%),1fr));gap:1.4rem}
.spec{
  background:var(--surface);border:1px solid var(--border-soft);border-radius:var(--radius);
  padding:1.5rem;transition:transform .35s var(--ease),box-shadow .35s,border-color .35s;
}
.spec:hover{transform:translateY(-6px);border-color:var(--border);box-shadow:var(--shadow),var(--ring)}
.spec h3{font-size:var(--fs-base);margin-bottom:.25rem}
.spec p{font-size:var(--fs-sm);color:var(--text-mute);margin-bottom:1rem}
.spec__list{display:flex;flex-wrap:wrap;gap:.45rem;list-style:none;padding:0;margin:0}
.spec__list li{
  font-family:var(--font-mono);font-size:var(--fs-xs);line-height:1;
  color:var(--text-mute);background:var(--surface-2);
  border:1px solid var(--border-soft);border-radius:999px;padding:.42rem .68rem;
  transition:color .25s ease,border-color .25s ease;
}
.spec:hover .spec__list li{color:var(--text);border-color:var(--border)}

/* ── AI & Robotics band (homepage) ──────────────────────────────────────────
   Sits between the three pillars and the course grid. It borrows the hero's
   aura so the new discipline reads as part of the same ecosystem rather than a
   bolted-on announcement, and the kit list is the honest part: real part
   numbers, so a parent can see the hardware is not a stock photo. */
.ai-band{position:relative;overflow:hidden}
.ai-band__aura{
  position:absolute;left:50%;top:-10%;width:min(1200px,130vw);aspect-ratio:1.9;
  transform:translateX(-50%);pointer-events:none;z-index:0;
  background:radial-gradient(50% 50% at 50% 45%,rgba(123,92,255,.16),transparent 70%);
  filter:blur(14px);
}
.ai-band > .container{position:relative;z-index:1}

.ai-band__kit{
  margin:clamp(1.6rem,3vw,2.4rem) auto 0;max-width:860px;
  background:var(--surface);border:1px solid var(--border-soft);border-radius:var(--radius);
  padding:clamp(1.2rem,2.4vw,1.6rem);text-align:center;
  transition:border-color .35s ease,box-shadow .35s ease;
}
.ai-band__kit:hover{border-color:var(--border);box-shadow:var(--shadow),var(--ring)}
.ai-band__kit figcaption{
  font-family:var(--font-mono);font-size:var(--fs-xs);letter-spacing:.18em;
  text-transform:uppercase;color:var(--text-mute);margin-bottom:.9rem;
}
.ai-band__kit .spec__list{justify-content:center}
.ai-band__kit:hover .spec__list li{color:var(--text);border-color:var(--border)}

/* ── Lab kit panel ──────────────────────────────────────────────────────────
   The skills-page version of the hardware list. Same honest part numbers as
   .ai-band__kit, but grouped into four bench categories so it reads as a spec
   sheet a parent or a hiring manager can scan, not a dumped inventory. */
.kit-panel{
  margin:clamp(1.6rem,3vw,2.4rem) 0 0;
  background:var(--surface);border:1px solid var(--border-soft);border-radius:var(--radius);
  padding:clamp(1.4rem,2.8vw,2rem);
  transition:border-color .35s ease,box-shadow .35s ease;
}
.kit-panel:hover{border-color:var(--border);box-shadow:var(--shadow),var(--ring)}
.kit-panel__head{text-align:center;max-width:52ch;margin:0 auto clamp(1.3rem,2.6vw,1.8rem)}
.kit-panel__head h3{font-size:var(--fs-lg);margin:.45rem 0 .4rem}
.kit-panel__head p{font-size:var(--fs-sm);color:var(--text-mute);margin:0}
.kit-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(1rem,2vw,1.6rem)}
.kit-col{
  border-top:1px solid var(--border-soft);padding-top:.9rem;
  transition:border-color .3s ease;
}
.kit-panel:hover .kit-col{border-color:color-mix(in srgb,var(--gold) 45%,transparent)}
.kit-col h4{
  display:flex;align-items:center;gap:.5rem;
  font-size:var(--fs-sm);margin-bottom:.75rem;
}
.kit-col__n{
  font-family:var(--font-mono);font-size:.66rem;letter-spacing:.1em;
  color:var(--gold);opacity:.85;
}
.kit-col .spec__list{gap:.4rem}
.kit-panel:hover .spec__list li{color:var(--text);border-color:var(--border)}
@media (max-width:900px){
  .kit-grid{grid-template-columns:repeat(2,1fr)}
}
@media (max-width:560px){
  .kit-grid{grid-template-columns:1fr}
}
