/* Switchboards motion system (sb-m), v1, 2026-10-06.
 Extracted from ai-foundations-v2.html, which keeps its own inline fj- copy for now. Guide: research/page-rebuild-2026-10/18-motion-system.md
 Contract:
 1. A trigger is any element with [data-sb-m]. motion.js adds .sb-m-in to it once, when it is on screen. Pattern classes go on the trigger or inside it. Don't nest triggers.
 2. Every hidden "before" state, transition and keyframe sits under @media screen + no-preference, and the before states also need html.sb-m-on. With no JS, reduced motion, print, or if the JS fails, every element renders in its final state.
 3. Motion uses opacity and the individual translate / rotate / scale properties, so it composes with a page's own transform (resting tilts survive) and never moves layout (CLS 0). Each move takes 400 ms or less and plays once.
 4. A pattern class owns `transition` (or `animation`) on its element. If the element already has its own, put the class on a wrapper or child instead.
 5. Delays: each pattern has a default --sb-m-d. Override it inline (style="--sb-m-d:.3s"). Inside a .sb-m-stagger child, the index × --sb-m-step (default 120 ms) is added. */
:root{--sb-m-ease:var(--sb-ease,cubic-bezier(.2,0,0,1));--sb-m-pop:cubic-bezier(.3,1.45,.5,1);--sb-m-blue:var(--sb-blue,#3044da);--sb-m-lime:var(--sb-lime,#dbf66f);--sb-m-paper:var(--sb-paper,#faf9f6);--sb-m-coral:#ff7066;--sb-m-sun:#ffdb65;--sb-m-rail:#b9c0f2}

/* ---------- Resting (final) states and defaults. These apply everywhere, with or without motion. ---------- */
/* Timing: delay = --sb-m-d + index of the nearest .sb-m-stagger child × --sb-m-step. */
.sb-m-rise,.sb-m-tilt,.sb-m-land,.sb-m-fill,.sb-m-roll>*,.sb-m-msg,.sb-m-line,.sb-m-mark,.sb-m-settle>*,.sb-m-alive>*{--sb-m-wait:calc(var(--sb-m-d,0s) + var(--sb-m-i,0)*var(--sb-m-step,120ms))}
.sb-m-rise,.sb-m-tilt,.sb-m-settle>*{--sb-m-d:0s}.sb-m-land{--sb-m-d:.55s}.sb-m-fill{--sb-m-d:.15s}.sb-m-roll{--sb-m-d:.2s}.sb-m-msg{--sb-m-d:.5s}.sb-m-line{--sb-m-d:1.5s;--sb-m-step:.2s}.sb-m-mark{--sb-m-d:.45s}.sb-m-msg .sb-m-mark{--sb-m-d:2.45s}.sb-m-settle{--sb-m-step:60ms;--sb-m-k:1}.sb-m-settle>.sb-m-answer{--sb-m-d:.45s}.sb-m-alive{--sb-m-d:.2s;--sb-m-step:90ms}
.sb-m-roll>*,.sb-m-msg,.sb-m-line,.sb-m-mark{--sb-m-i:0}
:is(.sb-m-stagger,.sb-m-settle,.sb-m-alive)>:first-child{--sb-m-i:0}
:is(.sb-m-stagger,.sb-m-settle,.sb-m-alive)>:nth-child(2){--sb-m-i:1}
:is(.sb-m-stagger,.sb-m-settle,.sb-m-alive)>:nth-child(3){--sb-m-i:2}
:is(.sb-m-stagger,.sb-m-settle,.sb-m-alive)>:nth-child(4){--sb-m-i:3}
:is(.sb-m-stagger,.sb-m-settle,.sb-m-alive)>:nth-child(5){--sb-m-i:4}
:is(.sb-m-stagger,.sb-m-settle,.sb-m-alive)>:nth-child(6){--sb-m-i:5}
:is(.sb-m-stagger,.sb-m-settle,.sb-m-alive)>:nth-child(7){--sb-m-i:6}
:is(.sb-m-stagger,.sb-m-settle,.sb-m-alive)>:nth-child(8){--sb-m-i:7}
.sb-m-line:nth-child(2 of .sb-m-line){--sb-m-i:1}
.sb-m-line:nth-child(3 of .sb-m-line){--sb-m-i:2}
.sb-m-line:nth-child(4 of .sb-m-line){--sb-m-i:3}
.sb-m-settle>.sb-m-answer{--sb-m-i:0}
@media(max-width:760px){.sb-m-settle{--sb-m-k:.45}}

/* roll: a before → after value in a one-line window, resting on "after". Markup: .sb-m-roll > span > b (before, aria-hidden) + b (after). Set --sb-m-lh to the line height. */
:where(.sb-m-roll){display:inline-block;vertical-align:top;height:var(--sb-m-lh,1.2em);line-height:var(--sb-m-lh,1.2em);overflow:hidden}
.sb-m-roll>*{display:block;translate:0 -50%}
.sb-m-roll>*>*{display:block;font-weight:inherit}
/* message: the card needs a positioned box for the typing dots (a page rule wins over this one). Dots: --sb-m-dx/--sb-m-dy place them, --sb-m-dd (.55 s) and --sb-m-dl (.95 s) time them. */
:where(.sb-m-msg){position:relative}
.sb-m-mark{color:inherit;background:linear-gradient(var(--sb-m-lime),var(--sb-m-lime)) 0 88%/100% 45% no-repeat}
.sb-m-dots{display:none;position:absolute;left:var(--sb-m-dx,44px);top:var(--sb-m-dy,42px);gap:5px}
:where(.sb-m-dots){padding:8px 12px;border-radius:12px;background:#eceef8}
.sb-m-dots>*{display:block;width:7px;height:7px;border-radius:50%}
:where(.sb-m-dots>*){background:#7c84c9}
/* track: N stops (--sb-m-n, default 3) on a rail from the first to the last; the packet parks under the last stop. */
:where(.sb-m-track){position:relative;display:grid;grid-template-columns:repeat(var(--sb-m-n,3),minmax(0,1fr))}
.sb-m-track{container-type:inline-size}
.sb-m-track:before,.sb-m-track:after{content:"";position:absolute;top:50%;left:calc(50% / var(--sb-m-n,3))}
.sb-m-track:before{right:calc(50% / var(--sb-m-n,3));height:2px;margin-top:-1px;background:var(--sb-m-rail);transform-origin:0 50%}
.sb-m-track:after{width:12px;height:12px;margin:-6px 0 0 -6px;border-radius:50%;background:var(--sb-m-lime);box-shadow:0 0 0 3px #fff,0 0 0 4px var(--sb-m-rail);opacity:0}
.sb-m-track>*{position:relative;z-index:1}
:where(.sb-m-track>*){justify-self:center;display:grid;place-items:center;width:40px;height:40px;border-radius:50%;background:var(--sb-m-blue);color:#fff;font-size:16px;font-weight:800;box-shadow:0 0 0 6px var(--sb-m-paper)}
.sb-m-fill{transform-origin:0 50%}
/* alive: a row of the four brand-mark colours. */
:where(.sb-m-alive){display:inline-flex;align-items:center;gap:4px}
:where(.sb-m-alive>*){display:block;width:7px;height:7px;border-radius:50%;background:var(--sb-m-blue);box-shadow:inset 0 0 0 1px #18181b1f}
:where(.sb-m-alive>:nth-child(2)){background:var(--sb-m-coral)}
:where(.sb-m-alive>:nth-child(3)){background:var(--sb-m-lime)}
:where(.sb-m-alive>:nth-child(4)){background:var(--sb-m-sun)}
/* door: the hover state itself is colour, not motion. The .sb-m-go arrow and .sb-m-alive dots also answer hover and focus inside any link or button, so they can go inside a card that keeps its own transitions. */
:where(.sb-m-go){display:inline-block}
@media(hover:hover){.sb-m-door:hover{border-color:var(--sb-m-blue);box-shadow:0 14px 30px #3044da1c}}

/* ---------- Motion: screen only, no reduced-motion preference. ---------- */
@media screen and (prefers-reduced-motion:no-preference){
 /* How each pattern moves to its final state. */
 .sb-m-rise{transition:opacity .38s var(--sb-m-ease) var(--sb-m-wait),translate .38s var(--sb-m-ease) var(--sb-m-wait)}
 .sb-m-tilt{transition:opacity .38s var(--sb-m-ease) var(--sb-m-wait),translate .38s var(--sb-m-ease) var(--sb-m-wait),rotate .38s var(--sb-m-ease) var(--sb-m-wait)}
 .sb-m-land{transition:opacity .2s ease var(--sb-m-wait),translate .38s var(--sb-m-pop) var(--sb-m-wait),rotate .38s var(--sb-m-pop) var(--sb-m-wait),scale .38s var(--sb-m-pop) var(--sb-m-wait)}
 .sb-m-fill{transition:scale .4s var(--sb-m-ease) var(--sb-m-wait)}
 .sb-m-roll>*{transition:translate .32s var(--sb-m-ease) var(--sb-m-wait)}
 .sb-m-msg{transition:opacity .3s var(--sb-m-ease) var(--sb-m-wait),translate .3s var(--sb-m-ease) var(--sb-m-wait),scale .3s var(--sb-m-ease) var(--sb-m-wait)}
 .sb-m-line{transition:opacity .28s var(--sb-m-ease) var(--sb-m-wait),translate .28s var(--sb-m-ease) var(--sb-m-wait)}
 .sb-m-mark{transition:background-size .35s var(--sb-m-ease) var(--sb-m-wait)}
 .sb-m-track:before{transition:scale .4s var(--sb-m-ease) .1s}
 .sb-m-settle>*{transition:translate .38s var(--sb-m-ease) var(--sb-m-wait),rotate .38s var(--sb-m-ease) var(--sb-m-wait)}
 .sb-m-settle>.sb-m-answer{transition:opacity .3s var(--sb-m-ease) var(--sb-m-wait),translate .3s var(--sb-m-ease) var(--sb-m-wait),scale .3s var(--sb-m-ease) var(--sb-m-wait)}
 .sb-m-alive>*{transition:opacity .3s ease var(--sb-m-wait),scale .3s var(--sb-m-pop) var(--sb-m-wait)}
 /* Before states: while the trigger is not .sb-m-in yet. */
 html.sb-m-on :is([data-sb-m]:not(.sb-m-in),[data-sb-m]:not(.sb-m-in) *):is(.sb-m-rise,.sb-m-tilt){opacity:0;translate:0 var(--sb-m-y,16px)}
 html.sb-m-on :is([data-sb-m]:not(.sb-m-in),[data-sb-m]:not(.sb-m-in) *).sb-m-tilt{--sb-m-y:20px;rotate:2.5deg}
 html.sb-m-on :is([data-sb-m]:not(.sb-m-in),[data-sb-m]:not(.sb-m-in) *).sb-m-land{opacity:0;translate:0 var(--sb-m-ly,-20px);rotate:var(--sb-m-lr,6deg);scale:.96}
 html.sb-m-on :is([data-sb-m]:not(.sb-m-in),[data-sb-m]:not(.sb-m-in) *).sb-m-fill{scale:0 1}
 html.sb-m-on :is([data-sb-m]:not(.sb-m-in),[data-sb-m]:not(.sb-m-in) *).sb-m-roll>*{translate:none}
 html.sb-m-on :is([data-sb-m]:not(.sb-m-in),[data-sb-m]:not(.sb-m-in) *).sb-m-msg{opacity:0;translate:0 14px;scale:.97}
 html.sb-m-on :is([data-sb-m]:not(.sb-m-in),[data-sb-m]:not(.sb-m-in) *).sb-m-line{opacity:0;translate:0 6px}
 html.sb-m-on :is([data-sb-m]:not(.sb-m-in),[data-sb-m]:not(.sb-m-in) *).sb-m-mark{background-size:0 45%}
 html.sb-m-on :is([data-sb-m]:not(.sb-m-in),[data-sb-m]:not(.sb-m-in) *).sb-m-track:before{scale:0 1}
 html.sb-m-on :is([data-sb-m]:not(.sb-m-in),[data-sb-m]:not(.sb-m-in) *).sb-m-alive>*{opacity:.25;scale:.6}
 html.sb-m-on .sb-m-settle:not(.sb-m-settled)>:not(.sb-m-answer){translate:calc(var(--sb-m-sx,0px) * var(--sb-m-k)) var(--sb-m-sy,0px);rotate:var(--sb-m-sr,0deg);animation:sb-m-drift 3.2s ease-in-out var(--sb-m-ph,0s) infinite alternate}
 html.sb-m-on .sb-m-settle:not(.sb-m-settled)>.sb-m-answer{opacity:0;translate:0 12px;scale:.96}
 /* One-off plays when the trigger comes in. */
 html.sb-m-on :is(.sb-m-in,.sb-m-in *).sb-m-track:after{animation:sb-m-packet .6s var(--sb-m-ease) .5s both}
 html.sb-m-on .sb-m-in .sb-m-dots{display:flex;animation:sb-m-life var(--sb-m-dl,.95s) linear var(--sb-m-dd,.55s) both}
 html.sb-m-on .sb-m-in .sb-m-dots>*{animation:sb-m-dot .5s ease-in-out var(--sb-m-dd,.55s) 2 alternate}
 html.sb-m-on .sb-m-in .sb-m-dots>:nth-child(2){animation-delay:calc(var(--sb-m-dd,.55s) + .16s)}
 html.sb-m-on .sb-m-in .sb-m-dots>:nth-child(3){animation-delay:calc(var(--sb-m-dd,.55s) + .32s)}
 /* motion.js sets these for one style pass: sb-m-boot when it switches motion on itself (elements below the fold go to their before state without a fade-out), sb-m-reset when a demo replays. */
 html.sb-m-boot :is([data-sb-m],[data-sb-m] *),html.sb-m-boot [data-sb-m]:before,.sb-m-reset,.sb-m-reset *,.sb-m-reset:before{transition:none!important}
 /* User-started motion (needs no JS): content revealed on click eases in; cards answer hover, focus and press. */
 :is([data-two-step=open],[data-open],details[open],.is-open) .sb-m-open{animation:sb-m-open .26s var(--sb-m-ease) both}
 .sb-m-door{transition:border-color .2s,box-shadow .2s,scale .12s var(--sb-m-ease)}
 .sb-m-press{transition:scale .12s var(--sb-m-ease)}
 .sb-m-door:active,.sb-m-press:active{scale:.98}
 .sb-m-go{transition:translate .2s var(--sb-m-ease)}
 :is(.sb-m-door,a,button):focus-visible .sb-m-go{translate:4px 0}
 :is(.sb-m-door,a,button):focus-visible .sb-m-alive>*{animation:sb-m-dot .3s ease-in-out calc(var(--sb-m-i,0)*80ms) 2 alternate}
}
@media screen and (hover:hover) and (prefers-reduced-motion:no-preference){
 :is(.sb-m-door,a,button):hover .sb-m-go{translate:4px 0}
 :is(.sb-m-door,a,button):hover .sb-m-alive>*{animation:sb-m-dot .3s ease-in-out calc(var(--sb-m-i,0)*80ms) 2 alternate}
}
@keyframes sb-m-packet{from{opacity:1}to{opacity:1;translate:calc(100cqw - 100cqw / var(--sb-m-n,3)) 0}}
@keyframes sb-m-life{0%{opacity:0}10%,85%{opacity:1}to{opacity:0;visibility:hidden}}
@keyframes sb-m-dot{to{opacity:.35;translate:0 -2px}}
@keyframes sb-m-drift{to{transform:translateY(-8px)}}
@keyframes sb-m-open{from{opacity:0;translate:0 6px;scale:.98}}
