/* LevelLink site stylesheet - 2026-09-12 rebuild.
   One file for every page. No framework, no build step, no jQuery, no
   Bootstrap: the old Mobirise pages pulled eleven dependencies to render a
   four-card grid, and every one of them was a way for the page to break.

   WATER BACKGROUND. The whole site sits on background12.jpg, the way
   changelog.html and howto_update.html already do, with content on
   translucent sheets so the water reads through without hurting legibility.
   Those pages hardcode an absolute https://www.levellink.com.au/... URL; this
   uses a RELATIVE path so the pages also render locally and on staging.

   Images stay at their existing assets/images/ paths so this drops onto the
   server root without moving any media. */

:root{
  --fg:#132030; --muted:#4f5f71; --line:#ccd8e4;
  --brand:#1259ad; --brand-dark:#0d4184;
  --sheet:rgba(255,255,255,.93);      /* main content sheet over the water */
  --card:rgba(255,255,255,.82);       /* cards sit ON the sheet - lighter */
  --bar:rgba(255,255,255,.88);        /* header / footer */
  --warn-bg:rgba(255,247,224,.94); --warn-line:#d8ae4a;
  --maxw:960px;
}

*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
  margin:0; color:var(--fg); line-height:1.6; min-height:100%;
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  background:#2f6bb3 url('images/background12.jpg') center center / cover no-repeat fixed;
}
/* background-attachment:fixed is expensive and stutters on touch devices;
   they get a normal scrolling background instead. */
@media (hover:none),(max-width:700px){
  body{background-attachment:scroll}
}
img{max-width:100%;height:auto;display:block}
a{color:var(--brand)}
a:hover{color:var(--brand-dark)}

/* ---------- header / nav ---------- */
.site-head{background:var(--bar);border-bottom:1px solid var(--line);
  position:sticky;top:0;z-index:20;backdrop-filter:saturate(140%) blur(6px)}
.site-head .wrap{max-width:var(--maxw);margin:0 auto;padding:12px 20px;
  display:flex;align-items:center;gap:20px;flex-wrap:wrap}
.brand{font-weight:700;font-size:1.15rem;color:var(--fg);text-decoration:none;letter-spacing:-.01em}
.brand span{color:var(--brand)}
nav.main{margin-left:auto;display:flex;gap:18px;flex-wrap:wrap}
nav.main a{color:var(--fg);text-decoration:none;font-size:.95rem}
nav.main a:hover,nav.main a[aria-current="page"]{color:var(--brand);text-decoration:underline}

/* ---------- hero: deliberately open water ----------
   The point of the hero is the expanse, not the furniture. No panel, no
   overlay box - just white type over the water with a shadow heavy enough to
   stay readable wherever the ripples happen to be light. */
.hero{padding:0}
.hero .wrap{max-width:var(--maxw);margin:0 auto;padding:86px 20px 96px;color:#fff;
  text-shadow:0 2px 14px rgba(6,28,54,.55),0 1px 3px rgba(6,28,54,.5)}
.hero h1{font-size:2.6rem;line-height:1.15;margin:0 0 .35em;letter-spacing:-.02em;color:#fff}
.hero p,.hero .lede{font-size:1.18rem;color:#f2f7fc;max-width:44em;margin:0}
@media(max-width:700px){ .hero .wrap{padding:56px 20px 62px} .hero h1{font-size:1.9rem} }

/* ---------- content sheet ---------- */
main{max-width:var(--maxw);margin:0 auto 44px;padding:30px 30px 48px;
  background:var(--sheet);border-radius:14px;
  box-shadow:0 6px 34px rgba(8,32,60,.22)}
@media(max-width:700px){ main{margin:0;border-radius:0;padding:22px 18px 36px} }
.narrow{max-width:760px}
h1{font-size:2rem;line-height:1.2;margin:.2em 0 .4em;letter-spacing:-.02em}
h2{font-size:1.32rem;margin:2em 0 .5em;letter-spacing:-.01em}
h3{font-size:1.05rem;margin:1.6em 0 .4em}
.lede{font-size:1.12rem;color:var(--muted);margin-top:0}
.meta{color:var(--muted);font-size:.9rem}

/* ---------- buttons ---------- */
.btn{display:inline-block;background:var(--brand);color:#fff;text-decoration:none;
  padding:11px 20px;border-radius:8px;font-weight:600;font-size:1rem;margin:6px 8px 6px 0}
.btn:hover{background:var(--brand-dark);color:#fff}
.btn.secondary{background:rgba(255,255,255,.9);color:var(--brand);border:1px solid var(--brand)}
.btn.secondary:hover{background:#fff;color:var(--brand-dark)}

/* ---------- cards ---------- */
.grid{display:grid;gap:18px;grid-template-columns:1fr}
@media(min-width:700px){.grid.two{grid-template-columns:1fr 1fr}
                        .grid.three{grid-template-columns:repeat(3,1fr)}}
@media(min-width:960px){.grid.four{grid-template-columns:repeat(4,1fr)}}
.card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:18px 20px}
.card h3{margin-top:0}
.card p:last-child{margin-bottom:0}

/* ---------- callouts ---------- */
.note{background:var(--card);border-left:4px solid var(--brand);
  border-radius:0 8px 8px 0;padding:14px 18px;margin:20px 0}
.warn{background:var(--warn-bg);border-left:4px solid var(--warn-line);
  border-radius:0 8px 8px 0;padding:14px 18px;margin:20px 0}
.note p:last-child,.warn p:last-child{margin-bottom:0}

/* ---------- steps ---------- */
ol.steps{counter-reset:step;list-style:none;padding:0;margin:24px 0}
ol.steps>li{counter-increment:step;position:relative;padding:0 0 22px 46px;margin:0}
ol.steps>li::before{content:counter(step);position:absolute;left:0;top:0;
  width:30px;height:30px;border-radius:50%;background:var(--brand);color:#fff;
  display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.95rem}
ol.steps>li h3{margin:.15em 0 .3em}

/* ---------- tables ---------- */
table{border-collapse:collapse;width:100%;font-size:.95rem;margin:18px 0}
th,td{border:1px solid var(--line);padding:9px 11px;text-align:left;vertical-align:top}
th{background:var(--card);font-weight:600}

/* ---------- footer ---------- */
.site-foot{background:var(--bar);border-top:1px solid var(--line);
  backdrop-filter:saturate(140%) blur(6px)}
.site-foot .wrap{max-width:var(--maxw);margin:0 auto;padding:24px 20px 32px;
  display:flex;gap:28px;flex-wrap:wrap;font-size:.92rem;color:var(--muted)}
.site-foot a{color:var(--muted)}
.site-foot .support{font-weight:600;color:var(--fg)}
.site-foot .support a{color:var(--brand)}

/* Dark mode keeps the water - it is the identity of the site - and darkens
   the sheets over it rather than swapping to a flat dark ground. */
@media (prefers-color-scheme: dark){
  :root{
    --fg:#e9eff6; --muted:#a7b6c6; --line:#3a4a5c;
    --brand:#7cb6f5; --brand-dark:#a9d0fa;
    --sheet:rgba(14,24,36,.90); --card:rgba(24,37,53,.86); --bar:rgba(12,21,32,.90);
    --warn-bg:rgba(48,40,20,.92); --warn-line:#8a7330;
  }
  body{background-color:#0c1520}
  .btn{color:#0b1521}
  .btn.secondary{background:rgba(20,32,46,.85)}
}

/* =====================================================================
   VIDEO HERO - full viewport, nothing but water above the fold.
   Vimeo's own ?background=1 gives a chromeless, muted, looping player
   from ONE iframe. The old site did the same thing through jQuery +
   jarallax + a Vimeo player plugin; this is the same result with no
   JavaScript and no libraries.
   ===================================================================== */
.vhero{position:relative;min-height:100svh;display:flex;align-items:flex-end;overflow:hidden}
/* .vbg is NOT tied to the hero - any positioned section can host it, which
   is what lets the video sit behind the second panel instead of the first.
   With no iframe inside it, it is simply the still water image. */
.vbg{position:absolute;inset:0;z-index:0;overflow:hidden;background:#2f6bb3
     url('images/background12.jpg') center center / cover no-repeat}
/* 16:9 cover, centred and cropped by the parent's overflow:hidden. The two
   max() values always agree on 16:9 - if 100vw is the larger width then
   56.25vw is necessarily the larger height, and vice versa - so the frame
   is never stretched. Sized against the VIEWPORT rather than the parent so
   the same rule covers both a full-height hero and a short band, neither of
   which exceeds the viewport. pointer-events off so the page scrolls over it. */
.vbg iframe{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
  width:max(100vw,177.78vh);height:max(56.25vw,100vh);border:0;pointer-events:none}
.vhero .tint{position:absolute;inset:0;z-index:1;
  background:linear-gradient(180deg,rgba(4,22,44,.42) 0%,rgba(4,22,44,.18) 38%,rgba(4,22,44,.66) 100%)}
.vhero .wrap{position:relative;z-index:2;max-width:var(--maxw);margin:0 auto;
  padding:0 20px 13vh;color:#fff;text-shadow:0 2px 18px rgba(4,20,40,.6)}
.vhero h1{color:#fff;font-size:clamp(2.1rem,6vw,4rem);line-height:1.06;margin:0 0 .3em;letter-spacing:-.025em}
.vhero p{color:#eaf3fb;font-size:clamp(1.02rem,2.1vw,1.3rem);max-width:34em;margin:0}
.vhero .btn{margin-top:26px}

/* Probe descending the right-hand edge of a hero and continuing off the
   bottom, clipped by the panel's overflow:hidden at both ends.

   The IMAGE is pre-rotated (probe-down-*.png) rather than rotated in CSS.
   transform is a single property, so a CSS rotation would have to be
   re-declared in every keyframe of the descent - miss one and the probe
   silently flips mid-fall. Pre-rotating removes that failure entirely and
   leaves translateY meaning exactly what it says.

   Deliberately NOT scroll-driven. animation-timeline: view() would be the
   elegant version and needs no JavaScript, but Safari does not support
   scroll-driven animations and this is an iOS-first product - it would do
   nothing for much of the audience.

   Hidden below 1100px: the hero .wrap is 960px wide, so under that there is
   no side margin left and the probe would cross the heading. */
/* A SPAN wrapping the image, not the image itself: pseudo-elements do not
   work on replaced elements, and the cable has to be a ::before. Making the
   cable a child also means it travels with the probe and disappears with it
   on the fade, so it clears itself with no extra timing to keep in step.

   `backwards` is essential, not decoration. During an animation-delay an
   element renders with its NORMAL styles, not the first keyframe - so
   without it the probe sits plainly visible at top:0 for the whole delay,
   then snaps out of sight when the animation finally starts. Visible, then
   gone, then falling. `backwards` applies the 0% keyframe during the wait. */
.vhero .probe-hang{position:absolute;z-index:2;right:5%;top:0;
  width:clamp(58px,6.5vw,96px);pointer-events:none;
  animation:probedrop 17s cubic-bezier(.5,.02,.55,1) 2.5s infinite backwards}
.vhero .probe-hang img{display:block;width:100%;height:auto;
  filter:drop-shadow(0 18px 30px rgba(2,14,28,.55))}
/* The cable, trailing up from the gland to well above the viewport. 200vh
   clears the longest fall (118vh) with the probe's own height to spare.

   Photographic width was 26.9% (68px of cable against a 253px body), centred.
   That measured right and looked wrong: against the rendered probe it sat
   6px too far left and 1px too wide on the right. Corrected to span 36.3 ->
   51.7px of the 83px probe, i.e. 18.5% wide centred at 53%. Kept as
   percentages so the correction scales with the probe rather than being
   pinned to the viewport it was judged at. */
.vhero .probe-hang::before{content:"";position:absolute;left:53%;bottom:99%;
  transform:translateX(-50%);width:18.5%;height:200vh;
  background:linear-gradient(180deg,#080a0d 0%,#15191f 100%);
  box-shadow:0 0 10px rgba(2,10,20,.5)}
/* Travel is in vh so it always clears the hero whatever its height; the
   starting -115% is of the probe's OWN height, so it begins fully hidden. */
@keyframes probedrop{
  0%,3%    {transform:translateY(-115%);opacity:0}
  9%       {opacity:1}
  60%      {opacity:1}
  74%,100% {transform:translateY(78vh);opacity:0}
}
/* The home page hero is a full 100svh, so the probe has considerably further
   to fall before it clears the bottom edge - reusing the 78vh travel above
   would strand it mid-screen. It also waits longer before starting, because
   that page opens deliberately still and the movement should arrive into
   stillness rather than greet you.

   A second keyframe set rather than a var() inside the existing one: custom
   properties in @keyframes are substituted before interpolation and engine
   support for that has been uneven. Explicit is worth the extra six lines. */
.vhero .probe-hang.late{right:3%;
  animation-name:probedropTall;animation-duration:21s;animation-delay:5s}
@keyframes probedropTall{
  0%,4%    {transform:translateY(-115%);opacity:0}
  10%      {opacity:1}
  64%      {opacity:1}
  78%,100% {transform:translateY(118vh);opacity:0}
}
@media(max-width:1099px){ .vhero .probe-hang{display:none} }
@media (prefers-reduced-motion: reduce){
  .vhero .probe-hang{animation:none;opacity:.9;transform:translateY(6vh)}
}

/* Brand-only opening panel: the name, one line, and water. Nothing else. */
.vhero .wrap.brandonly{text-align:center;padding-bottom:0;
  position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:100%}

/* The wordmark keeps EXACTLY its original treatment - solid white, the
   "Link" half pale blue, the parent's text-shadow carrying legibility.
   The shimmer is ADDITIVE: a duplicate of the word sits over the top in a
   pseudo-element, and only that copy is clipped to a moving highlight.

   The previous version clipped the REAL text and set text-fill-color to
   transparent. That had two faults: it discarded the two-tone colouring,
   and any failure to paint the gradient left nothing on screen at all,
   because transparent fill wins over the colour fallback. Here the visible
   text is never made transparent - if the overlay fails to render, the
   wordmark is simply unshimmered. */
.vhero .wrap.brandonly h1{position:relative;font-size:clamp(2.6rem,9vw,6rem);
  margin:0;letter-spacing:-.03em;font-weight:700;color:#fff;
  animation:brandin 1.1s cubic-bezier(.2,.7,.3,1) both}
.vhero .wrap.brandonly h1 em{font-style:normal;color:#bfe2ff}

/* The overlay copy, drawn from data-text on the h1. It inherits font size,
   weight, letter-spacing and centring from the heading, so it lands exactly
   on top of the real letters. text-shadow MUST be cleared: this copy is
   transparent-filled, so an inherited shadow would show through it.

   THE BAND IS BLUE, NOT WHITE, AND THAT IS THE WHOLE POINT. The first
   version swept rgba(255,255,255,.92) across text that is already pure
   white: mechanically perfect - clip applied, animation running, position
   sweeping - and completely invisible, because white light on white text
   has no contrast. The base is the brightest thing on the page, so the
   highlight cannot be brighter; it has to be a COOL TINT instead, reading
   as light refracting through water rather than as a lamp passing over.
   The drop-shadow adds a halo that travels with the lit band, which is
   what sells it as light rather than as a smudge. */
.vhero .wrap.brandonly h1::after{
  content:attr(data-text);
  position:absolute;left:0;right:0;top:0;pointer-events:none;
  background-image:linear-gradient(100deg,
    rgba(255,255,255,0) 45%,
    rgba(255,255,255,1) 50%,
    rgba(255,255,255,0) 55%);
  background-size:300% 100%;background-repeat:no-repeat;
  -webkit-background-clip:text;background-clip:text;
  color:transparent;-webkit-text-fill-color:transparent;text-shadow:none;
  /* THE GLOW IS THE EFFECT, NOT THE FILL. The band's glyph pixels are pure
     white over text that is already pure white, so they change nothing -
     which is the point: no tinting, genuinely additive. What you see is
     this drop-shadow, which applies to the element's RENDERED output (the
     clipped band) and therefore travels with it, throwing a halo onto the
     dark water around whichever letters are currently lit.

     Two earlier attempts failed here. A white band on white text was
     invisible. Adding blue flanks made it visible but as a smudge, because
     on a white base any visible colour must be DARKER than the text - a
     brighter highlight is arithmetically impossible. The halo escapes that
     trap by landing on the background instead of on the glyphs. */
  filter:drop-shadow(0 0 16px rgba(190,232,255,.95))
         drop-shadow(0 0 30px rgba(130,195,255,.55));
  animation:shimmer 6s ease-in-out 1.1s infinite}
@keyframes brandin{
  from{opacity:0;transform:translateY(14px)}
  to  {opacity:1;transform:none}
}
@keyframes shimmer{
  0%,100%{background-position:140% 0}
  50%    {background-position:-40% 0}
}
@media (prefers-reduced-motion: reduce){
  .vhero .wrap.brandonly h1{animation:none}
  .vhero .wrap.brandonly h1::after{animation:none;opacity:0}
}

/* Product shots. object-fit:contain inside a fixed-ratio panel because the
   source images have very different aspect ratios (1.70, 0.95, 1.33) and
   the box PNG is transparent - cropping them to a common ratio would cut
   the product itself. */
.shots{display:grid;gap:18px;grid-template-columns:1fr;margin:22px 0 6px}
@media(min-width:700px){ .shots{grid-template-columns:repeat(3,1fr)} }
.shots figure{margin:0}
.shots .frame{aspect-ratio:4/3;border-radius:12px;border:1px solid var(--line);
  background:var(--card);display:grid;place-items:center;padding:14px;overflow:hidden}
.shots .frame img{max-width:100%;max-height:100%;width:auto;object-fit:contain}
.shots figcaption{margin-top:8px;color:var(--muted);font-size:.9rem;text-align:center}
.vhero .wrap.brandonly p{margin:.6em auto 0;font-size:clamp(.95rem,2.2vw,1.25rem);
  letter-spacing:.22em;text-transform:uppercase;color:#dceaf6;max-width:none}

/* Second panel: the proposition, and where the water moves. The image is
   NOT repeated here - the nested .vbg supplies it beneath the video, so the
   section only needs a colour for the instant before .vbg paints.
   min-height gives the video room to read as water rather than a strip. */
/* Full viewport, matching .vhero above it - a shorter second panel read as a
   narrow band rather than as a continuation of the opening. */
.stillwater{position:relative;overflow:hidden;background:#2f6bb3;
  min-height:100svh;display:flex;align-items:center}
.stillwater .tint{position:absolute;inset:0;z-index:1;background:rgba(6,30,56,.48)}
/* width:100% because .stillwater is now a flex container - without it the
   wrap sizes to its content instead of filling out to --maxw. */
.stillwater .wrap{position:relative;z-index:2;max-width:var(--maxw);width:100%;
  margin:0 auto;padding:64px 20px 70px;color:#fff;text-shadow:0 2px 16px rgba(4,22,44,.55)}
.stillwater h2{color:#fff;margin:0 0 .4em;font-size:clamp(1.6rem,4.2vw,2.5rem);letter-spacing:-.02em}
.stillwater p{color:#e9f3fb;font-size:clamp(1rem,2vw,1.18rem);max-width:40em}

/* App preview video panel - self-hosted, so it works in every browser and
   cannot break when Apple rotates its CDN asset ids. */
.videopanel{position:relative;overflow:hidden;
  background:#10486f url('images/mbr-1920x1188.jpg') center center / cover no-repeat}
.videopanel .tint{position:absolute;inset:0;background:rgba(5,34,56,.55)}
.videopanel .wrap{position:relative;z-index:2;max-width:var(--maxw);margin:0 auto;
  padding:56px 20px 62px;color:#fff;text-shadow:0 2px 14px rgba(4,26,46,.5)}
.videopanel h2{color:#fff;margin-top:0}
.videopanel p{color:#e9f3fb;max-width:44em}
.videopanel .frame{margin:26px auto 0;max-width:760px;border-radius:14px;overflow:hidden;
  box-shadow:0 18px 50px rgba(3,20,38,.5);background:#02121f;line-height:0}
.videopanel video{width:100%;height:auto;display:block}

/* Variant for the FIRST of the two app panels, so two video sections running
   back to back do not read as the same slab repeated. Deeper ground, and the
   still water image rather than the second photograph. */
.videopanel.usage{background:#0b3554 url('images/background12.jpg') center 60% / cover no-repeat}
.videopanel.usage .tint{background:linear-gradient(180deg,rgba(3,22,40,.72) 0%,rgba(3,22,40,.58) 100%)}
.videopanel.usage .frame{max-width:820px}

/* .stores was previously scoped to .apppanel only, so the identical markup
   inside .videopanel got no layout at all. Now generic. */
.stores{display:flex;gap:12px;flex-wrap:wrap;margin-top:22px;align-items:center}
/* Brand glyphs inside store buttons. fill, not stroke - these are solid
   marks, unlike the outline icons in .feat .ico. Sized well above the cap
   height of the label beside them: at 16px the Apple mark read as a bullet
   point rather than as a brand. */
.stores .btn svg,.badge-soon svg{width:25px;height:25px;fill:currentColor;
  stroke:none;vertical-align:-7px;margin-right:9px}
/* The badge stacks "Coming soon" under its label, so its glyph needs to sit
   against the first line rather than the block as a whole. */
.badge-soon svg{vertical-align:-6px;margin-right:8px}

/* Small utilities used by a couple of pages. `center` was inherited from the
   old hand-written pages' own stylesheets, which no longer exist. */
.center{text-align:center}

/* =====================================================================
   FEATURE CARDS - the four-up and two-up panels on the home, how-it-works
   and support pages. A plain .card is a bordered box with paragraphs in
   it, which is why a grid of them reads as a dump rather than a design.
   These add an icon chip, an accent edge and a hover lift.

   .card is now UNUSED - zero occurrences across all ten pages, hardware-b
   .html having been the last holdout. The rule above is kept deliberately
   rather than deleted: the rebuild is not live yet, and four lines is
   cheap insurance for a reverted or newly added page. Delete it once the
   site is up and .card is still unused.
   ===================================================================== */
.feat{position:relative;background:var(--card);border:1px solid var(--line);
  border-radius:14px;padding:24px 20px 20px;overflow:hidden;
  transition:transform .25s ease,box-shadow .25s ease,border-color .25s ease}
.feat::before{content:"";position:absolute;top:0;left:0;right:0;height:3px;
  background:linear-gradient(90deg,var(--brand),#5fb3f7)}
.feat:hover{transform:translateY(-3px);border-color:var(--brand);
  box-shadow:0 14px 34px rgba(8,32,60,.2)}
.feat .ico{width:44px;height:44px;border-radius:12px;display:grid;place-items:center;
  background:rgba(18,89,173,.13);color:var(--brand);margin-bottom:14px}
/* fill:none + currentColor means one icon set works on both themes. */
.feat .ico svg{width:23px;height:23px;fill:none;stroke:currentColor;
  stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
.feat h3{margin:0 0 .35em;font-size:1.05rem;letter-spacing:-.01em}
.feat p{margin:0;color:var(--muted);font-size:.95rem}
@media (prefers-color-scheme: dark){ .feat .ico{background:rgba(124,182,245,.16)} }
@media (prefers-reduced-motion: reduce){
  .feat{transition:none}
  .feat:hover{transform:none}
}

/* Small uppercase label that sits above a heading. */
.eyebrow{text-transform:uppercase;letter-spacing:.2em;font-size:.74rem;
  font-weight:600;color:var(--brand);margin:2.4em 0 .2em}
.eyebrow+h2{margin-top:0}

/* HelpfulCrowd Q&A mount wrapper. The old page gave this an inline
   background-color:#549CFF; that is dropped deliberately - the widget draws
   its own chrome and a hard blue block fought the water ground. This just
   gives it a readable sheet to sit on and room to breathe. */
.hc-widget{background:var(--card);border:1px solid var(--line);border-radius:10px;
  padding:18px 20px;min-height:120px}
.hc-widget:empty::after,.hc-widget > div:empty::after{
  content:"Loading questions\2026";color:var(--muted);font-size:.95rem}

/* Scroll cue - the page is deliberately empty above the fold, so say so. */
.scrollcue{position:absolute;left:50%;bottom:26px;z-index:3;transform:translateX(-50%);
  color:#fff;opacity:.85;font-size:.82rem;letter-spacing:.09em;text-transform:uppercase;
  text-align:center;text-shadow:0 1px 8px rgba(4,20,40,.7)}
.scrollcue span{display:block;font-size:1.5rem;line-height:1;margin-top:2px;animation:nudge 2.1s ease-in-out infinite}
@keyframes nudge{0%,100%{transform:translateY(0)}50%{transform:translateY(7px)}}

/* =====================================================================
   DRIFTING CLOUD BAND - pure CSS, no image, no dependency. Soft blurred
   ellipses crossing a sky gradient at different speeds and heights.
   ===================================================================== */
.clouds{position:relative;overflow:hidden;min-height:230px;
  background:linear-gradient(180deg,#9ecbef 0%,#c9e4f8 45%,#eaf5fd 100%)}
.clouds .cloud{position:absolute;background:rgba(255,255,255,.92);border-radius:50%;
  filter:blur(14px);will-change:transform;animation:drift linear infinite}
.clouds .cloud::after{content:"";position:absolute;top:-38%;left:22%;width:62%;height:110%;
  background:inherit;border-radius:50%}
.clouds .c1{width:300px;height:74px;top:16%;animation-duration:64s}
.clouds .c2{width:210px;height:54px;top:44%;animation-duration:92s;animation-delay:-22s;opacity:.85}
.clouds .c3{width:390px;height:92px;top:64%;animation-duration:78s;animation-delay:-48s;opacity:.75}
.clouds .c4{width:160px;height:44px;top:30%;animation-duration:110s;animation-delay:-70s;opacity:.7}
@keyframes drift{from{transform:translateX(-42vw)}to{transform:translateX(142vw)}}
.clouds .wrap{position:relative;z-index:2;max-width:var(--maxw);margin:0 auto;
  padding:54px 20px;text-align:center;color:#123047}
.clouds h2{margin-top:0;color:#0e2b42}
.clouds p{max-width:40em;margin:0 auto;color:#24455e}

/* =====================================================================
   SPLASH PANEL - transparent-background splash art beside the probe.
   ===================================================================== */
.splash{background:var(--sheet);border-radius:14px;box-shadow:0 6px 34px rgba(8,32,60,.22);
  max-width:var(--maxw);margin:0 auto 44px;padding:34px 30px;overflow:hidden}
.splash .inner{display:grid;gap:26px;grid-template-columns:1fr;align-items:center}
@media(min-width:760px){.splash .inner{grid-template-columns:1.05fr .95fr}}
.splash .art{position:relative;text-align:center}
.splash .art .splashimg{margin:0 auto;max-height:290px;width:auto;
  animation:bob 7s ease-in-out infinite}
/* The cut-out probe is 4.64:1 where the old photograph was 0.75:1, so the
   same max-height rendered it about 65px wide and spindly. Taller, and less
   negative overlap, because there is no longer a wide body to tuck under the
   splash art. */
.splash .art .probe{margin:-18px auto 0;max-height:380px;width:auto;
  filter:drop-shadow(0 14px 24px rgba(8,32,60,.3))}
@keyframes bob{0%,100%{transform:translateY(0) scale(1)}50%{transform:translateY(-9px) scale(1.015)}}
@media(max-width:759px){.splash{border-radius:0;margin-left:0;margin-right:0}}

/* =====================================================================
   APP PANEL - the SECOND water backdrop, distinct from the site ground.
   ===================================================================== */
.apppanel{position:relative;overflow:hidden;
  background:#1a8fc4 url('images/mbr-1920x1188.jpg') center center / cover no-repeat}
.apppanel .tint{position:absolute;inset:0;background:rgba(6,42,66,.42)}
.apppanel .wrap{position:relative;z-index:2;max-width:var(--maxw);margin:0 auto;
  padding:58px 20px;color:#fff;text-shadow:0 2px 14px rgba(4,26,46,.55)}
.apppanel h2{color:#fff;margin-top:0}
.apppanel p{color:#eaf4fb;max-width:44em}
.apppanel .stores{display:flex;gap:12px;flex-wrap:wrap;margin-top:22px;align-items:center}
.badge-soon{display:inline-block;border:1px dashed rgba(255,255,255,.75);color:#fff;
  border-radius:8px;padding:10px 16px;font-weight:600;font-size:.95rem}
.badge-soon small{display:block;font-weight:400;opacity:.85;font-size:.8rem}

/* =====================================================================
   ALERT PANEL - the app speaking, shown as iOS-style notifications
   dropping in. Pure CSS on a loop: no JS, no library, nothing to fail.
   The tint is a ::before rather than a .tint div - this panel needs no
   extra markup for it.
   ===================================================================== */
.alerts{position:relative;overflow:hidden;
  background:#0e3b60 url('images/mbr-1920x1188.jpg') center 40% / cover no-repeat}
.alerts::before{content:"";position:absolute;inset:0;
  background:linear-gradient(180deg,rgba(4,26,46,.80) 0%,rgba(4,26,46,.66) 100%)}
.alerts .wrap{position:relative;z-index:2;max-width:var(--maxw);margin:0 auto;
  padding:64px 20px 72px;display:grid;gap:34px;grid-template-columns:1fr;
  align-items:center;color:#fff;text-shadow:0 2px 14px rgba(4,22,44,.5)}
@media(min-width:820px){ .alerts .wrap{grid-template-columns:1.02fr .98fr;gap:46px} }
.alerts .eyebrow{color:#9ed2ff;margin:0 0 .5em}
.alerts h2{color:#fff;margin:0 0 .45em;font-size:clamp(1.6rem,3.6vw,2.3rem);letter-spacing:-.02em}
.alerts p{color:#e9f3fb;max-width:34em}
.alerts ul{margin:20px 0 0;padding:0;list-style:none}
.alerts li{position:relative;padding:7px 0 7px 26px;color:#e9f3fb;font-size:.97rem}
.alerts li strong{color:#fff}
.alerts li::before{content:"";position:absolute;left:3px;top:15px;width:8px;height:8px;
  border-radius:50%;background:#5fb3f7;box-shadow:0 0 0 4px rgba(95,179,247,.22)}

/* =====================================================================
   STORE BADGES. The generic .btn read as a plain blue rectangle next to
   product photography. These are centred, dark, and carry the mark at a
   size where it is recognisably the brand rather than a bullet point.
   .btn and .badge-soon are left alone - info.html still uses both.
   ===================================================================== */
.stores{justify-content:center}
.storebtn{display:inline-flex;align-items:center;gap:12px;min-width:196px;
  background:linear-gradient(180deg,#1b2836 0%,#0b131c 100%);color:#fff;
  text-decoration:none;border:1px solid rgba(255,255,255,.22);border-radius:13px;
  padding:10px 22px 10px 17px;box-shadow:0 10px 26px rgba(2,14,28,.45);
  transition:transform .22s ease,box-shadow .22s ease,border-color .22s ease}
.storebtn:hover{color:#fff;transform:translateY(-3px);
  border-color:rgba(150,210,255,.8);
  box-shadow:0 16px 36px rgba(2,14,28,.55),0 0 0 3px rgba(95,179,247,.2)}
.storebtn svg{width:28px;height:28px;fill:currentColor;stroke:none;flex:none}
.storebtn span{display:flex;flex-direction:column;line-height:1.16;text-align:left}
.storebtn small{font-size:.7rem;opacity:.72;letter-spacing:.02em}
.storebtn strong{font-size:1.04rem;font-weight:600;letter-spacing:-.01em}
/* Not a link yet - no lift, no pointer, visibly pending. */
.storebtn.soon{opacity:.6;cursor:default}
.storebtn.soon:hover{transform:none;border-color:rgba(255,255,255,.22);
  box-shadow:0 10px 26px rgba(2,14,28,.45)}
@media (prefers-reduced-motion: reduce){
  .storebtn{transition:none} .storebtn:hover{transform:none}
}

/* =====================================================================
   REMOTE PANEL - the connection drawn, not described.
   ===================================================================== */
.remote{position:relative;overflow:hidden;
  background:#0c3253 url('images/background12.jpg') center 30% / cover no-repeat}
.remote::before{content:"";position:absolute;inset:0;
  background:linear-gradient(180deg,rgba(4,24,44,.82) 0%,rgba(4,24,44,.70) 100%)}
.remote .wrap{position:relative;z-index:2;max-width:var(--maxw);margin:0 auto;
  padding:62px 20px 68px;display:grid;gap:30px;grid-template-columns:1fr;
  align-items:center;color:#fff;text-shadow:0 2px 14px rgba(4,22,44,.5)}
@media(min-width:860px){ .remote .wrap{grid-template-columns:.95fr 1.05fr;gap:42px} }
.remote .eyebrow{color:#9ed2ff;margin:0 0 .5em}
.remote h2{color:#fff;margin:0 0 .45em;font-size:clamp(1.6rem,3.6vw,2.3rem);letter-spacing:-.02em}
.remote p{color:#e9f3fb;max-width:34em}
.remote-art svg{display:block;width:100%;height:auto;overflow:visible}
/* Flowing dashes. 10+14=24 per period; -48 is exactly two periods, so the
   restart is invisible. */
.remote-line{stroke-dasharray:10 14;animation:flow 1.7s linear infinite}
.remote-line.d2{animation-delay:-.85s}
@keyframes flow{ to{stroke-dashoffset:-48} }
@media (prefers-reduced-motion: reduce){
  .remote-line{animation:none;stroke-dasharray:none;opacity:.85}
}

/* =====================================================================
   HARDWARE B - board banner, raytrace figure, and the solar add-on panel.
   ===================================================================== */

/* The spin video and the raytrace share one figure treatment so they read as a
   matched pair at the same width. The full-bleed .pcbbanner variant was
   removed when the video moved inline - it is no longer used anywhere. */
.pcbshot{margin:26px 0 10px}
.pcbshot img,.pcbshot video{display:block;width:100%;height:auto;border-radius:12px;
  border:1px solid var(--line);box-shadow:0 14px 34px rgba(8,32,60,.22);background:#0a1622}
.pcbshot figcaption{margin-top:10px;color:var(--muted);font-size:.9rem;text-align:center}

/* Explanatory diagrams. Colours come from site tokens so the SVG follows dark
   mode with the rest of the page rather than staying stubbornly light. */
.diagram{margin:24px 0 10px;padding:16px 14px 10px;background:var(--card);
  border:1px solid var(--line);border-radius:12px}
.diagram svg{display:block;width:100%;height:auto}
.diagram figcaption{margin-top:8px;color:var(--muted);font-size:.9rem;text-align:center}

.solar{position:relative;overflow:hidden;
  background:#0d3a5c url('images/mbr-1920x1188.jpg') center 45% / cover no-repeat}
.solar::before{content:"";position:absolute;inset:0;
  background:linear-gradient(180deg,rgba(4,26,46,.86) 0%,rgba(4,26,46,.74) 100%)}
.solar .wrap{position:relative;z-index:2;max-width:var(--maxw);margin:0 auto;
  padding:62px 20px 68px;display:grid;gap:32px;grid-template-columns:1fr;
  align-items:center;color:#fff;text-shadow:0 2px 14px rgba(4,22,44,.5)}
@media(min-width:880px){ .solar .wrap{grid-template-columns:1.25fr .75fr;gap:44px} }
.solar .eyebrow{color:#ffd479;margin:0 0 .5em}
.solar h2{color:#fff;margin:0 0 .45em;font-size:clamp(1.6rem,3.6vw,2.3rem);letter-spacing:-.02em}
.solar p{color:#e9f3fb;max-width:38em}
.solar .meta{color:#a9c6de;font-size:.86rem}
/* The table sits on dark ground here, so the site's default light-sheet
   table styling has to be overridden rather than inherited. */
.solar table{margin:20px 0;background:rgba(255,255,255,.06);border-radius:10px;overflow:hidden}
.solar th,.solar td{border-color:rgba(255,255,255,.18);color:#e9f3fb;text-shadow:none;
  font-size:.93rem;padding:9px 12px}
.solar th{background:rgba(255,255,255,.10);color:#fff}
.solar td strong{color:#ffd479}
.solar .warn{background:rgba(255,214,120,.14);border-left-color:#ffc93c}
.solar .warn p{color:#fff4d8;max-width:none}

/* =====================================================================
   PANEL SYSTEM. The rhythm index.html uses: full-bleed sections that
   alternate ground, each with an eyebrow, a heading and room for a graphic
   beside the copy. .solar and .devpanel below predate this and are its two
   worked examples; this generalises them so a whole page can be built from
   the same parts instead of a narrow text column with panels bolted on.

   .dark  = water photograph under a heavy tint, white type
   .light = the standard content sheet, normal type
   .split = two columns above 880px, stacked below
   ===================================================================== */
.panel{position:relative;overflow:hidden}
.panel .wrap{position:relative;z-index:2;max-width:var(--maxw);margin:0 auto;
  padding:58px 20px 64px}
.panel.split .wrap{display:grid;gap:32px;grid-template-columns:1fr;align-items:center}
@media(min-width:880px){
  .panel.split .wrap{grid-template-columns:1.04fr .96fr;gap:44px}
  .panel.split.wide .wrap{grid-template-columns:.8fr 1.2fr}
}
.panel .eyebrow{margin:0 0 .5em}
.panel h2{margin:0 0 .45em;font-size:clamp(1.55rem,3.5vw,2.25rem);letter-spacing:-.02em}
.panel h3{font-size:1.04rem;letter-spacing:-.01em}
.panel .art svg,.panel .art img,.panel .art video{display:block;width:100%;height:auto}

/* EVERY panel sits on the water. The earlier version alternated water against
   var(--sheet), which is defined to follow the page background - so in dark
   mode the "light" panels came out as a slightly different dark and the
   rhythm read as mud rather than as contrast. The modifiers now vary only the
   TINT DEPTH over the same ground, which holds its relationship in both
   themes because it never touches a theme token. */
.panel{background:#0b2f4d url('images/background12.jpg') center 35% / cover no-repeat}
.panel::before{content:"";position:absolute;inset:0;
  background:linear-gradient(180deg,rgba(4,22,40,.86) 0%,rgba(4,22,40,.74) 100%)}
.panel.light::before{background:linear-gradient(180deg,rgba(6,32,56,.72) 0%,rgba(6,32,56,.60) 100%)}
.panel.dark::before{background:linear-gradient(180deg,rgba(3,18,34,.90) 0%,rgba(3,18,34,.80) 100%)}
.panel .wrap{color:#fff;text-shadow:0 2px 14px rgba(4,22,44,.5)}
.panel .eyebrow{color:#9ed2ff}
.panel h2,.panel h3{color:#fff}
.panel p,.panel li{color:#e9f3fb}
.panel .meta{color:#a9c6de}
.panel strong{color:#fff}
/* .feat is built for the light content sheet - on water it needs its own
   surface, or the cards vanish into the tint. */
.panel .feat{background:rgba(255,255,255,.09);border-color:rgba(255,255,255,.20);
  text-shadow:none}
.panel .feat:hover{background:rgba(255,255,255,.13);border-color:rgba(150,210,255,.65)}
.panel .feat h3{color:#fff}
.panel .feat p{color:#dce9f5}
.panel .feat .ico{background:rgba(126,194,255,.18);color:#9ed2ff}
.panel .note{background:rgba(255,255,255,.09);border-left-color:#5fb3f7}
.panel .note p{color:#eaf4fd;text-shadow:none}
.panel .diagram{background:rgba(255,255,255,.07);border-color:rgba(255,255,255,.18)}
.panel .diagram figcaption{color:#bcd6ea}
.panel table{background:rgba(255,255,255,.06);border-radius:10px;overflow:hidden}
.panel th,.panel td{border-color:rgba(255,255,255,.18);color:#e9f3fb;text-shadow:none}
.panel th{background:rgba(255,255,255,.10);color:#fff}

/* Bulleted lists inside panels, matching .solar / .devpanel. */
.panel .dotlist{margin:18px 0 14px;padding:0;list-style:none}
.panel .dotlist li{position:relative;padding:8px 0 8px 26px;font-size:.96rem;max-width:38em}
.panel .dotlist li::before{content:"";position:absolute;left:3px;top:16px;width:8px;height:8px;
  border-radius:50%;background:#5fb3f7;box-shadow:0 0 0 4px rgba(95,179,247,.22)}

/* =====================================================================
   DEVICE SETTINGS PANEL - tall screenshot beside an explanatory column.
   The image column is placed SECOND in the markup so it reads first on a
   phone, where the grid collapses and a 945px-tall screenshot ahead of the
   text would push every word below the fold.
   ===================================================================== */
.devpanel{position:relative;overflow:hidden;
  background:#0b2f4d url('images/background12.jpg') center 35% / cover no-repeat}
.devpanel::before{content:"";position:absolute;inset:0;
  background:linear-gradient(180deg,rgba(4,22,40,.86) 0%,rgba(4,22,40,.76) 100%)}
.devpanel .wrap{position:relative;z-index:2;max-width:var(--maxw);margin:0 auto;
  padding:62px 20px 68px;display:grid;gap:32px;grid-template-columns:1fr;
  align-items:center;color:#fff;text-shadow:0 2px 14px rgba(4,22,44,.5)}
@media(min-width:900px){
  .devpanel .wrap{grid-template-columns:.82fr 1.18fr;gap:44px;align-items:start}
  .devpanel .devshot{position:sticky;top:24px}
}
.devpanel .eyebrow{color:#9ed2ff;margin:0 0 .5em}
.devpanel h2{color:#fff;margin:0 0 .45em;font-size:clamp(1.6rem,3.6vw,2.3rem);letter-spacing:-.02em}
.devpanel p{color:#e9f3fb;max-width:38em}
.devpanel .meta{color:#a9c6de;font-size:.86rem}
.devpanel ul{margin:18px 0 14px;padding:0;list-style:none}
.devpanel li{position:relative;padding:8px 0 8px 26px;color:#e9f3fb;font-size:.96rem;
  max-width:38em}
.devpanel li strong{color:#fff}
.devpanel li::before{content:"";position:absolute;left:3px;top:16px;width:8px;height:8px;
  border-radius:50%;background:#5fb3f7;box-shadow:0 0 0 4px rgba(95,179,247,.22)}
.devshot img{display:block;width:100%;height:auto;border-radius:12px;
  border:1px solid rgba(255,255,255,.22);box-shadow:0 18px 46px rgba(2,14,28,.55)}

.solar-art svg{display:block;width:100%;height:auto;overflow:visible}
/* Rays breathe; the feed line flows. 6+10=16 per period, -32 is two whole
   periods so the restart is invisible. */
.sunrays{animation:sunpulse 3.4s ease-in-out infinite;transform-box:fill-box;
  transform-origin:center}
@keyframes sunpulse{0%,100%{opacity:.55;transform:scale(.96)}50%{opacity:1;transform:scale(1.04)}}
.sunbeam{opacity:.75;animation:sunpulse 3.4s ease-in-out .4s infinite}
.solar-line{stroke-dasharray:6 10;animation:flow 1.5s linear infinite}
@media (prefers-reduced-motion: reduce){
  .sunrays,.sunbeam{animation:none;opacity:.9;transform:none}
  .solar-line{animation:none;stroke-dasharray:none}
}

.alerts-phone{display:grid;gap:12px}
.notif{display:flex;gap:12px;align-items:flex-start;opacity:0;
  background:rgba(248,251,255,.95);color:#10202f;border-radius:18px;
  padding:13px 15px;box-shadow:0 16px 40px rgba(2,16,32,.45);text-shadow:none;
  animation:notifDrop 9s cubic-bezier(.2,.8,.3,1) infinite}
.notif.n1{animation-delay:.4s}
.notif.n2{animation-delay:2.4s}
.notif img{width:40px;height:40px;border-radius:10px;flex:none}
.notif .ntext{min-width:0}
.notif .nhead{display:flex;align-items:baseline;gap:8px}
.notif .nhead strong{font-size:.86rem;letter-spacing:.01em}
.notif .nhead span{margin-left:auto;color:#6b7d8e;font-size:.76rem}
.notif p{margin:3px 0 0;max-width:none;color:#243545;font-size:.92rem;line-height:1.42}
@keyframes notifDrop{
  0%  {opacity:0;transform:translateY(-30px) scale(.95)}
  6%  {opacity:1;transform:translateY(0) scale(1)}
  78% {opacity:1;transform:translateY(0) scale(1)}
  88% {opacity:0;transform:translateY(-16px) scale(.97)}
  100%{opacity:0;transform:translateY(-30px) scale(.95)}
}
@media (prefers-reduced-motion: reduce){
  .notif{animation:none;opacity:1;transform:none}
}

/* =====================================================================
   REVEAL ON SCROLL. Default state is VISIBLE: the hiding class is only
   applied once the script has run, so with JavaScript off or broken the
   page is fully readable rather than blank.
   ===================================================================== */
.js-reveal .reveal{opacity:0;transform:translateY(16px);
  transition:opacity .7s ease,transform .7s ease}
.js-reveal .reveal.in{opacity:1;transform:none}

@media (prefers-reduced-motion: reduce){
  .clouds .cloud,.scrollcue span,.splash .art .splashimg{animation:none}
  .js-reveal .reveal{opacity:1;transform:none;transition:none}
}

/* =====================================================================
   INSTALL SEQUENCE ANIMATION  (install.html only)

   One SVG scene, pure CSS keyframes, no library and no JavaScript. Every
   animated element runs the SAME 18s duration on an infinite loop and
   moves inside its own percentage window, holding still before and after.
   One clock, so the picture and the words cannot drift apart.

   EVERY name is prefixed sa- / sa. That is not decoration - three real
   collisions exist without it: install.html already carries id="probe"
   and id="box" on its list items, this file already defines .note and
   .wrap, and `@keyframes drift` above already drives the cloud band.
   An unprefixed name would be silently hijacked by whichever definition
   came last.

   Colours come from the site tokens, so dark mode is inherited rather
   than re-declared.
   ===================================================================== */
.sa-panel{position:relative;margin:22px 0 6px}
/* Sits in the page margin beside the scene rather than inside it, so the
   animation keeps its full width. main is 960px inside a wider viewport,
   which is the gap it occupies - hence hidden once that gap closes. */
.sa-probe{position:absolute;right:-84px;top:6px;width:64px;height:auto;
  pointer-events:none;filter:drop-shadow(0 10px 20px rgba(8,32,60,.35))}
@media(max-width:1180px){ .sa-probe{display:none} }
.sa-scene{position:relative;border-radius:12px;overflow:hidden;
  border:1px solid var(--line);box-shadow:0 6px 24px rgba(8,32,60,.18);
  background:linear-gradient(180deg,#bcdcf3 0%,#dceaf5 62%,#eaf3fa 100%)}
.sa-scene svg{display:block;width:100%;height:auto}

/* The captions cross-fade in place, so the box must hold its height rather
   than collapse and make the steps below jump on every change. All four
   share ONE grid cell: they stack, and the box sizes itself to the tallest
   at every viewport. A fixed min-height would be a guess - and on a narrow
   phone the longest caption would overflow and collide with the numbered
   steps underneath, because nothing in flow is holding the box open. */
.sa-cap{display:grid;margin-top:16px;
  border-left:3px solid var(--brand);padding-left:15px}
.sa-step{grid-area:1/1;opacity:0;animation:18s linear infinite}
.sa-step h3{margin:0 0 .25em;font-size:1.02rem}
.sa-step p{margin:0;color:var(--muted);font-size:.95rem;max-width:44em}
.sa-s1{animation-name:sa-cap1}
.sa-s2{animation-name:sa-cap2}
.sa-s3{animation-name:sa-cap3}
.sa-s4{animation-name:sa-cap4}
@keyframes sa-cap1{0%,1%{opacity:0} 4%,26%{opacity:1} 28%,100%{opacity:0}}
@keyframes sa-cap2{0%,27%{opacity:0} 30%,44%{opacity:1} 46%,100%{opacity:0}}
@keyframes sa-cap3{0%,45%{opacity:0} 48%,62%{opacity:1} 64%,100%{opacity:0}}
@keyframes sa-cap4{0%,63%{opacity:0} 66%,97%{opacity:1} 100%{opacity:0}}

.sa-dots{display:flex;gap:8px;margin-top:16px;align-items:center}
.sa-dots i{width:34px;height:4px;border-radius:2px;background:var(--line);
  display:block;overflow:hidden;position:relative}
.sa-dots i::after{content:"";position:absolute;inset:0;background:var(--brand);
  transform:scaleX(0);transform-origin:left;animation:18s linear infinite}
.sa-dots i:nth-child(1)::after{animation-name:sa-d1}
.sa-dots i:nth-child(2)::after{animation-name:sa-d2}
.sa-dots i:nth-child(3)::after{animation-name:sa-d3}
.sa-dots i:nth-child(4)::after{animation-name:sa-d4}
@keyframes sa-d1{0%,4%{transform:scaleX(0)} 26%,100%{transform:scaleX(1)}}
@keyframes sa-d2{0%,30%{transform:scaleX(0)} 44%,100%{transform:scaleX(1)}}
@keyframes sa-d3{0%,48%{transform:scaleX(0)} 62%,100%{transform:scaleX(1)}}
@keyframes sa-d4{0%,66%{transform:scaleX(0)} 96%,100%{transform:scaleX(1)}}

/* Probe descends, then topples onto its side. The rotation pivot is the
   CABLE GLAND, not the nose - the pivot IS the attachment point, so it
   cannot move while the body swings, and the cable stays joined.
   Untransformed the probe already stands with its nose on the tank floor,
   so the descent runs to translateY(0); the topple then drops it a further
   46 so the barrel (34 wide, therefore 34 tall once horizontal) comes to
   rest ON the floor rather than through it. view-box because the pivot is
   given in canvas user units. */
#saProbe{transform-box:view-box;transform-origin:381px 375px;
  animation:sa-probeDrop 18s linear infinite}
@keyframes sa-probeDrop{
  0%,4%   {transform:translateY(-330px) rotate(0deg)}
  20%     {transform:translateY(0px)    rotate(0deg)}
  26%,100%{transform:translateY(46px)   rotate(-90deg)}
}
/* The cable end must sit on the gland at EVERY frame, not merely at the
   keyframe ends. The path runs y=30..421 (length 391) with dasharray 500,
   so the visible end lands at y = 30 + (500 - offset). Setting that equal
   to the gland's y (375 + translateY) gives offset = 155 - translateY, and
   the three offsets below are exactly that formula applied to the three
   translateY values above. Both ramps are linear across identical windows,
   so they stay joined throughout - including mid-topple, when the gland is
   descending and rotating at the same time. */
#saCable{stroke-dasharray:500;animation:sa-cableFeed 18s linear infinite}
@keyframes sa-cableFeed{
  0%,4%   {stroke-dashoffset:485}
  20%     {stroke-dashoffset:155}
  26%,100%{stroke-dashoffset:109}
}

#saBox{opacity:0;animation:sa-boxDrop 18s linear infinite}
@keyframes sa-boxDrop{
  0%,30%  {opacity:0;transform:translateY(-110px)}
  34%     {opacity:1}
  44%,100%{opacity:1;transform:translateY(0)}
}
#saLed{opacity:0;animation:sa-ledOn 18s linear infinite}
@keyframes sa-ledOn{0%,43%{opacity:0} 45%{opacity:1} 47%{opacity:.3} 49%{opacity:1} 100%{opacity:1}}

.sa-arc{opacity:0;transform-box:view-box;transform-origin:416px 44px;
  animation:18s linear infinite}
.sa-a1{animation-name:sa-arc1}
.sa-a2{animation-name:sa-arc2}
.sa-a3{animation-name:sa-arc3}
@keyframes sa-arc1{0%,48%{opacity:0;transform:scale(.45)} 53%{opacity:1;transform:scale(1)} 62%,100%{opacity:.95;transform:scale(1)}}
@keyframes sa-arc2{0%,52%{opacity:0;transform:scale(.45)} 57%{opacity:1;transform:scale(1)} 64%,100%{opacity:.8;transform:scale(1)}}
@keyframes sa-arc3{0%,56%{opacity:0;transform:scale(.45)} 61%{opacity:1;transform:scale(1)} 66%,100%{opacity:.6;transform:scale(1)}}

#saTablet{opacity:0;animation:sa-slideIn 18s linear infinite}
@keyframes sa-slideIn{0%,66%{opacity:0;transform:translate(26px,14px)} 74%,100%{opacity:1;transform:translate(0,0)}}
#saUsage{opacity:0;animation:sa-usageIn 18s linear infinite}
@keyframes sa-usageIn{0%,76%{opacity:0;transform:translate(26px,-12px)} 84%,100%{opacity:1;transform:translate(0,0)}}

.sa-badge{opacity:0;transform-box:fill-box;transform-origin:center;
  animation:18s linear infinite}
.sa-b1{animation-name:sa-badge1}
.sa-b2{animation-name:sa-badge2}
@keyframes sa-badge1{0%,85%{opacity:0;transform:scale(.82)} 89%,100%{opacity:1;transform:scale(1)}}
@keyframes sa-badge2{0%,89%{opacity:0;transform:scale(.82)} 93%,100%{opacity:1;transform:scale(1)}}

/* The tank is simply half full - the level never rises. This is only a slow
   drift of the surface. The profile repeats every 148 units, so translating
   by exactly -148 loops seamlessly. Named sa-waterdrift because plain
   `drift` is already taken by the cloud band above. */
.sa-wave{animation:sa-waterdrift 9s linear infinite}
@keyframes sa-waterdrift{from{transform:translateX(0)} to{transform:translateX(-148px)}}

/* Reduced motion gets the FINISHED scene, not a blank one. */
@media (prefers-reduced-motion: reduce){
  .sa-scene *,.sa-step,.sa-dots i::after{animation:none!important}
  #saProbe{transform:translateY(46px) rotate(-90deg)}
  #saCable{stroke-dashoffset:109}
  #saBox,#saLed,#saTablet,#saUsage,.sa-badge{opacity:1;transform:none}
  .sa-arc{opacity:.8;transform:none}
  .sa-dots i::after{transform:scaleX(1)}
  .sa-step{opacity:0}
  .sa-s1{opacity:1}
}
