/* ============================================================================
   JS Technology Group: site stylesheet
   ----------------------------------------------------------------------------
   The design system from mockups/index-d.html ("Visual Tech"), extracted from
   that file's inline CSS into one cache-busted stylesheet owned by build2.py.

   Faithful to D where D had an answer. Where it did not, this file adds one,
   and every addition is marked NEW with the reason:

     - mobile navigation. D, E and F all hide .nav-links below 980px with
       nothing replacing it, so on a phone the nav simply vanished.
     - a skip link and visible focus rings. No mockup had either.
     - forms, FAQ accordion, tables, breadcrumbs, long-form prose, pricing
       cards, before/after comparison, the Orbit module picker, and frames
       that hold a real screenshot rather than drawn CSS furniture. All of
       these exist on the live site and had no counterpart in a homepage-only
       mockup.

   Nothing here strips D's background. Sean's explicit instruction, 30 Sep:
   the mesh blobs, the canvas node network, the circuit traces and the code
   wash all stay, on every page, mobile included.
   ============================================================================ */

/* ---------- fonts -----------------------------------------------------------
   Inter is self-hosted (OFL) and is now a FALLBACK only. Archivo, Newsreader and
   JetBrains Mono come from Google
   until Sean approves pulling the woff2 files down; both degrade to the
   fallbacks declared in --head and --mono, so a blocked request costs the
   look and not the layout. */
@font-face{font-family:'Inter';font-style:normal;font-weight:400;font-display:swap;src:url('fonts/inter-400.woff2') format('woff2')}
@font-face{font-family:'Inter';font-style:normal;font-weight:500;font-display:swap;src:url('fonts/inter-500.woff2') format('woff2')}
@font-face{font-family:'Inter';font-style:normal;font-weight:600;font-display:swap;src:url('fonts/inter-600.woff2') format('woff2')}
@font-face{font-family:'Inter';font-style:normal;font-weight:700;font-display:swap;src:url('fonts/inter-700.woff2') format('woff2')}
@font-face{font-family:'Inter';font-style:normal;font-weight:800;font-display:swap;src:url('fonts/inter-800.woff2') format('woff2')}

/* ---------- tokens ---------------------------------------------------------
   Lifted from index-d.html :root verbatim. Do not drift these: they are the
   whole reason the site reads as one thing. */
:root{
  --bg:#080d1a; --bg2:#0b1226;
  --panel:#0f162c; /* OPAQUE. Sean, 20 October: the background was showing through
     the content. It was rgba(19,28,54,.66), so the fixed canvas, the mesh blobs and
     the circuit traces rendered through all fifteen surfaces that use this: cards,
     bands, tables, the FAQ, the pricing panels. Individual network nodes were
     visible as cyan dots inside the cards, and because the background is
     position:fixed they did not travel with the card when the page scrolled.
     #0f162c is the exact colour the old value composited to over the page
     background, sampled from the rendered page, so nothing else changes.
     The nav is deliberately NOT this: it keeps rgba(8,13,26,.66), because a sticky
     bar blurring the content travelling under it is the point of that one. */ --panelb:rgba(255,255,255,.08);
  --line:rgba(255,255,255,.09); --line2:rgba(255,255,255,.16);
  --blue:#2f7bf6; --cyan:#28d7ee; --violet:#7c6cff; --indigo:#5a54f0;
  --pink:#e24bd6; --lime:#43e08a; --amber:#e2a24b; --red:#ff6b6b;
  --white:#fff; --slate:#9fb0cc; --slate2:#c9d6ea;
  /* ARCHIVO AND NEWSREADER, Sean's instruction of 21 October. These are the two
     faces James settled on for the Orbit brochure on 7 October after calling the
     alternatives blocky: headings Archivo, running text Newsreader. Syne is gone.
     Inter stays in the heading fallback and its five @font-face rules stay, because
     it is SELF-HOSTED and therefore paints immediately: a local fallback close in
     metrics to the webfont is what keeps the swap from moving the page. Newsreader
     falls back to Georgia for the same reason, a serif to a serif. */
  --head:'Archivo','Inter',system-ui,sans-serif;
  --body:'Newsreader',Georgia,'Times New Roman',serif;
  --mono:'JetBrains Mono',ui-monospace,'Cascadia Mono',Consolas,monospace;
  --max:1200px;
  --grad:linear-gradient(120deg,var(--blue),var(--cyan) 50%,var(--violet));

  /* Radius scale. Was 17 different hand-picked values; a scale is what makes a set of
     components read as one system rather than as parts from different places. */
  --r-xs:8px; --r-sm:11px; --r:14px; --r-lg:20px; --r-xl:28px; --r-pill:50px;
  --r-device:34px;   /* a phone bezel is not a card corner */
  --r-hair:3px;      /* tiny decorative bars and dots */

  /* Accent glows are NOT elevation. --e-1..3 describe how far an object sits off the
     surface; these describe light coming off an accent. Keeping them separate is why the
     coherence scanner flags four "shadows" it cannot tell apart from depth. */
  --glow-accent:0 0 10px var(--cyan);
  --glow-live:0 0 10px var(--lime);
  --glow-btn:0 10px 30px rgba(47,123,246,.4);
  --glow-btn-hover:0 16px 44px rgba(40,215,238,.5);
  --glow-focus:0 0 0 3px rgba(40,215,238,.14);

  /* Elevation scale. Was 14 different shadows. Three levels is enough: a card sitting on
     the surface, a panel lifted off it, and something genuinely floating. */
  --e-1:0 2px 10px rgba(0,0,0,.28);
  --e-2:0 18px 44px rgba(0,0,0,.42);
  --e-3:0 38px 86px rgba(0,0,0,.56);

  /* Easing. Entrances ease OUT: fast to start, settling at the end. The audit found nine
     uses of ease-in on UI motion, which does the opposite and feels sluggish then abrupt. */
  --ease:cubic-bezier(.22,.61,.36,1);
  --ease-soft:cubic-bezier(.33,1,.68,1);

  /* Editorial type scale. The old one stepped politely from 1rem and everything sat in a
     narrow band. Sophistication comes from CONTRAST: a very large display against small,
     wide-tracked labels, with nothing much in between. */
  /* The lower bound is set by the phone, not by taste. Written for Syne, which is a
     WIDER face than the Archivo that replaced it, so this bound is now conservative
     rather than tight. At 2.2rem
     "your business" overflowed a 334px column by a few pixels, stranding "your" on its
     own line. 1.95rem is the largest size that keeps the headline to four lines at 390px. */
  --t-display:clamp(2.5rem,5.4vw,4.5rem);
  --t-h2:clamp(1.8rem,3.4vw,2.9rem);
  --t-h3:1.15rem;
  --t-body:1rem;
  --t-lead:1.15rem;
  --t-label:.72rem;
}

/* ---------- reset and base ------------------------------------------------ */
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%}
body{
  font-family:var(--body);background:var(--bg);color:var(--slate2);
  line-height:1.6;-webkit-font-smoothing:antialiased;
  /* clip rather than hidden: hidden silently breaks position:sticky on an ancestor.
     dvh rather than vh: vh is the tallest the viewport ever gets, so on a phone the page
     jumps the moment the address bar hides. */
  overflow-x:clip;
  min-height:100dvh;
}
a{color:inherit;text-decoration:none}
img{max-width:100%;display:block;height:auto}
button,input,select,textarea{font:inherit;color:inherit}
h1,h2,h3,h4{font-family:var(--head);color:#fff;line-height:1.05;letter-spacing:-1px}
h5,h6{font-family:var(--head);color:#fff;line-height:1.2}
strong,b{color:#fff}
hr{border:none;border-top:1px solid var(--line);margin:32px 0}
::selection{background:rgba(40,215,238,.3);color:#fff}

/* NEW. Focus visibility. No mockup defined one, and a dark site with no focus
   ring is unusable by keyboard. */
:focus-visible{outline:2px solid var(--cyan);outline-offset:3px;border-radius:var(--r-hair)}

/* NEW. Skip link, first focusable element on every page. */
.skip{
  position:absolute;left:12px;top:-60px;z-index:200;
  background:var(--cyan);color:#04121b;font-weight:700;font-size:.9rem;
  padding:11px 18px;border-radius:0 0 12px 12px;transition:top .18s;
}
.skip:focus{top:0}

/* ---------- layout and shared utilities ---------------------------------- */
/* EDGE TO EDGE. The content column was capped at 1200px and centred, which on a wide
   monitor left a large empty gutter either side. Sean's call on 2 October: use the
   screen. The side padding scales with the viewport instead, so the page still has a
   margin rather than text touching the glass, and it grows on a big display.

   .wrap-n is unused and stays capped, since a deliberately narrow block is a different
   intent from the default column. */
.wrap{max-width:none;margin:0 auto;padding:0 clamp(18px,3.5vw,72px);position:relative;z-index:3}
.wrap-n{max-width:900px}

/* Was a background-clip:text gradient. That is gate 14 of the anti-slop list, called out
   as the single most recognisable AI tell, allowed by no mood. The emphasis now comes from
   one solid accent colour, which is what the gradient was standing in for. */
.grad-t{color:var(--cyan)}
/* The pill-with-a-dot on every section is gate 51, a template signature. It is now a
   plain tracked label with a short rule, which reads as editorial rather than as chrome,
   and the pill survives only where it genuinely flags something (see .eyebrow--pill). */
.eyebrow{
  display:inline-flex;align-items:center;gap:11px;font-size:var(--t-label);font-weight:700;
  letter-spacing:2.2px;text-transform:uppercase;color:var(--slate);
}
.eyebrow::before{
  content:"";width:26px;height:1px;background:var(--cyan);flex:none;
}
.eyebrow--pill{
  color:var(--cyan);background:rgba(40,215,238,.08);
  border:1px solid rgba(40,215,238,.24);padding:7px 15px;border-radius:var(--r-pill);
}
.eyebrow--pill::before{display:none}
.eyebrow .pdot{width:6px;height:6px;border-radius:50%;background:var(--cyan);box-shadow:var(--glow-accent)}

.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:9px;
  font-weight:700;font-size:.95rem;padding:15px 28px;border-radius:var(--r);
  border:none;cursor:pointer;transition:.22s;font-family:var(--body);
  text-align:center;
}
.btn-g{background:var(--grad);color:#04121b;box-shadow:var(--glow-btn)}
.btn-g:hover{transform:translateY(-2px);box-shadow:var(--glow-btn-hover)}
/* The brand lockup: the JS mark as a vector, with the company name as real text.

   The header used to carry the full logo image at 66px wide. Its own "TECHNOLOGY
   GROUP" wordmark rendered about 5px tall at that size, so the company was never
   actually named anywhere above the fold: it was a texture, not a word. Sean's call
   on 4 October was mark plus text.

   Text rather than a bigger image because it stays sharp at any size, scales with the
   user's own font settings, and is read properly by a screen reader. The mark carries
   alt="" precisely because the name beside it is already the accessible name; marking
   both up would announce the company twice. */
.brand{display:inline-flex;align-items:center;text-decoration:none}
.bname{
  font-family:var(--head);font-size:1.16rem;line-height:1.05;color:#fff;
  letter-spacing:-.012em;white-space:nowrap;font-weight:800;
}
.bname b{font-weight:800;color:var(--slate2)}
.fbrand .bname{font-size:1.05rem}
/* THE NAME IS NEVER HIDDEN. While the mark was there it could be, because a logo
   remained. With the name as the only brand element, hiding it would leave the header
   empty, so it steps down in size instead and the NAV collapses earlier: the desktop
   links now give way to the hamburger at 1100px rather than 980px, because a 310px
   wordmark plus six links plus a CTA does not fit below that. Measured: at 1024px the
   CTA was 47px off the right edge before this change. */
@media(max-width:1100px){.bname{font-size:1.08rem}}
@media(max-width:620px){.bname{font-size:.96rem}}
@media(max-width:400px){.bname{font-size:.88rem}}

/* The secondary button was a 6% white fill inside a 16% white border, which on this
   background is close to invisible: Sean could not see "See it on Google Play" in the
   Orbit hero at all. It now carries the site's cyan, so it reads as a button and as
   part of the same palette as the eyebrow rules, links and the gradient primary.
   Site-wide on purpose; every secondary button had the same problem. */
.btn-glass{
  background:rgba(40,215,238,.16);color:#fff;
  border:1px solid var(--cyan);backdrop-filter:blur(8px);
  box-shadow:0 0 0 1px rgba(40,215,238,.12),0 6px 22px rgba(40,215,238,.18);
}
.btn-glass:hover{
  background:rgba(40,215,238,.28);transform:translateY(-2px);
  box-shadow:0 0 0 1px rgba(40,215,238,.3),0 10px 28px rgba(40,215,238,.3);
}
/* A button that is not wired up yet. A <span>, so it cannot navigate at all, which
   is safer than a dead href somebody later reads as a working link.

   FULL COLOUR, not dimmed. It was at opacity .5 so it would not promise a click it
   cannot honour, and Sean read that as the buttons losing their colour, which is fair:
   next to three live buttons a faded one looks broken rather than pending. It now
   matches its neighbours exactly and simply does nothing when clicked. The trade is
   deliberate and worth knowing: these look live and are not, so wire them up or remove
   them before the redesign is promoted. */
.btn-pending{cursor:default}
.btn-sm{padding:11px 20px;font-size:.85rem}
.btn-full{width:100%}
.btn:disabled{opacity:.55;cursor:not-allowed;transform:none!important;box-shadow:none}
.arrow{width:16px;height:16px;flex:none}

/* ---------- the layered background --------------------------------------
   D's, unchanged and complete. Four blurred mesh blobs, an animated canvas
   node network (theme/bg.js), SVG circuit traces, a masked monospace code
   wash and a vignette. Fixed, so it sits still while content scrolls over it.
   pointer-events:none throughout so none of it can ever eat a click. */
.bg{position:fixed;inset:0;z-index:0;overflow:hidden;pointer-events:none}
.mesh{position:absolute;inset:0}
.mesh i{position:absolute;border-radius:50%;filter:blur(90px);opacity:.5}
.mesh .m1{width:640px;height:640px;left:-160px;top:-180px;background:radial-gradient(circle,rgba(47,123,246,.6),transparent 66%);animation:drift1 24s var(--ease-soft) infinite}
.mesh .m2{width:600px;height:600px;right:-140px;top:6%;background:radial-gradient(circle,rgba(124,108,255,.5),transparent 66%);animation:drift2 28s var(--ease-soft) infinite}
.mesh .m3{width:560px;height:560px;left:28%;bottom:-12%;background:radial-gradient(circle,rgba(40,215,238,.4),transparent 66%);animation:drift1 32s var(--ease-soft) infinite reverse}
.mesh .m4{width:440px;height:440px;right:20%;bottom:6%;background:radial-gradient(circle,rgba(226,75,214,.28),transparent 66%);animation:drift2 36s var(--ease-soft) infinite}
/* main OWNS A LAYER ABOVE THE BACKGROUND, rather than relying on .wrap doing it.
   .wrap{z-index:3} only beats #net while main is not itself a stacking context, and
   anything that puts a transform, a filter or an opacity below 1 on main makes it one,
   at which point every z-index inside is sealed in and the fixed canvas paints over the
   lot. The page-entry tween did exactly that for months. This makes the guarantee
   structural: whatever happens inside main, main as a whole is above .bg and #net. */
main{position:relative;z-index:3}
#net{position:fixed;inset:0;width:100vw;height:100dvh;z-index:1;pointer-events:none;opacity:.9}
.circuit{position:absolute;inset:0;opacity:.14;z-index:1;width:100%;height:100%}
.codewash::before{
  /* The decorative code texture. It used to be element text, which put a block of Python at
     character zero of all sixteen pages, and the 1 October copy audit flagged it as an exposed
     source fragment. Moving it to a data attribute took it out of the page TEXT but left the
     string in every page's HTML, which does not meet "remove it from all public pages". It
     lives here instead: one stylesheet, no page carries it, and the visual is unchanged. */
  content:"@router.post(\"/members\", response_model=MemberOut)\Aasync def create_member(\A    payload: MemberCreate,\A    scope: Scope = Depends(require_scope(\"members:write\")),\A):\A    async with db.begin_nested():\A        await audit.emit(scope, \"member.created\", payload.email)\A        member = await members.create(scope, payload)\A    return member";
}
.codewash{
  position:absolute;inset:0;z-index:1;font-family:var(--mono);font-size:12px;
  line-height:2.1;color:rgba(120,170,255,.06);white-space:pre;overflow:hidden;
  padding:80px 30px;
  /* Rendered from an attribute for the same reason as the headline above: as element text
     this put a block of Python at character zero of every page on the site. */
  -webkit-mask-image:radial-gradient(ellipse 90% 70% at 70% 10%,#000,transparent 75%);
  mask-image:radial-gradient(ellipse 90% 70% at 70% 10%,#000,transparent 75%);
}
.vign{position:absolute;inset:0;background:radial-gradient(ellipse 120% 80% at 50% -10%,transparent 40%,rgba(8,13,26,.6) 100%)}
/* Film grain. Mockup F had this and D dropped it. It costs about 400 bytes inline and it is
   what stops a large flat dark area reading as an empty void rather than as a surface. */
.grain{
  position:absolute;inset:0;z-index:2;opacity:.055;mix-blend-mode:overlay;pointer-events:none;
  background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size:180px 180px;
}

@keyframes drift1{50%{transform:translate(60px,44px) scale(1.08)}}
@keyframes drift2{50%{transform:translate(-54px,54px) scale(1.1)}}
@keyframes floaty{50%{transform:translateY(-14px)}}
@keyframes floaty2{50%{transform:translateY(-10px)}}
@keyframes marq{to{transform:translateX(-50%)}}
@keyframes blink{50%{opacity:.2}}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(1.4)}}
@keyframes wavebar{0%,100%{transform:scaleY(.45)}50%{transform:scaleY(1)}}

/* ---------- navigation --------------------------------------------------- */
nav{
  position:sticky;top:0;z-index:40;backdrop-filter:blur(18px);
  background:rgba(8,13,26,.66);border-bottom:1px solid var(--line);
  transition:background .25s,border-color .25s;
}
nav.scrolled{background:rgba(8,13,26,.9);border-bottom-color:var(--line2)}
.nav-in{
  max-width:var(--max);margin:0 auto;padding:15px 28px;display:flex;
  align-items:center;justify-content:space-between;gap:20px;
}
.brand{
  display:flex;align-items:center;gap:10px;font-family:var(--head);
  font-weight:800;font-size:1.12rem;color:#fff;letter-spacing:-.5px;flex:none;
}
.brand img{height:38px;width:auto}
/* THE MIN-HEIGHT IS A LAYOUT-SHIFT FIX AND IT IS NOT COSMETIC. Three of the nav
   labels are two words, and in the fallback font they are narrow enough to sit on
   one line. When SYNE arrived they each wrap to two, the bar grew from 26px to
   47px, and EVERY PAGE BELOW THE HEADER DROPS 55px. Measured on fitara.html at 4x
   CPU: 0.1785 cumulative layout shift with fonts allowed against 0.0058 with them
   blocked, and it is the same on all sixteen pages because it is the header.

   Lighthouse attributes it to whatever large element happened to move, which on
   that page is the hero laptop. That element never changes size; it is only being
   pushed. Chasing the reported element rather than the measured cause would have
   fixed nothing.

   47px is the height the nav settles at, measured identically at 1920, 1600, 1440,
   1280 and 1150. Reserving it changes nothing on screen once the font has loaded:
   it only stops the pre-font render being shorter than the post-font one.

   RE-MEASURED 21 OCTOBER, AFTER THE MOVE TO ARCHIVO, AND THE SHIFT IS GONE.
   Archivo is a narrower face than Syne and none of the labels wrap any more: the
   natural height is 26px with the webfonts allowed AND with them blocked, measured
   identically at 1920, 1600, 1440, 1280 and 1150, and the h1 no longer reflows
   either. So the reservation is down from 47px to 26px; at 47 it was holding 21px
   of empty bar that nothing was ever going to fill. The guard stays rather than
   being deleted, because it costs nothing and the next font or label change can
   bring the wrap straight back. RE-MEASURE IT IF EITHER CHANGES.

   The original cure is still JS-00856, self-hosting Archivo and
   JetBrains Mono so there is no swap to shift around. IF A NAV LABEL IS EVER
   CHANGED, re-measure this number: it is tied to the longest label wrapping to two
   lines, and a shorter set of labels would leave a gap here. */
.nav-links{display:flex;align-items:center;gap:25px;min-height:26px}
.nav-links a,.nav-links .men{
  color:var(--slate);font-size:.9rem;font-weight:500;cursor:pointer;
  display:inline-flex;align-items:center;gap:5px;transition:.2s;
  background:none;border:none;padding:0;font-family:inherit;
}
.nav-links a:hover,.nav-links .men:hover,.nav-links .men[aria-expanded="true"]{color:#fff}
.nav-links a.active,.nav-links .men.active{color:#fff;font-weight:600}
.nav-links a.active::after{
  content:"";display:block;position:absolute;left:0;right:0;bottom:-6px;height:2px;
  background:var(--grad);border-radius:var(--r-hair);
}
.nav-links a.active{position:relative}
.caret{width:12px;height:12px;transition:transform .2s}
.men[aria-expanded="true"] .caret{transform:rotate(180deg)}

.dd{position:relative}
.dd-menu{
  position:absolute;top:150%;left:50%;transform:translateX(-50%);min-width:256px;
  background:#111a30;border:1px solid var(--line2);border-radius:var(--r);
  box-shadow:var(--e-2);padding:8px;
  opacity:0;visibility:hidden;transition:.2s;
}
.dd:hover .dd-menu,.dd:focus-within .dd-menu,.dd.open .dd-menu{opacity:1;visibility:visible;top:132%}
.dd-menu a{display:flex;flex-direction:column;gap:2px;padding:11px 14px;border-radius:var(--r-sm)}
.dd-menu a:hover,.dd-menu a:focus-visible{background:rgba(255,255,255,.05)}
.dd-menu .t{font-family:var(--head);font-weight:700;font-size:.92rem;color:#fff}
.dd-menu .s{font-size:.76rem;color:var(--slate)}
.nav-cta{padding:11px 20px;font-size:.85rem;flex:none}

/* NEW. The burger. D had nothing here at all. */
.burger{
  display:none;width:44px;height:44px;border-radius:var(--r-sm);flex:none;
  background:rgba(255,255,255,.06);border:1px solid var(--line2);
  cursor:pointer;position:relative;
}
.burger i{
  position:absolute;left:12px;right:12px;height:2px;background:#fff;border-radius:var(--r-hair);
  transition:transform .22s,opacity .18s;
}
.burger i:nth-child(1){top:15px}
.burger i:nth-child(2){top:21px}
.burger i:nth-child(3){top:27px}
.burger[aria-expanded="true"] i:nth-child(1){transform:translateY(6px) rotate(45deg)}
.burger[aria-expanded="true"] i:nth-child(2){opacity:0}
.burger[aria-expanded="true"] i:nth-child(3){transform:translateY(-6px) rotate(-45deg)}

@media(max-width:1100px){
  .nav-links{display:none}
  .nav-cta{display:none}
  .burger{display:block}
  .nav-in{padding:12px 18px}
}

/* ---------- mobile navigation (NEW) --------------------------------------
   A full-height sheet rather than a dropdown, because the nav has two
   sub-groups and a dropdown-inside-a-dropdown on a phone is unusable. Groups
   are always expanded: there are only nine links, so hiding them behind
   another tap buys nothing. */
.mobnav{
  position:fixed;inset:0;z-index:60;background:rgba(8,13,26,.97);
  backdrop-filter:blur(20px);overflow-y:auto;padding:0 0 40px;
  opacity:0;visibility:hidden;transform:translateY(-8px);
  transition:opacity .22s,transform .22s,visibility .22s;
}
.mobnav.open{opacity:1;visibility:visible;transform:none}
.mobnav-top{
  display:flex;align-items:center;justify-content:space-between;
  padding:12px 18px;border-bottom:1px solid var(--line);
  position:sticky;top:0;background:rgba(8,13,26,.9);backdrop-filter:blur(12px);
}
.mobnav-close{
  width:44px;height:44px;border-radius:var(--r-sm);background:rgba(255,255,255,.06);
  border:1px solid var(--line2);color:#fff;font-size:1.3rem;line-height:1;cursor:pointer;
}
.mobnav .inner{padding:22px 18px 0}
.mobnav .grp{margin-bottom:26px}
.mobnav .grp h4{
  font-family:var(--body);font-size:.7rem;letter-spacing:1.4px;text-transform:uppercase;
  color:var(--cyan);margin-bottom:10px;
}
.mobnav a{
  display:block;padding:13px 0;font-size:1.05rem;font-weight:600;color:var(--slate2);
  border-bottom:1px solid var(--line);
}
.mobnav a:last-child{border-bottom:none}
.mobnav a.active{color:#fff}
.mobnav a.active::before{content:"";display:inline-block;width:4px;height:4px;border-radius:50%;background:var(--cyan);vertical-align:3px;margin-right:9px}
.mobnav a .s{display:block;font-size:.78rem;font-weight:400;color:var(--slate);margin-top:2px}
.mobnav .mcta{margin-top:10px}
.mobnav .mtel{
  display:flex;align-items:center;gap:10px;margin-top:14px;padding:14px;
  border:1px solid var(--line);border-radius:var(--r);color:#fff;font-weight:700;
  background:rgba(255,255,255,.04);
}
body.nav-open{overflow:hidden}

/* ---------- hero ---------------------------------------------------------- */
.hero{position:relative;padding:34px 0 14px}
/* minmax(0,...) rather than a bare fr: an fr column's automatic minimum is auto, so the
   unbreakable long words in the h1 were overriding the ratio and taking 722px of 1096,
   leaving the three-screen deck 374px to fit 440px of phones into. */
/* 1.2/1 rather than 1/.92. Measured: at 56px the headline's third line wanted 582px for
   "your business" and the column was 573, so "your" was stranded alone by nine pixels.
   600px puts it right. The art column keeps 500px, comfortably over the 440px the
   three-screen deck needs. text-wrap:balance is set on the h1 and is supported here, but
   Chromium declines to balance this particular block, so the column width is the lever
   that actually works. */
/* minmax(0,...) rather than a bare fr. An fr column's automatic minimum is auto, so the
   headline's own min-content width overrides the declared ratio: measured at 921px of
   1100 for the text, leaving the art column 179px to fit a 560px laptop into, which
   crushed it to a sliver with the phone sitting on top. The typewriter markup made it
   worse (it was 722/374 before), but the ratio was never being honoured either way.

   The ratio is 1.3/1 rather than the old 1.02/.98 because minmax lets a column go
   NARROWER than its content, which trades one failure for another: at an even split
   the headline overflowed and ran underneath the laptop. "business runs on" measures
   about 600px at 72px in Syne. Archivo sets narrower, so the headline now needs
   less than that; the split is unchanged and simply has more slack. */
.hero-in{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1fr);gap:40px;align-items:center}
.hero h1{font-size:clamp(2.5rem,5.4vw,4.5rem);font-weight:800;margin:14px 0 0}
.hero .sub{text-wrap:pretty;color:var(--slate);font-size:1.16rem;margin:16px 0 22px;max-width:520px}
.hero .actions{display:flex;gap:14px;flex-wrap:wrap}
@media(max-width:980px){.hero-in{grid-template-columns:1fr}.shot{display:none}.hero{padding:24px 0 12px}}
/* The homepage headline is clamp(2.5rem,5.4vw,4.5rem), so below about 400px the
   clamp floor stops it shrinking and the longest word runs 3px past the viewport:
   measured 363px of h1 in a 360px window, which is 21px of horizontal scroll once
   the typewriter wrapper is counted. The floor comes down on small phones. Found
   while checking the brand change, pre-existing rather than caused by it. */
@media(max-width:400px){.hero h1,.hero.solo h1{font-size:2.05rem}}

.stores{display:flex;gap:11px;margin-top:22px;flex-wrap:wrap}
.store{
  display:inline-flex;align-items:center;gap:10px;background:rgba(255,255,255,.05);
  border:1px solid var(--line2);border-radius:var(--r-sm);padding:9px 15px;color:#fff;
  transition:.2s;
}
.store:hover{background:rgba(255,255,255,.1);transform:translateY(-2px)}
.store svg{width:19px;height:19px;flex:none}
.store i{font-style:normal;font-size:.6rem;color:var(--slate);display:block;line-height:1}
.store b{font-size:.9rem}

/* hero product shot: laptop plus phone, drawn rather than photographed.
   The scene is one 3D stage so the whole composition tilts together as a single object
   rather than three things drifting independently, which is what made the hero read as
   cluttered. theme/hero.js assembles it on load, then tracks the pointer. */
.shot{position:relative;height:470px;perspective:1500px}
.deck{position:absolute;inset:0;transform-style:preserve-3d;will-change:transform}
.deck > *{will-change:transform}
.laptop{position:absolute;left:0;top:40px;width:560px;max-width:100%;animation:floaty 8s ease-in-out infinite}
.screen{background:#0c1526;border:1px solid var(--line2);border-radius:14px 14px 0 0;box-shadow:0 40px 90px rgba(0,0,0,.6);overflow:hidden}
.winbar{height:34px;background:#0f1a30;display:flex;align-items:center;gap:7px;padding:0 14px;border-bottom:1px solid var(--line)}
.winbar .d{width:11px;height:11px;border-radius:50%;flex:none}
.winbar .d1{background:#ff5f57}.winbar .d2{background:#febc2e}.winbar .d3{background:#28c840}
.winbar .url{margin-left:12px;height:16px;flex:1;max-width:260px;border-radius:6px;background:rgba(255,255,255,.06);border:1px solid var(--line)}
.dash{display:grid;grid-template-columns:120px 1fr;height:280px}
.side{background:#0a1424;border-right:1px solid var(--line);padding:14px 12px}
.side .lg{height:12px;width:70%;border-radius:5px;background:var(--grad);margin-bottom:16px}
.side .nv{height:9px;border-radius:5px;background:rgba(255,255,255,.1);margin:11px 0}
.side .nv.on{background:rgba(40,215,238,.55);width:85%}
.main{padding:16px}
.kpis{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:12px}
.kpi{background:rgba(255,255,255,.04);border:1px solid var(--line);border-radius:10px;padding:10px}
.kpi b{color:#fff;font-family:var(--head);font-size:1.1rem}
.kpi span{display:block;height:6px;width:60%;border-radius:4px;background:rgba(255,255,255,.14);margin-top:6px}
.chart{background:rgba(255,255,255,.03);border:1px solid var(--line);border-radius:10px;padding:12px;display:flex;align-items:flex-end;gap:8px;height:120px}
.chart i{flex:1;border-radius:5px 5px 0 0;background:var(--grad);opacity:.85;transform-origin:bottom}
/* ---------- the homepage laptop: a WEBSITE wireframe --------------------
   The homepage hero used to draw the Orbit dashboard above (.dash, .kpis, .chart).
   That composition moved to orbit.html, where it depicts the actual product. The
   homepage sells three services, so its artwork has to depict all three and claim
   nothing: this wireframe is Website Design, the phone beside it is App Development
   and the .cf card with .cfwave is VoIP.

   Deliberately shapes rather than numbers. The dashboard it replaced showed KPIs of
   1,284, 96% and 42, which were invented. A wireframe cannot state a figure nobody
   has. Same 280px height as .dash so the laptop keeps its proportions. */
.wf{height:280px;display:flex;flex-direction:column;
  background:radial-gradient(120% 90% at 12% 0%,rgba(40,215,238,.1),transparent 60%),#0a1322}
.wf-nav{height:38px;display:flex;align-items:center;gap:13px;padding:0 16px;
  border-bottom:1px solid var(--line);flex:none}
.wf-lg{width:26px;height:10px;border-radius:4px;background:var(--grad);flex:none}
.wf-nav i{height:7px;width:32px;border-radius:4px;background:rgba(255,255,255,.26);flex:none}
.wf-cta{margin-left:auto;width:54px;height:19px;border-radius:var(--r-xs);flex:none;
  background:linear-gradient(135deg,rgba(90,123,255,.55),rgba(40,215,238,.55));
  border:1px solid rgba(40,215,238,.6)}
.wf-hero{padding:22px 16px 15px;display:flex;flex-direction:column;gap:9px;align-items:flex-start}
.wf-h{height:18px;width:70%;border-radius:6px;background:var(--grad)}
.wf-p{height:8px;width:86%;border-radius:4px;background:rgba(255,255,255,.3)}
.wf-p.sh{width:58%;background:rgba(255,255,255,.2)}
.wf-btn{margin-top:7px;width:94px;height:23px;border-radius:var(--r-xs);
  background:linear-gradient(135deg,var(--blue),var(--cyan))}
.wf-cards{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;padding:0 16px 16px;margin-top:auto}
.wfc{height:62px;border-radius:10px;padding:9px;background:rgba(255,255,255,.055);
  border:1px solid var(--line2);display:flex;flex-direction:column;gap:5px}
.wfc .wfi{width:17px;height:17px;border-radius:5px;flex:none;
  background:linear-gradient(135deg,var(--blue),var(--cyan))}
.wfc i{display:block;height:5px;border-radius:var(--r-hair);background:rgba(255,255,255,.26)}
.wfc i.sh{width:58%;background:rgba(255,255,255,.16)}

/* The waveform on the floating card, the VoIP third of the composition. Same
   transform-origin as .chart i, because theme/hero.js animates both the same way. */
.cfwave{display:flex;align-items:flex-end;gap:3px;height:20px;margin-top:10px}
.cfwave i{flex:1;border-radius:var(--r-hair);background:var(--grad);opacity:.85;transform-origin:bottom}

.lidbase{height:12px;background:linear-gradient(#0f1a30,#0a1220);border:1px solid var(--line2);border-top:none;border-radius:0 0 16px 16px;box-shadow:0 20px 40px rgba(0,0,0,.5)}
.phone{
  position:absolute;right:2%;bottom:-6px;width:186px;height:376px;border-radius:34px;
  background:#0a141d;border:8px solid #0c1a2b;box-shadow:0 40px 80px rgba(0,0,0,.6);
  overflow:hidden;animation:floaty2 6.5s ease-in-out infinite;z-index:3;
}
.phone .pbar{height:96px;background:var(--grad);padding:22px 16px 0}
.phone .pbar b{display:block;color:#fff;font-family:var(--head);font-size:1.05rem}
/* A <b>, not an <h5>. The deck is decorative artwork inside aria-hidden, and an
   h5 in it put a heading with no h4 above it into the document outline. The label
   is a picture of an app title, not a heading of the page. */
.phone .pbar p{color:rgba(255,255,255,.85);font-size:.66rem}
.phone .pl{padding:12px}
.phone .pi{display:flex;gap:9px;align-items:center;background:rgba(255,255,255,.05);border:1px solid var(--line);border-radius:11px;padding:9px;margin-bottom:9px}
.phone .pi .b{width:28px;height:28px;border-radius:8px;background:linear-gradient(135deg,var(--blue),var(--cyan));flex:none}
.phone .pi .l{flex:1}
.phone .pi .l i{display:block;height:6px;border-radius:3px;background:rgba(255,255,255,.2);margin:3px 0}
.phone .pi .l i.sh{width:52%}
.cf{
  position:absolute;border-radius:13px;background:rgba(15,24,44,.82);border:1px solid var(--line2);
  backdrop-filter:blur(10px);padding:11px 14px;box-shadow:0 14px 36px rgba(0,0,0,.45);z-index:4;
}
.cf b{color:#fff;font-family:var(--head);font-size:.82rem;display:block}
.cf span{font-size:.7rem;color:var(--cyan)}
.cf1{top:0;right:12%;animation:floaty 6s ease-in-out infinite}
.cf .row{display:flex;align-items:center;gap:8px}
.live{width:8px;height:8px;border-radius:50%;background:var(--lime);box-shadow:0 0 10px var(--lime);animation:pulse 2s infinite;flex:none}

/* Typewriter in the headline, carried across from the current live site at Sean's
   request. The caret is a plain blinking bar rather than a text character, so it cannot be
   selected, copied or read out.

   .tw-sr holds the real phrase. It is taken out of the visual flow once JS is running but
   stays in the accessibility tree, which is what keeps the h1 a proper heading rather than
   a span that mutates. Without JS the static phrase is simply what you see, and the empty
   animated span and the caret collapse to nothing. */
/* The headline is PINNED: .tw-ghost is an invisible copy of the longest phrase that holds
   the space open, and .tw-live is laid over the top of it. The ghost never changes, so the
   height never changes, so neither the headline nor the artwork beside it can move while the
   text types. Deriving the height from real text means it needs no magic number and no
   per-breakpoint tuning.

   display:block puts the phrase on its own line permanently, so it cannot reflow the words
   in front of it either.

   The trade is that a short phrase sits in a box sized for the longest one, leaving some
   empty space beneath it. On a left-aligned headline that reads as spacing. */
.tw-wrap{display:block;position:relative}
/* The sizing copy is rendered from an ATTRIBUTE, not written as text. It holds the box open
   exactly as before, but generated content is not part of the document's text, so the heading
   no longer reads as two merged phrases to a crawler or anything else parsing the page. That
   fault was reported by the copy audit of 1 October and it was introduced the same morning. */
.tw-ghost{visibility:hidden}
.tw-ghost::before{content:attr(data-ghost)}
.tw-live{position:absolute;inset:0}
.tw-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
.tw-caret{
  display:inline-block;width:3px;height:.78em;margin-left:5px;vertical-align:middle;
  border-radius:var(--r-hair);background:var(--cyan);
  animation:blink 1s step-end infinite;
}
/* No JS: nothing has filled #tw, so show the real phrase and drop the caret. */
.tw-live:not(.on) .tw-sr{position:static;width:auto;height:auto;clip-path:none;white-space:normal}
.tw-live:not(.on) .tw-caret{display:none}

/* Heading decode. The real words stay in the flow as the sizing layer so the box cannot
   change size while random glyphs of different widths pass through it, and only their colour
   is removed. colour:transparent rather than visibility:hidden is deliberate: visibility
   would take the real text out of the accessibility tree, and this element IS the heading.
   Both wrappers are removed by the script once the animation finishes. */
.sc-ghost{color:transparent}
.sc-live{position:absolute;inset:0}
.shead h2{position:relative}

/* Live measurement panels: page vitals on the Website Design page, connection quality on
   VoIP. Both deliberately plain. These are evidence, and evidence that is heavily styled
   looks like marketing, which is the opposite of the point. */
.vitals,.netpanel{
  border:1px solid var(--line2);border-radius:var(--r-lg);background:rgba(10,17,33,.55);
  backdrop-filter:blur(14px);padding:6px 22px 18px;box-shadow:var(--e-2);
}
.vrow{display:grid;grid-template-columns:1fr auto auto;gap:16px;align-items:center;
  padding:15px 0;border-bottom:1px solid rgba(255,255,255,.055)}
.vrow b{font-weight:600;color:var(--slate2);font-size:.95rem}
.vval{font-family:var(--mono);font-size:1.05rem;color:#fff}
.vchip{font-family:var(--mono);font-size:.68rem;letter-spacing:1.2px;text-transform:uppercase;
  padding:4px 10px;border-radius:var(--r-pill);border:1px solid transparent;min-width:62px;text-align:center}
.v-good{color:#43e08a;background:rgba(67,224,138,.1);border-color:rgba(67,224,138,.3)}
.v-ok{color:#e2a24b;background:rgba(226,162,75,.1);border-color:rgba(226,162,75,.3)}
.v-slow{color:#ff7a7a;background:rgba(255,122,122,.1);border-color:rgba(255,122,122,.3)}
.vnote{margin:16px 0 0;font-size:.84rem;color:var(--slate);line-height:1.6}

/* The line check, dressed as an instrument rather than three numbers over a line.
   Every value on it is still measured in the visitor's own browser; nothing here
   adds a claim, it only presents the same four readings better. */
.nethead{display:flex;align-items:center;justify-content:space-between;gap:12px;
  padding-bottom:14px;border-bottom:1px solid var(--line)}
.netlive{display:inline-flex;align-items:center;gap:8px;font-size:.7rem;font-weight:800;
  letter-spacing:1.4px;text-transform:uppercase;color:var(--cyan)}
.netlive i{width:8px;height:8px;border-radius:50%;background:var(--cyan);flex:none;
  box-shadow:0 0 0 0 rgba(40,215,238,.6);animation:netpulse 1.8s ease-out infinite}
@keyframes netpulse{
  0%{box-shadow:0 0 0 0 rgba(40,215,238,.55)}
  70%{box-shadow:0 0 0 9px rgba(40,215,238,0)}
  100%{box-shadow:0 0 0 0 rgba(40,215,238,0)}
}
.netcount{font-family:var(--mono);font-size:.74rem;color:var(--slate)}

.netgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;padding:18px 0 6px}
.netstat{
  text-align:left;padding:16px 18px;border-radius:var(--r);
  background:rgba(255,255,255,.035);border:1px solid var(--line);transition:border-color .3s;
}
.netstat .nlab{display:block;font-size:.68rem;letter-spacing:1.4px;text-transform:uppercase;
  color:var(--slate)}
.netstat .nval{display:block;font-family:var(--mono);font-size:2.1rem;line-height:1.1;
  color:#fff;margin:6px 0 10px;font-variant-numeric:tabular-nums}
.netstat .nbar{display:block;height:4px;border-radius:var(--r-hair);
  background:rgba(255,255,255,.08);overflow:hidden}
.netstat .nbar i{display:block;height:100%;width:0;border-radius:var(--r-hair);
  background:var(--grad);transition:width .5s var(--ease-soft),background .3s}
.netstat .nhint{display:block;margin-top:9px;font-size:.72rem;color:var(--slate2)}
/* the tile takes the colour of its own reading, so the three can disagree */
.netstat.q-good{border-color:rgba(67,224,138,.35)}
.netstat.q-good .nval{color:#43e08a}
.netstat.q-good .nbar i{background:#43e08a}
.netstat.q-ok{border-color:rgba(226,162,75,.35)}
.netstat.q-ok .nval{color:#e2a24b}
.netstat.q-ok .nbar i{background:#e2a24b}
.netstat.q-poor{border-color:rgba(255,122,122,.35)}
.netstat.q-poor .nval{color:#ff7a7a}
.netstat.q-poor .nbar i{background:#ff7a7a}

.netchart{position:relative;margin:14px 0 4px;border-radius:var(--r);overflow:hidden;
  background:linear-gradient(180deg,rgba(255,255,255,.028),rgba(255,255,255,0));
  border:1px solid var(--line)}
#netgraph{width:100%;height:150px;display:block}

.netverdict{display:flex;align-items:flex-start;gap:11px;margin:12px 0 0;font-size:.95rem;
  font-weight:600;padding:14px 16px;border-radius:var(--r-sm);border:1px solid transparent}
.netverdict .vdot{width:9px;height:9px;border-radius:50%;flex:none;margin-top:6px;
  background:currentColor}
.netverdict.v-good{color:#43e08a;background:rgba(67,224,138,.08);border-color:rgba(67,224,138,.26)}
.netverdict.v-ok{color:#e2a24b;background:rgba(226,162,75,.08);border-color:rgba(226,162,75,.26)}
.netverdict.v-slow{color:#ff7a7a;background:rgba(255,122,122,.08);border-color:rgba(255,122,122,.26)}
@media(max-width:700px){.netgrid{grid-template-columns:1fr}}
@media(max-width:560px){
  .vrow{grid-template-columns:1fr auto;gap:8px}
  .vrow .vchip{grid-column:2}
  .netstat .nval{font-size:1.25rem}
}

/* App configurator. Choices on the left, the phone and the written spec on the right,
   collapsing to one column on a phone with the choices first. */
.cfg{display:grid;grid-template-columns:1fr 1fr;gap:34px;align-items:start}
.cfg-h{font-family:var(--head);font-size:1rem;color:#fff;margin:0 0 14px}
.cfg-pick .cfg-h + .cplat + .cfg-h{margin-top:28px}
.cplat{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:28px}
.cpill input{position:absolute;opacity:0;pointer-events:none}
.cpill span{display:inline-block;padding:9px 18px;border-radius:var(--r-pill);border:1px solid var(--line2);
  color:var(--slate2);font-size:.9rem;cursor:pointer;transition:.18s}
.cpill input:checked + span{background:rgba(40,215,238,.12);border-color:rgba(40,215,238,.5);color:var(--cyan)}
.cpill input:focus-visible + span{box-shadow:var(--glow-focus)}

.cfeats{display:grid;gap:9px}
.cfeat{display:flex;gap:12px;align-items:flex-start;padding:13px 15px;border:1px solid var(--line);
  border-radius:var(--r-sm);cursor:pointer;transition:.18s;background:rgba(255,255,255,.02);position:relative}
.cfeat:hover{border-color:var(--line2);background:rgba(255,255,255,.04)}
.cfeat input{position:absolute;opacity:0;pointer-events:none}
.cbox{width:19px;height:19px;border-radius:var(--r-xs);border:1px solid var(--line2);flex:none;margin-top:2px;
  position:relative;transition:.15s}
.cfeat input:checked ~ .cbox{background:var(--cyan);border-color:var(--cyan)}
.cfeat input:checked ~ .cbox::after{content:"";position:absolute;left:6px;top:2px;width:5px;height:10px;
  border:solid #04121b;border-width:0 2px 2px 0;transform:rotate(45deg)}
.cfeat input:focus-visible ~ .cbox{box-shadow:var(--glow-focus)}
.ctext b{display:block;color:#fff;font-size:.93rem;font-weight:600}
.ctext em{display:block;font-style:normal;color:var(--slate);font-size:.82rem;margin-top:3px;line-height:1.5}

.cphone{border:8px solid #0c1a2b;border-radius:var(--r-device);background:#0a141d;overflow:hidden;
  box-shadow:var(--e-3);min-height:330px}
/* ---------- the app preview is app chrome, not a list ------------------
   A top bar that gains chips, a content area, and a tab bar along the bottom
   that fills up as features are chosen. The tab bar is the part that does the
   work: a row of tabs appearing one by one is the clearest signal there is
   that an application is being built, and it is the thing a list of cards
   could never say. */
.cphone{display:flex;flex-direction:column}
.cph-bar{background:var(--grad);padding:14px 16px;display:flex;align-items:center;gap:10px}
.cph-barmain{flex:1;min-width:0}
.cph-chips{display:flex;align-items:center;gap:6px;flex:none}
.cph-chip{display:block;width:20px;height:20px;border-radius:50%;flex:none;
  background:rgba(255,255,255,.3);border:1px solid rgba(255,255,255,.45)}
.cph-chip.avatar{background:rgba(255,255,255,.55)}
.cph-chip.bell{border-radius:7px 7px 3px 3px;height:18px;width:15px;background:rgba(255,255,255,.5)}
.cph-chip.sync{background:none;border:2px solid rgba(255,255,255,.55);border-right-color:transparent}

/* The notification banner. Transient by design: it drops in when push is
   ticked and leaves again, because a banner that stayed would be claiming an
   alert that is not happening. */
.cph-note{min-height:0;opacity:0;margin:0 10px;padding:0 10px;color:#fff;font-size:.72rem;
  line-height:26px;height:26px;border-radius:var(--r-xs);background:rgba(255,255,255,.14);
  border:1px solid rgba(255,255,255,.2);margin-top:8px}

.cph-list{flex:1}

.cph-tabs{display:flex;align-items:stretch;gap:2px;border-top:1px solid var(--line);
  background:rgba(255,255,255,.03);padding:7px 6px}
/* A FIXED HEIGHT ON EVERY TAB, or they do not line up. The icons are different
   shapes and one of them is a background-positioned pair of bars, so left to
   their content the tabs sit at slightly different heights and the row looks
   broken. */
.cph-tab{display:flex;flex-direction:column;align-items:center;justify-content:flex-start;
  gap:5px;flex:1;height:42px;font-style:normal;overflow:hidden;min-width:0}
/* .cph-tabs in the selector is not padding. There are two other rules in this
   file for a class called "chart", one of them the hero dashboard's, and the
   tab icon was resolving to 26px instead of 16px and pushing its own label out
   of a clipped box. Scoping the icon box to the tab bar settles it rather than
   leaving it to whichever .chart rule happens to be last. */
.cph-tabs .cph-tab s{display:block;width:16px;height:16px;flex:none;text-decoration:none;
  background:rgba(255,255,255,.3);border-radius:3px;box-sizing:border-box}
.cph-tab.on s{background:var(--cyan)}
.cph-tab u{text-decoration:none;color:var(--slate);font-size:.6rem;letter-spacing:.3px;
  white-space:nowrap}
.cph-tab.on u{color:#fff}
.cph-tab s.home{border-radius:3px 3px 1px 1px}
.cph-tab s.doc{border-radius:2px;background:rgba(255,255,255,.34)}
.cph-tab s.chat{border-radius:5px 5px 5px 1px}
.cph-tab s.cal{border-radius:2px;
  background:linear-gradient(to bottom,rgba(40,215,238,.6) 0 4px,rgba(255,255,255,.3) 4px)}
.cph-tabs .cph-tab s.chart{height:16px;background:
  linear-gradient(to top,rgba(255,255,255,.34) 0 60%,transparent 60%),
  linear-gradient(to top,rgba(255,255,255,.34) 0 100%,transparent 100%);
  background-size:5px 100%,5px 100%;background-position:1px 100%,9px 100%;
  background-repeat:no-repeat}

.cph-bar b{display:block;color:#fff;font-family:var(--head);font-size:1rem}
.cph-bar span{font-size:.72rem;color:rgba(255,255,255,.85)}
.cph-list{padding:12px}
.cph-empty{color:var(--slate);font-size:.82rem;text-align:center;padding:26px 10px;margin:0}
.cph-row{background:rgba(255,255,255,.05);border:1px solid var(--line);border-radius:var(--r-xs);
  padding:11px 12px;margin-bottom:8px;animation:cfgin .32s var(--ease) both}
.cph-row b{color:#fff;font-size:.82rem;font-weight:600;display:block}
@keyframes cfgin{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}

/* ---------- the feature panels inside the app preview -------------------
   Each ticked feature brings a small structural sketch of itself, built from
   empty divs. DELIBERATELY ABSTRACT: a bar is a bar, a bubble is a bubble,
   and none of it carries a number or a name. Inventing plausible content for
   a visitor's own app would be putting made-up data on a sales page, which is
   the thing the whole site has been careful not to do. Shapes suggest the
   KIND of thing a feature is and stop there.

   Height is animated by the script, so the row must not fight it: no fixed
   height here, and overflow hidden so a collapsing row clips rather than
   spilling its contents over the one below. */
.cph-row{overflow:hidden}
.cph-art{display:flex;align-items:center;flex-wrap:wrap;gap:5px;margin-top:8px}
.cph-art .sh{display:block;background:rgba(255,255,255,.14);border-radius:3px;flex:none}
.cph-art .line{height:5px;border-radius:var(--r-hair)}
.cph-art .w70{width:70%}.cph-art .w60{width:60%}.cph-art .w55{width:55%}
.cph-art .w50{width:50%}.cph-art .w45{width:45%}.cph-art .w40{width:40%}
.cph-art .w35{width:35%}
.cph-art .avatar{width:18px;height:18px;border-radius:50%;background:rgba(40,215,238,.4)}
.cph-art .bell{width:13px;height:15px;border-radius:6px 6px 2px 2px;background:rgba(226,162,75,.5)}
.cph-art .card{width:34px;height:21px;border-radius:4px;background:rgba(67,224,138,.32);
  border:1px solid rgba(67,224,138,.5)}
.cph-art .bar{width:100%;height:7px;border-radius:var(--r-hair);background:rgba(67,224,138,.3)}
.cph-art .cloud{width:22px;height:13px;border-radius:7px;background:rgba(124,108,255,.42)}
.cph-art .doc{width:15px;height:19px;border-radius:2px;background:rgba(255,255,255,.1);
  border:1px solid var(--line2)}
.cph-art .bubble{height:13px;border-radius:7px;background:rgba(47,123,246,.38)}
.cph-art .bubble.l{width:52%}
.cph-art .bubble.r{width:38%;margin-left:auto;background:rgba(40,215,238,.3)}
/* FIXED-SIZE GLYPHS, NOT FULL-WIDTH REPEATS. The calendar and the chart were
   repeating gradients stretched across the whole row, which at this scale read
   as a barcode and an empty box rather than as a calendar and a chart. A small
   glyph of a known size beside a couple of lines is legible, and it matches how
   every other feature in this set is drawn. */
.cph-art .cal{width:58px;height:24px;border-radius:3px;border:1px solid var(--line2);
  background:
    linear-gradient(to bottom,rgba(40,215,238,.3) 0 6px,transparent 6px),
    linear-gradient(90deg,transparent 0 5px,rgba(255,255,255,.18) 5px 15px,
      transparent 15px 23px,rgba(255,255,255,.18) 23px 33px,
      transparent 33px 41px,rgba(255,255,255,.18) 41px 51px,transparent 51px);
  background-position:0 0,0 11px;background-size:100% 100%,100% 9px;
  background-repeat:no-repeat}
.cph-art .chart{width:58px;height:26px;border-radius:2px;background:
  linear-gradient(to top,rgba(40,215,238,.42) 0 55%,transparent 55%),
  linear-gradient(to top,rgba(47,123,246,.42) 0 100%,transparent 100%),
  linear-gradient(to top,rgba(124,108,255,.42) 0 72%,transparent 72%);
  background-size:14px 100%,14px 100%,14px 100%;
  background-position:0 100%,22px 100%,44px 100%;
  background-repeat:no-repeat}
.cph-art .win{width:100%;height:22px;border-radius:3px;border:1px solid var(--line2);
  background:linear-gradient(to bottom,rgba(255,255,255,.1) 0 6px,transparent 6px)}
.cph-art .stack{width:26px;height:18px;border-radius:3px;background:rgba(124,108,255,.32);
  box-shadow:3px 3px 0 rgba(124,108,255,.18),6px 6px 0 rgba(124,108,255,.1)}

.cspec{margin-top:20px}
.cspec ul{list-style:none;margin:0 0 18px;padding:0}
.cspec li{padding:10px 0;border-bottom:1px solid rgba(255,255,255,.055);font-size:.88rem;color:var(--slate2);line-height:1.55}
.cspec li b{color:#fff}
.cspec-none{color:var(--slate)}
@media(max-width:860px){.cfg{grid-template-columns:1fr}}

/* The build-gate list on the engineering page. Same datasheet restraint as the live
   measurement panels: this is evidence, and evidence that is heavily styled reads as
   marketing. */
.gates{border:1px solid var(--line2);border-radius:var(--r-lg);background:rgba(10,17,33,.55);
  backdrop-filter:blur(14px);padding:4px 22px;box-shadow:var(--e-2)}
.grow{display:grid;grid-template-columns:210px 1fr;gap:18px;align-items:baseline;
  padding:14px 0;border-bottom:1px solid rgba(255,255,255,.055)}
.grow:last-child{border-bottom:0}
.grow b{font-family:var(--mono);font-size:.78rem;letter-spacing:.6px;color:var(--cyan)}
.grow span{color:var(--slate2);font-size:.9rem;line-height:1.55}
@media(max-width:620px){.grow{grid-template-columns:1fr;gap:4px}}

/* ---------- marquee ------------------------------------------------------ */
.marq-wrap{
  margin-top:14px;border-top:1px solid var(--line);border-bottom:1px solid var(--line);
  padding:15px 0;overflow:hidden;position:relative;z-index:3;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 10%,#000 90%,transparent);
  mask-image:linear-gradient(90deg,transparent,#000 10%,#000 90%,transparent);
}
.marq{display:flex;gap:40px;width:max-content;animation:marq 26s linear infinite}
.marq span{display:inline-flex;align-items:center;gap:10px;color:var(--slate);font-weight:600;font-size:.9rem;white-space:nowrap;font-family:var(--mono)}
.marq span .d{width:6px;height:6px;border-radius:50%;background:var(--cyan);flex:none}

/* ---------- sections ----------------------------------------------------- */
section{position:relative;padding:22px 0}
@media(max-width:820px){section{padding:16px 0}}
/* Section headings open with a short accent hairline rather than an eyebrow label.
   Gate 51: a label above all 26 section headings was a template signature, and the words
   were generic category names ("What you get", "How it works") that the h2 under them
   already said. The rule keeps the vertical rhythm the label gave without the extra word.
   The hero eyebrow is untouched; it names the service and is genuinely load-bearing. */
/* Section heads are CENTRED everywhere, Sean's call on 2 October. The site had 42
   left-aligned and 3 centred, so the accent rule above a heading landed in a
   different place depending on which page you were on.

   The intro paragraph centres with it. That was left alone at first, because the
   width cap came off these in the same session so they now fill the content column
   and long centred lines are harder to read, but Sean's call was uniform centring
   across the whole site and a centred heading over a left paragraph was the exact
   inconsistency he was pointing at. .shead.c is kept as a no-op alias so existing
   markup still parses. */
.shead{margin-bottom:16px;text-align:center}
.shead::before{
  content:"";display:block;width:34px;height:2px;border-radius:var(--r-hair);
  background:var(--cyan);margin-bottom:18px;
}
.shead::before{margin-left:auto;margin-right:auto}
.shead.c{margin-left:auto;margin-right:auto}
.shead h2{font-size:var(--t-h2);margin-top:12px;text-wrap:balance}
.shead p{color:var(--slate);font-size:1.08rem;margin-top:12px;max-width:none}
.shead.c p{margin-left:auto;margin-right:auto}

/* NEW. Interior page hero. The mockups only ever had a homepage.

   CENTRED, to match the section heads. Sean's call on 2 October: the H1 follows the
   same pattern as every h2 on the site, so the heading, the lead under it and the
   buttons all centre together. Centring the h1 alone would have reproduced the exact
   heading-over-left-paragraph mismatch that pass was fixing.

   The BREADCRUMBS stay left. They are navigation above the heading block rather than
   part of it, and a centred breadcrumb trail reads as a mistake rather than a choice. */
.phero{position:relative;padding:24px 0 12px;text-align:center}
.phero h1{font-size:clamp(2.1rem,4.2vw,3.2rem);margin:12px 0 0;letter-spacing:-.022em;line-height:1.05;text-wrap:balance}
.phero .sub{color:var(--slate);font-size:1.12rem;margin:14px 0 0;max-width:none}
.phero .actions{display:flex;gap:14px;flex-wrap:wrap;margin-top:20px;justify-content:center}
.phero .crumbs{text-align:left}

/* The HOMEPAGE hero carries NO composition. The laptop, phone and card deck is
   Orbit's artwork and now lives only on orbit.html. Sean's instruction on 2 October
   was about the ANIMATION, not about what was drawn inside it: re-skinning the same
   deck with a website wireframe left the same object in the same place, which is why
   he said nothing had changed. One column, centred, and the layered background does
   the visual work. */
.hero.solo .hero-in{grid-template-columns:1fr;justify-items:center;text-align:center}
.hero.solo .hero-in > div{max-width:980px}
.hero.solo h1{margin-top:18px}
.hero.solo .sub{max-width:760px;margin-left:auto;margin-right:auto}
.hero.solo .actions,
.hero.solo .stores{justify-content:center}
.hero.solo{padding:46px 0 18px}
@media(max-width:980px){.hero.solo{padding:28px 0 12px}}

/* NEW. .hero on an INTERIOR page. orbit.html uses the full hero grid, because it
   now carries the laptop-and-phone composition moved off the homepage, but it should
   not shout at homepage scale. Keeping the .hero class is what makes theme/hero.js
   pick the deck up with no change: every selector in it is scoped to .hero. */
.hero.hsub{padding:24px 0 12px;position:relative}
.hero.hsub h1{font-size:clamp(2.1rem,4.2vw,3.2rem);letter-spacing:-.022em;line-height:1.05;text-wrap:balance}
.hero.hsub .sub{font-size:1.12rem;max-width:none}
/* Orbit keeps the two column hero, so its text centres inside its own column
   rather than across the page. Same pattern, different container. */
.hero.hsub .hero-in > div:first-child{text-align:center}
.hero.hsub .actions{justify-content:center}
.hero.hsub .crumbs{text-align:left}
.hero.hsub .crumbs{margin-bottom:6px}

/* NEW. Breadcrumbs, to match the BreadcrumbList schema the site already emits. */
.crumbs{display:flex;flex-wrap:wrap;gap:8px;font-size:.82rem;color:var(--slate);margin-bottom:12px}
.crumbs a{color:var(--slate)}
.crumbs a:hover{color:#fff}
.crumbs span[aria-current]{color:var(--slate2)}
.crumbs .sep{opacity:.5}

/* NEW. Long-form prose, for the content the audit says these pages are short of. */
.prose{max-width:none}
.prose h2{font-size:clamp(1.6rem,3vw,2.3rem);margin:22px 0 10px}
.prose h3{font-size:1.18rem;margin:16px 0 6px}
/* Headings span; body text keeps a readable measure rather than running to 1200px. */
.prose p,.prose ul,.prose ol{max-width:none}
.prose p{color:var(--slate2);margin-bottom:12px}
.prose ul,.prose ol{margin:0 0 12px 22px;color:var(--slate2)}
.prose li{margin-bottom:8px}
.prose li::marker{color:var(--cyan)}
.prose a:not(.btn){color:var(--cyan);text-decoration:underline;text-underline-offset:3px}
.prose > :first-child{margin-top:0}
.prose .lead{font-size:1.1rem;color:var(--slate2)}

/* ---------- code panel -------------------------------------------------- */
.build2{display:grid;grid-template-columns:.95fr 1.05fr;gap:36px;align-items:center}
.techchips{display:flex;flex-wrap:wrap;gap:9px;margin-top:20px}
.techchips span{font-family:var(--mono);font-size:.78rem;color:var(--slate2);background:rgba(255,255,255,.04);border:1px solid var(--line);border-radius:var(--r-xs);padding:7px 12px}
.code{background:rgba(10,17,34,.85);border:1px solid var(--line2);border-radius:16px;overflow:hidden;box-shadow:0 30px 70px rgba(0,0,0,.5);backdrop-filter:blur(6px)}
.code .winbar{height:38px}
.code .winbar .fn{margin-left:12px;font-family:var(--mono);font-size:.76rem;color:var(--slate)}
.code pre{margin:0;padding:18px 20px;font-family:var(--mono);font-size:.82rem;line-height:1.85;overflow-x:auto;position:relative}
/* The panel types itself out by CLIPPING each line, never by rebuilding it. Every
   character is in the DOM from the start, so the panel is always full size and cannot
   shove the section below it around while it plays.

   width:max-content is load-bearing: a full-width block would make a 50% clip mean half
   the panel rather than half the text, and the per-character stepping would not line up.

   .type is added by theme/hero.js and only when the animation is actually going to run,
   so with no JavaScript, or under reduced motion, no clip is ever applied and the panel
   renders complete exactly as it does now. */
.code .cl{display:block;width:max-content}
.code.type .cl{clip-path:inset(0 100% 0 0)}
/* While the loop runs the travelling caret does all the work, so the static one at
   the end of the last line would just sit underneath it. It stays as the no-JS and
   reduced-motion state, where no .type class is ever added. */
.code.type .cursor{display:none}
.tcaret{position:absolute;top:0;left:0;width:8px;height:15px;margin-top:5px;background:var(--cyan);pointer-events:none;animation:blink 1.1s step-end infinite}
.code .ln{color:rgba(255,255,255,.25);margin-right:16px;-webkit-user-select:none;user-select:none}
.kw{color:#7c9cff}.fn2{color:#28d7ee}.str{color:#43e08a}.num{color:#e2a24b}.cm{color:#5a6b88}.pn{color:#c9d6ea}
.cursor{display:inline-block;width:8px;height:15px;background:var(--cyan);vertical-align:-2px;animation:blink 1.1s step-end infinite}
@media(max-width:820px){.build2{grid-template-columns:1fr}.code pre{font-size:.74rem}}

/* ---------- bento tiles -------------------------------------------------- */
.bento{display:grid;grid-template-columns:repeat(3,1fr);grid-auto-rows:minmax(200px,auto);gap:18px}
.tile{
  position:relative;border-radius:var(--r-lg);border:1px solid var(--line);
  background:var(--panel);backdrop-filter:blur(10px);padding:26px;overflow:hidden;
  transition:.25s;display:flex;flex-direction:column;
}
.tile:hover{transform:translateY(-5px);border-color:var(--line2);box-shadow:var(--e-2)}
.tile .tag{font-size:.7rem;font-weight:800;letter-spacing:.7px;text-transform:uppercase;color:var(--cyan);margin-bottom:9px}
.tile h3{font-size:1.45rem;margin-bottom:9px}
.tile p{color:var(--slate);font-size:.9rem}
.tile .lk{margin-top:auto;padding-top:16px;display:inline-flex;align-items:center;gap:7px;color:#fff;font-weight:700;font-size:.86rem}
.tile:hover .lk{gap:11px}
.tile .lk svg{color:var(--cyan)}
.tile .glow{position:absolute;width:280px;height:280px;border-radius:50%;filter:blur(70px);opacity:.4;pointer-events:none}
/* .tile.wide is gone. The App Development tile used to span two columns and two
   rows and carry a capability chip row, which duplicated app-development.html.
   Sean's call on 2 October: all three pillars are signposts, so all three are the
   same size and sit in one line. */
.tile.g-blue .glow{background:var(--blue);right:-60px;top:-60px}
.tile.accent{background:linear-gradient(135deg,rgba(47,123,246,.18),rgba(124,108,255,.18));border-color:rgba(124,108,255,.32)}
.tile.accent .tag{color:var(--violet)}
.tile.g-violet .glow{background:var(--violet);left:-60px;bottom:-70px}
.tile.g-lime .glow{background:var(--lime);right:-60px;bottom:-70px;opacity:.3}
.tile.g-cyan .glow{background:var(--cyan);right:-60px;top:-60px;opacity:.3}
/* The two app logos inside the App Development pillar tile. The homepage used to
   carry a whole "Two apps we built, and still run" section, which was the same two
   cards with near-identical copy to the one on app-development.html. Sean's call on
   2 October: the proof rides along with the signpost instead, and the descriptions
   live only on the page somebody reaches when they are actually interested. */
.tproof{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin:16px 0 4px}
.tproof img{
  width:32px;height:32px;border-radius:var(--r-xs);border:1px solid var(--line2);
  display:block;flex:none;
}
.tproof span{color:var(--slate);font-size:.8rem;flex:1;min-width:150px}
.modrow{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0 4px}
.modrow span{font-size:.74rem;color:var(--slate2);background:rgba(255,255,255,.05);border:1px solid var(--line);border-radius:var(--r-pill);padding:6px 12px}
@media(max-width:900px){.bento{grid-template-columns:1fr 1fr}}
@media(max-width:620px){.bento{grid-template-columns:1fr}}


/* ---------- band and steps ---------------------------------------------- */
.band{
  border-radius:var(--r-xl);border:1px solid var(--line);
  background:linear-gradient(160deg,rgba(18,32,60,.7),rgba(10,18,36,.7));
  backdrop-filter:blur(8px);padding:28px 32px;position:relative;overflow:hidden;
}
.band .glow{position:absolute;width:420px;height:420px;border-radius:50%;filter:blur(90px);background:rgba(124,108,255,.32);right:-100px;top:-120px;pointer-events:none}
.band > *{position:relative}
.steps{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-top:18px}
.step{background:rgba(255,255,255,.03);border:1px solid var(--line);border-radius:var(--r);padding:20px;position:relative}
.step .n{
  width:34px;height:34px;border-radius:var(--r-sm);background:var(--grad);color:#04121b;
  font-family:var(--head);font-weight:800;display:flex;align-items:center;justify-content:center;margin-bottom:14px;
}
.step h4{color:#fff;font-size:1.05rem;margin-bottom:6px}
.step p{color:var(--slate);font-size:.85rem}
@media(max-width:820px){.band{padding:22px 18px}.steps{grid-template-columns:1fr 1fr}}
@media(max-width:520px){.steps{grid-template-columns:1fr}}

/* ---------- the FAQ page -------------------------------------------------
   Eleven groups is a long page, so it opens with a jump list. Each group keeps
   its own id because the page it came off links straight to it: somebody sent
   here from the VoIP page lands on the VoIP questions, not at the top. */
.faqjump{display:flex;flex-wrap:wrap;gap:9px;margin-top:4px}
.faqjump a{
  font-size:.84rem;color:var(--slate2);background:rgba(255,255,255,.04);
  border:1px solid var(--line);border-radius:var(--r-pill);padding:8px 15px;
  transition:.18s;
}
.faqjump a:hover{color:#fff;border-color:var(--line2);background:rgba(255,255,255,.08)}
.faqjump a:focus-visible{box-shadow:var(--glow-focus)}
.faq-group{padding-top:30px;padding-bottom:0}
.faq-group:last-of-type{padding-bottom:24px}
/* scroll-margin so a jump link does not park the heading under the sticky header */
.faq-group{scroll-margin-top:86px}

/* ---------- service area, two panels -------------------------------------
   Replaces two paragraphs whose real content was one distinction: remote work
   has no geography, on-site work does. No map on purpose; see the note on
   jstg/home.where_we_work(). */
.area2{display:grid;grid-template-columns:1fr 1.25fr;gap:18px;margin-top:22px}
.apanel{
  background:var(--panel);backdrop-filter:blur(10px);border:1px solid var(--line);
  border-radius:var(--r-lg);padding:26px;position:relative;overflow:hidden;
}
.apanel .ic{
  width:46px;height:46px;border-radius:var(--r-sm);display:flex;align-items:center;
  justify-content:center;background:var(--grad);color:#04121b;margin-bottom:16px;
}
.apanel .ic svg{width:23px;height:23px}
.apanel .atag{font-size:.7rem;font-weight:800;letter-spacing:.7px;text-transform:uppercase;
  color:var(--cyan);margin-bottom:7px}
.apanel h3{font-size:1.5rem;margin-bottom:9px}
.apanel p{color:var(--slate);font-size:.95rem}
.towns{display:flex;flex-wrap:wrap;gap:7px;margin-top:18px}
.towns span{
  font-size:.76rem;color:var(--slate2);background:rgba(255,255,255,.05);
  border:1px solid var(--line);border-radius:var(--r-pill);padding:5px 11px;
}
@media(max-width:820px){.area2{grid-template-columns:1fr}}

/* ---------- the engagement flow ------------------------------------------
   Four connected nodes for "A straightforward way of working". It was four
   headed paragraphs, which read as a wall of text on an otherwise visual page.
   The connector is a ::after on each node rather than a line drawn behind the
   row, so it cannot fall out of step when the grid rewraps. */
/* As many columns as there are steps, not four. This was hard-coded to repeat(4,1fr)
   for the homepage's four-step flow, so the app builder's three steps filled columns
   one to three and left 270px of empty grid on the right: the whole group read as
   shoved to the left rather than centred in its panel. grid-auto-flow:column makes
   the track count follow the markup, so a flow with any number of steps fills its
   container evenly. */
.flow{display:grid;grid-auto-flow:column;grid-auto-columns:1fr;margin-top:28px}
.fnode{position:relative;padding:0 14px;text-align:center}
.fnode::after{
  content:"";position:absolute;top:25px;left:calc(50% + 36px);right:calc(-50% + 36px);height:2px;
  background:linear-gradient(90deg,rgba(40,215,238,.5),rgba(90,123,255,.18));
}
.fnode:last-child::after{display:none}
.fnum{
  width:52px;height:52px;border-radius:50%;margin:0 auto 15px;position:relative;z-index:1;
  display:flex;align-items:center;justify-content:center;
  font-family:var(--head);font-weight:800;font-size:1.15rem;color:#04121b;
  background:var(--grad);box-shadow:0 0 0 7px rgba(40,215,238,.07);
}
.fnode h3{font-size:1.02rem;color:#fff;margin-bottom:7px}
.fnode p{color:var(--slate);font-size:.86rem}
@media(max-width:820px){
  .flow{grid-auto-flow:row;grid-auto-columns:auto}
  .fnode{display:grid;grid-template-columns:52px 1fr;gap:16px;text-align:left;padding:0 0 26px}
  .fnode .fnum{margin:0}
  .fnode::after{top:56px;bottom:4px;left:25px;right:auto;width:2px;height:auto;
    background:linear-gradient(180deg,rgba(40,215,238,.5),rgba(90,123,255,.12))}
  .fnode:last-child{padding-bottom:0}
}

/* ---------- voip summary and plan rows ---------------------------------- */
.voip{display:grid;grid-template-columns:1fr 1fr;gap:22px;align-items:stretch}
.voip-copy{
  background:var(--panel);backdrop-filter:blur(10px);border:1px solid var(--line);
  border-radius:var(--r-lg);padding:38px;display:flex;flex-direction:column;justify-content:center;
}
.voip-copy h2{font-size:clamp(1.6rem,3vw,2.3rem);margin-bottom:14px}
.voip-copy p{color:var(--slate);margin-bottom:22px}
.wave{display:flex;align-items:flex-end;gap:5px;height:38px;margin-bottom:22px}
.wave i{width:5px;border-radius:var(--r-hair);background:var(--grad);transform-origin:bottom;animation:wavebar 1.6s var(--ease-soft) infinite}
.plans{display:grid;gap:12px}
.plan{
  display:flex;align-items:center;gap:14px;background:var(--panel);backdrop-filter:blur(10px);
  border:1px solid var(--line);border-radius:var(--r);padding:19px 22px;transition:.2s;
}
.plan:hover{border-color:var(--line2);transform:translateX(4px)}
.plan .nm{font-family:var(--head);font-weight:700;color:#fff;font-size:1.05rem}
.plan .ds{font-size:.8rem;color:var(--slate)}
.plan .pr{margin-left:auto;font-family:var(--head);font-weight:800;color:#fff;font-size:1.3rem;text-align:right;flex:none}
.plan .pr small{font-size:.72rem;color:var(--slate);font-weight:600}
.plan.feat{background:linear-gradient(135deg,rgba(47,123,246,.24),rgba(40,215,238,.14));border-color:rgba(40,215,238,.4)}
.plan.feat .pr{color:var(--cyan)}
@media(max-width:820px){.voip{grid-template-columns:1fr}.voip-copy{padding:28px 22px}}

/* ---------- services grid ----------------------------------------------- */
.srv{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
.srv.five{grid-template-columns:repeat(5,1fr)}
.sv{
  background:var(--panel);backdrop-filter:blur(10px);border:1px solid var(--line);
  border-radius:var(--r);padding:22px 18px;text-align:center;transition:.2s;
}
.sv:hover{transform:translateY(-4px);border-color:var(--line2)}
.sv .ic{
  width:46px;height:46px;border-radius:var(--r-sm);background:rgba(47,123,246,.16);
  border:1px solid rgba(47,123,246,.28);display:flex;align-items:center;justify-content:center;margin:0 auto 12px;
}
.sv .ic svg{width:22px;height:22px;color:var(--cyan)}
.sv b{color:#fff;font-family:var(--head);font-size:.9rem;font-weight:700;display:block}
.sv p{color:var(--slate);font-size:.8rem;margin-top:7px}
@media(max-width:820px){.srv,.srv.five{grid-template-columns:1fr 1fr 1fr}}
@media(max-width:520px){.srv,.srv.five{grid-template-columns:1fr 1fr}}

/* ---------- stats ------------------------------------------------------- */
.statband{
  display:grid;grid-template-columns:repeat(3,1fr);gap:20px;
  border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:20px 0;
}
.sb{text-align:center}
.sb b{font-family:var(--head);font-size:clamp(1.8rem,3.4vw,3rem);display:block;line-height:1}
.sb span{color:var(--slate);font-size:.86rem}
@media(max-width:620px){.statband{grid-template-columns:1fr;gap:24px}}
/* THE FLANKED VARIANT: one number, the heading, the other number. Sean, 20 October.
   The wrapper keeps .statband on purpose, because theme/motion.js finds the counters
   both as `b[data-count]` inside a stagger container and, on the legacy path, by the
   .statband class itself. The borders come off: they read as a rule above and below a
   row of figures, and framing a section heading in them looks like a mistake. */
.statband.flank{
  grid-template-columns:1fr minmax(0,2.1fr) 1fr;gap:34px;align-items:center;
  border:0;padding:0;
}
.statband.flank .shead{margin:0}
/* Narrow: the heading takes the full width and the two numbers sit under it, side by
   side. This has to beat the 620px rule above, which it does on specificity alone
   rather than on order, so it holds all the way down. */
@media(max-width:900px){
  .statband.flank{grid-template-columns:1fr 1fr;gap:22px 18px}
  .statband.flank .shead{grid-column:1/-1;grid-row:1;margin-bottom:6px}
}

/* ---------- why cards --------------------------------------------------- */
.why{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}
.wy{background:var(--panel);backdrop-filter:blur(10px);border:1px solid var(--line);border-radius:var(--r-lg);padding:30px 26px}
.wy .ic{width:52px;height:52px;border-radius:var(--r);background:var(--grad);display:flex;align-items:center;justify-content:center;margin-bottom:16px}
.wy .ic svg{width:24px;height:24px;color:#04121b}
.wy h3{font-size:1.1rem;margin-bottom:8px}
.wy p{color:var(--slate);font-size:.9rem}
@media(max-width:820px){.why{grid-template-columns:1fr}}

/* NEW. Generic feature card grid, for the interior pages. */
.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}
.cards.two{grid-template-columns:repeat(2,1fr)}
.card{background:var(--panel);backdrop-filter:blur(10px);border:1px solid var(--line);border-radius:var(--r-lg);padding:26px;transition:.2s}
.card:hover{border-color:var(--line2);transform:translateY(-4px)}
.card .ic{width:46px;height:46px;border-radius:var(--r-sm);background:rgba(40,215,238,.14);border:1px solid rgba(40,215,238,.26);display:flex;align-items:center;justify-content:center;margin-bottom:15px}
.card .ic svg{width:22px;height:22px;color:var(--cyan)}
/* Real app icons rather than a generic glyph, for Orbit and Fitara. The tint and
   the cyan border that .card .ic gives a line icon are dropped: these are finished
   artwork with their own backgrounds, so the tile is just a rounded mask around
   them. Sized 64 on screen from a 128px file, so it stays sharp on a retina display.
   Source files are the real app icons from Orbit_App and PT-Mobile. */
.card .ic.logo{
  width:64px;height:64px;border-radius:var(--r);background:none;border:1px solid var(--line2);
  padding:0;overflow:hidden;
}
.card .ic.logo img{width:100%;height:100%;display:block;object-fit:cover}
.card h3{font-size:1.1rem;margin-bottom:8px}
.card p{color:var(--slate);font-size:.9rem}
.card ul{margin:12px 0 0 18px;color:var(--slate);font-size:.88rem}
.card li{margin-bottom:6px}
.card li::marker{color:var(--cyan)}
@media(max-width:900px){.cards,.cards.two{grid-template-columns:1fr 1fr}}
@media(max-width:620px){.cards,.cards.two{grid-template-columns:1fr}}

/* ---------- pricing cards (NEW, the full VoIP page) --------------------- */
.ptable{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;align-items:start}
.pcard{
  position:relative;background:var(--panel);backdrop-filter:blur(10px);
  border:1px solid var(--line);border-radius:var(--r-lg);padding:32px 28px;
  display:flex;flex-direction:column;transition:.25s;
}
.pcard:hover{border-color:var(--line2);transform:translateY(-4px)}
.pcard.feat{
  background:linear-gradient(150deg,rgba(47,123,246,.22),rgba(40,215,238,.1));
  border-color:rgba(40,215,238,.42);box-shadow:var(--e-2);
}
.pcard .pop{
  position:absolute;top:-13px;left:50%;transform:translateX(-50%);
  background:var(--grad);color:#04121b;font-size:.68rem;font-weight:800;
  letter-spacing:.8px;text-transform:uppercase;padding:6px 14px;border-radius:var(--r-pill);white-space:nowrap;
}
.pcard .nm{font-size:.72rem;font-weight:800;letter-spacing:1.2px;text-transform:uppercase;color:var(--cyan)}
.pcard h3{font-size:1.35rem;margin:8px 0 4px}
.pcard .price{font-family:var(--head);font-weight:800;color:#fff;font-size:2.6rem;line-height:1;margin:16px 0 4px}
.pcard .price small{font-size:.9rem;color:var(--slate);font-weight:600}
.pcard .price .cur{font-size:1.4rem;vertical-align:.5rem}
.pcard .blurb{color:var(--slate);font-size:.9rem;margin-bottom:20px}
.pcard ul{list-style:none;margin:0 0 24px;flex:1}
.pcard li{display:flex;gap:10px;align-items:flex-start;font-size:.89rem;color:var(--slate2);padding:7px 0;border-bottom:1px solid var(--line)}
.pcard li:last-child{border-bottom:none}
.pcard li svg{width:16px;height:16px;color:var(--lime);flex:none;margin-top:4px}
/* The "before" half of a two-column comparison. Its icons are crosses, and inheriting the
   lime above made them GREEN crosses, which signals approval and refusal at the same time.
   Muted, so the eye reads the positive column as the answer. */
.pcard.neg li svg{color:var(--slate)}
.pcard.neg li{color:var(--slate)}
@media(max-width:900px){.ptable{grid-template-columns:1fr;max-width:460px;margin:0 auto}}

/* NEW. The no-VAT notice, which appears on three pages and is a legal statement. */
.note{
  background:rgba(40,215,238,.07);border:1px solid rgba(40,215,238,.22);
  border-radius:var(--r);padding:16px 20px;font-size:.88rem;color:var(--slate2);
  display:flex;gap:12px;align-items:flex-start;
}
.note svg{width:18px;height:18px;color:var(--cyan);flex:none;margin-top:3px}
.note b{color:#fff}
.note.warn{background:rgba(226,162,75,.08);border-color:rgba(226,162,75,.26)}
.note.warn svg{color:var(--amber)}

/* ---------- tables (NEW) ------------------------------------------------ */
.tblwrap{overflow-x:auto;border:1px solid var(--line);border-radius:var(--r-lg);background:var(--panel);backdrop-filter:blur(10px)}
.tbl{width:100%;border-collapse:collapse;min-width:520px}
.tbl th,.tbl td{text-align:left;padding:15px 20px;border-bottom:1px solid var(--line);font-size:.9rem}
.tbl th{font-family:var(--body);font-size:.72rem;letter-spacing:1.1px;text-transform:uppercase;color:var(--cyan);font-weight:700}
.tbl tr:last-child td{border-bottom:none}
.tbl td:first-child{color:#fff;font-weight:600}

/* ---------- FAQ accordion (NEW) ---------------------------------------- */
.faq{display:grid;gap:10px;max-width:860px;margin:0 auto}
/* The FAQ sits immediately above the closing CTA, so it is pulled in tight and centred to
   read as one closing block rather than as another left-aligned section. */
.faq-section{padding-bottom:6px}
.faq-section + .cta-band{padding-top:0}
.faq details{background:var(--panel);backdrop-filter:blur(10px);border:1px solid var(--line);border-radius:var(--r);overflow:hidden;transition:border-color .2s}
.faq details[open]{border-color:var(--line2)}
.faq summary{
  cursor:pointer;padding:16px 22px;font-family:var(--head);font-weight:700;color:#fff;
  font-size:1.02rem;display:flex;align-items:center;gap:14px;list-style:none;
}
.faq summary::-webkit-details-marker{display:none}
.faq summary::after{
  content:"";width:11px;height:11px;margin-left:auto;flex:none;
  border-right:2px solid var(--cyan);border-bottom:2px solid var(--cyan);
  transform:rotate(45deg);transition:transform .2s;
}
.faq details[open] summary::after{transform:rotate(-135deg)}
.faq .ans{padding:0 24px 22px;color:var(--slate);font-size:.94rem}
.faq .ans p+p{margin-top:12px}

/* ---------- forms (NEW) ------------------------------------------------- */
.formcard{
  background:var(--panel);backdrop-filter:blur(12px);border:1px solid var(--line);
  border-radius:var(--r-lg);padding:34px;
}
.formcard h3{font-size:1.35rem;margin-bottom:8px}
.formcard .intro{color:var(--slate);font-size:.92rem;margin-bottom:26px}
.field{margin-bottom:17px}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:15px}
.field label{display:block;font-size:.8rem;font-weight:600;color:var(--slate2);margin-bottom:7px}
.field label .req{color:var(--cyan)}
.inp,.sel,.ta{
  width:100%;background:rgba(255,255,255,.04);border:1px solid var(--line2);
  border-radius:var(--r);padding:13px 15px;color:#fff;font-size:.95rem;transition:.2s;
}
.inp::placeholder,.ta::placeholder{color:rgba(159,176,204,.55)}
.inp:focus,.sel:focus,.ta:focus{outline:none;border-color:var(--cyan);background:rgba(255,255,255,.07);box-shadow:var(--glow-focus)}
.inp:user-invalid,.ta:user-invalid{border-color:var(--red)}
.ta{min-height:120px;resize:vertical}
.sel{appearance:none;cursor:pointer;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239fb0cc' stroke-width='2.5' stroke-linecap='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 13px center;background-size:16px;padding-right:40px}
.sel option{background:#111a30;color:#fff}
.phonerow{display:grid;grid-template-columns:118px 1fr;gap:10px}
.consent{display:flex;gap:11px;align-items:flex-start;font-size:.84rem;color:var(--slate);cursor:pointer;line-height:1.5}
.consent input{
  appearance:none;width:20px;height:20px;flex:none;margin-top:1px;border-radius:var(--r-xs);
  border:1px solid var(--line2);background:rgba(255,255,255,.05);cursor:pointer;position:relative;transition:.15s;
}
.consent input:checked{background:var(--grad);border-color:transparent}
.consent input:checked::after{
  content:"";position:absolute;left:6px;top:2px;width:6px;height:11px;
  border-right:2px solid #04121b;border-bottom:2px solid #04121b;transform:rotate(40deg);
}
/* the honeypot. Off screen rather than display:none, because some bots skip
   hidden fields but not positioned ones. */
.hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
.fstat{margin-top:16px;padding:14px 16px;border-radius:var(--r);font-size:.9rem;display:none}
.fstat.show{display:block}
.fstat.ok{background:rgba(67,224,138,.1);border:1px solid rgba(67,224,138,.3);color:#b6f2d2}
.fstat.err{background:rgba(255,107,107,.1);border:1px solid rgba(255,107,107,.3);color:#ffc9c9}
@media(max-width:620px){.row2{grid-template-columns:1fr}.formcard{padding:24px 20px}}

.contact-grid{display:grid;grid-template-columns:.85fr 1.15fr;gap:28px;align-items:start}
@media(max-width:900px){.contact-grid{grid-template-columns:1fr}}
.cinfo{display:grid;gap:14px}
.cinfo .ci{display:flex;gap:14px;align-items:flex-start;background:var(--panel);backdrop-filter:blur(10px);border:1px solid var(--line);border-radius:var(--r);padding:20px}
.cinfo .ci .ic{width:42px;height:42px;border-radius:var(--r-sm);background:rgba(40,215,238,.14);border:1px solid rgba(40,215,238,.26);display:flex;align-items:center;justify-content:center;flex:none}
.cinfo .ci .ic svg{width:20px;height:20px;color:var(--cyan)}
.cinfo .ci .lb{font-size:.74rem;letter-spacing:1px;text-transform:uppercase;color:var(--slate);font-weight:700}
.cinfo .ci .vl{color:#fff;font-weight:700;font-size:1rem;margin-top:3px}
.cinfo .ci .vl a:hover{color:var(--cyan)}

/* ---------- before and after comparison (NEW) --------------------------
   THE TWO SOURCE IMAGES DO NOT MATCH, and the CSS has to absorb that.
   before.webp is 863x767 (ratio 1.125) and after.webp is 919x954 (ratio 0.963):
   the same street from the same position, but different framing and a different
   shape. Letting each image size itself, which is what the old site did, means
   the scene shifts as you drag the slider and the comparison quietly misleads.

   So both images fill ONE box at a fixed aspect ratio, cover-cropped from the
   top. The subject (the houses and the two cars) sits in the upper two thirds of
   both, so cropping the excess foreground road costs nothing and actually frames
   them better. Recapturing a properly matched pair would be better still and is
   noted for Sean. */
.ba{position:relative;border-radius:var(--r-lg);overflow:hidden;border:1px solid var(--line2);background:#0a1424;max-width:820px;aspect-ratio:4/3}
.ba > img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center top;display:block}
.ba .after{position:absolute;inset:0;overflow:hidden;width:50%}
/* The inner image is sized to the OUTER box, not to the clipped half, or it
   would squash as the slider moves. */
.ba .after img{position:absolute;top:0;left:0;height:100%;width:820px;max-width:none;object-fit:cover;object-position:center top}
.ba .handle{position:absolute;top:0;bottom:0;width:3px;background:var(--cyan);box-shadow:0 0 18px rgba(40,215,238,.7);left:50%;pointer-events:none}
.ba .handle::after{
  content:"";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
  width:38px;height:38px;border-radius:50%;background:var(--grad);border:2px solid #04121b;
}
.ba input[type=range]{
  position:absolute;inset:0;width:100%;height:100%;margin:0;opacity:0;cursor:ew-resize;
}
/* The comparison drifts on a slow loop so it reads as alive rather than as a static image
   somebody has to discover is draggable. Capped at 22% to 78% so neither state is ever
   fully hidden: a visitor glancing at any moment still sees both. CSS rather than GSAP,
   because this page deliberately does not load it. The moment anyone drags the slider,
   theme/app.js adds .manual and the loop stops for good. */
@keyframes ba-drift{0%,100%{width:30%}50%{width:70%}}
.ba:not(.manual) .after{animation:ba-drift 9s var(--ease-soft) infinite}
.ba:not(.manual) .handle{animation:ba-drift-h 9s var(--ease-soft) infinite}
@keyframes ba-drift-h{0%,100%{left:30%}50%{left:70%}}
@media(prefers-reduced-motion:reduce){
  .ba:not(.manual) .after{animation:none;width:50%}
  .ba:not(.manual) .handle{animation:none;left:50%}
}
.ba .lab{position:absolute;bottom:14px;font-size:.72rem;font-weight:800;letter-spacing:1px;text-transform:uppercase;background:rgba(8,13,26,.8);color:#fff;padding:6px 12px;border-radius:var(--r-pill);border:1px solid var(--line2)}
.ba .lab.l{left:14px}
.ba .lab.r{right:14px}

/* ---------- Orbit module picker (NEW markup, behaviour preserved) ------- */
.picker{display:grid;grid-template-columns:1.35fr .65fr;gap:22px;align-items:start}
.pickcats{display:grid;gap:18px}
.pcat h3{font-size:1.05rem;margin-bottom:12px}
.pmods{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.pmod{
  display:flex;gap:11px;align-items:flex-start;background:var(--panel);backdrop-filter:blur(8px);
  border:1px solid var(--line);border-radius:var(--r);padding:13px 15px;cursor:pointer;transition:.18s;
}
.pmod:hover{border-color:var(--line2)}
.pmod input{
  appearance:none;width:19px;height:19px;flex:none;margin-top:2px;border-radius:var(--r-xs);
  border:1px solid var(--line2);background:rgba(255,255,255,.05);cursor:pointer;position:relative;transition:.15s;
}
.pmod input:checked{background:var(--grad);border-color:transparent}
.pmod input:checked::after{content:"";position:absolute;left:6px;top:2px;width:5px;height:10px;border-right:2px solid #04121b;border-bottom:2px solid #04121b;transform:rotate(40deg)}
.pmod .nm{color:#fff;font-weight:600;font-size:.9rem}
.pmod .pp{color:var(--slate);font-size:.78rem;margin-top:2px}
.pmod.soon{opacity:.62;cursor:not-allowed}
.pmod.soon input{cursor:not-allowed}
.pmod .badge{
  display:inline-block;font-size:.62rem;font-weight:800;letter-spacing:.6px;text-transform:uppercase;
  color:var(--amber);background:rgba(226,162,75,.12);border:1px solid rgba(226,162,75,.3);
  padding:3px 8px;border-radius:var(--r-pill);margin-left:7px;vertical-align:1px;
}
.quote{
  position:sticky;top:96px;background:var(--panel);backdrop-filter:blur(12px);
  border:1px solid var(--line2);border-radius:var(--r-lg);padding:26px;
}
.quote h3{font-size:1.1rem;margin-bottom:4px}
.quote .base{font-size:.84rem;color:var(--slate);margin-bottom:18px}
.quote .tot{font-family:var(--head);font-weight:800;color:#fff;font-size:2.4rem;line-height:1}
.quote .tot small{font-size:.85rem;color:var(--slate);font-weight:600}
.quote .cnt{font-size:.84rem;color:var(--cyan);margin-top:6px}
.quote .disc{font-size:.76rem;color:var(--slate);margin:18px 0;line-height:1.55}
@media(max-width:900px){.picker{grid-template-columns:1fr}.quote{position:static}}
@media(max-width:560px){.pmods{grid-template-columns:1fr}}

/* ---------- real screenshots in device frames (NEW) -------------------- */
/* 30 PERCENT SMALLER, Sean's call, and the same cause as the Orbit console frame:
   these were sized by the 1200px content column, and going edge to edge let them grow
   to the width of the monitor, which made every phone frame enormous. 70% of the
   column, centred, and full width again on a phone where there is nothing to give. */
.shots{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;width:70%;margin-left:auto;margin-right:auto}
.shots.three{grid-template-columns:repeat(3,1fr)}
/* Two landscape console captures rather than four phone ones. Used by the Orbit
   pinned story's narrow-viewport fallback, which must be a .shots for the rule
   that hides it once the stage is running to reach it. */
.shots.two{grid-template-columns:repeat(2,1fr)}
@media(max-width:860px){.shots,.shots.three{width:100%}}
.frame{
  border-radius:var(--r-xl);background:#0a141d;border:7px solid #0c1a2b;overflow:hidden;
  box-shadow:var(--e-3);transition:.25s;
}
.frame:hover{transform:translateY(-6px)}
.frame img{width:100%;display:block}
.shotcap{text-align:center;font-size:.8rem;color:var(--slate);margin-top:11px}
@media(max-width:900px){.shots,.shots.three{grid-template-columns:1fr 1fr}}
@media(max-width:520px){.shots,.shots.three{grid-template-columns:1fr;max-width:280px;margin:0 auto}}

/* a wide browser frame holding a real screenshot */
.wframe{border:1px solid var(--line2);border-radius:var(--r);overflow:hidden;background:#0c1526;box-shadow:var(--e-3)}
.wframe img{width:100%;display:block}
/* 30 PERCENT SMALLER, Sean's call. The console screenshot used to be capped by the
   1200px content column; going edge to edge took that cap away and it became as wide
   as the monitor, which is far too much page for one settings screen. 70% of the
   column, centred. */
.wframe-sm{width:70%;margin:20px auto 0}
@media(max-width:860px){.wframe-sm{width:100%}}

/* Everything else on the site centres its headings now, so the two full width
   panels that did not were the ones left sticking out: .band, which holds the
   process steps, and .prose, which holds the long form sections. Sean's call,
   2 October.

   LISTS STAY LEFT. Centring the lines of a bulleted list puts the markers in a
   different place on every row, which reads as broken rather than as a choice.
   They are centred as a BLOCK instead, via display:table and auto margins.

   .build2 and .voip-copy are deliberately untouched: their text is one column of
   a two column layout sitting beside artwork, so there is no card width to span. */
.band h2,.band h3,.band p,.band .lead{text-align:center}
/* The step cards inside a band centre, numbers included. These were left-aligned
   at first, on the reasoning that every other card on the site is left inside, but
   Sean looked at them next to a centred heading and centred intro and called it:
   they are a numbered sequence rather than a grid of content cards, so they read
   better centred. The number badge has to be centred explicitly, since it is a
   fixed-size flex box and text-align does not move it. */
.band .step,.band .step h4,.band .step p{text-align:center}
.band .step .n{margin-left:auto;margin-right:auto}
/* The band's own closing button centres with its heading rather than hanging off
   the left edge under four centred cards. */
.band .bandcta{margin-top:30px;text-align:center}
.band .bandcta .btn{display:inline-flex}
.prose h2,.prose h3,.prose p,.prose .lead{text-align:center}
.prose ul,.prose ol{text-align:left;display:table;margin-left:auto;margin-right:auto}

/* ---------- two build routes ---------------------------------------------
   Replaces a .prose block that ran to about 200 words under two h3s. The real
   content was a comparison of two routes, so it is drawn as one: two panels that
   arrive from opposite sides and a joining bar underneath for the half that is
   true of both. The arrival is motion.js's existing .reveal--l / .reveal--r, so
   this needed no new JavaScript. */
.routes{display:grid;grid-template-columns:1fr 54px 1fr;align-items:stretch;gap:0;margin-top:26px}
.route{
  background:var(--panel);backdrop-filter:blur(10px);border:1px solid var(--line);
  border-radius:var(--r-lg);padding:26px;text-align:left;
}
.route .rtag{font-size:.7rem;font-weight:800;letter-spacing:.7px;text-transform:uppercase;
  color:var(--cyan);margin-bottom:8px}
.route h3{font-size:1.45rem;margin-bottom:8px}
.route .rlead{color:#fff;font-size:1rem;margin-bottom:14px}
.route ul{margin:0;padding:0;list-style:none}
.route li{
  color:var(--slate);font-size:.9rem;margin-bottom:10px;padding-left:22px;position:relative;
}
.route li::before{
  content:"";position:absolute;left:0;top:7px;width:9px;height:9px;border-radius:50%;
  background:var(--grad);
}
.route li b{color:#fff;font-weight:700}
.rsplit{display:flex;align-items:center;justify-content:center;position:relative}
.rsplit::before{content:"";position:absolute;top:0;bottom:0;width:1px;background:var(--line)}
.rsplit span{
  position:relative;font-family:var(--mono);font-size:.72rem;color:var(--slate);
  background:var(--bg);border:1px solid var(--line);border-radius:var(--r-pill);padding:5px 10px;
}
.rjoin{margin-top:0;text-align:center}
.rjoin-line{
  width:2px;height:34px;margin:0 auto;
  background:linear-gradient(180deg,rgba(40,215,238,.6),rgba(90,123,255,.15));
}
.rjoin p{color:var(--slate);font-size:1rem;max-width:760px;margin:16px auto 0}
.rjoin b{color:#fff}
@media(max-width:820px){
  .routes{grid-template-columns:1fr;gap:0}
  .rsplit{height:52px}
  .rsplit::before{top:auto;bottom:auto;left:0;right:0;width:auto;height:1px}
}

/* The configurator needed to say what it IS before anybody could use it. Sean,
   2 October: the heading did not explain it, the two question groups were bare
   labels, and the obvious assumption about a feature picker is that it prices the
   job. So it gets the three-step flow from the homepage above it, numbered badges
   on each group, and a plain line underneath saying it is not a quote. */
.cfg-steps{margin:0 0 30px}
.cfg-steps .fnode p{font-size:.84rem}
.cfg-h{display:flex;align-items:center;gap:10px}
.cfg-n{
  width:24px;height:24px;border-radius:50%;flex:none;display:inline-flex;
  align-items:center;justify-content:center;font-size:.78rem;font-weight:800;
  background:var(--grad);color:#04121b;font-family:var(--head);
}
.cfg-note{color:var(--slate);font-size:.84rem;margin-top:14px;text-align:left}

/* The configurator sits in a band of its own, so it reads as a feature rather
   than as loose content between two sections. Sean's call, 2 October. Same panel
   as the process bands, plus a small badge, because an interactive thing that
   looks like ordinary page furniture does not get used. */
.cfgbox{padding:30px 32px 34px}
.cfgbox .glow{background:rgba(40,215,238,.22);right:-120px;top:-140px}
.cfgbadge{
  display:inline-flex;align-items:center;gap:7px;font-size:.7rem;font-weight:800;
  letter-spacing:.7px;text-transform:uppercase;color:var(--cyan);
  background:rgba(40,215,238,.1);border:1px solid rgba(40,215,238,.3);
  border-radius:var(--r-pill);padding:6px 13px;margin-bottom:4px;
}
.cfgbadge::before{
  content:"";width:7px;height:7px;border-radius:50%;background:var(--cyan);flex:none;
}
/* the controls inside the band keep their own left alignment: .cfg is the app
   builder on app-development.html, .picker is Orbit's module chooser. Both sit in
   the same cfgbox so the two interactive things on the site look like siblings. */
.cfgbox .cfg,.cfgbox .cfg *,
.cfgbox .picker,.cfgbox .picker *{text-align:left}
.cfgbox .shead,.cfgbox .shead h2,.cfgbox .shead p{text-align:center}
.cfgbox .fnode{text-align:center}
@media(max-width:820px){.cfgbox{padding:24px 18px 26px}}

/* Orbit's app showcase, and the placeholders standing in until the captures exist.

   A placeholder holds the EXACT aspect ratio of the real screenshot, so dropping the
   files in moves nothing on the page. It is also unmistakably a placeholder rather
   than a broken image: this site has shipped enough things that looked fine in the
   markup and wrong on screen. See jstg/orbit.shot(). */
.shotcell{min-width:0}
.shotph{
  width:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:6px;text-align:center;padding:14px;
  background:
    repeating-linear-gradient(135deg,rgba(255,255,255,.028) 0 10px,transparent 10px 20px),
    linear-gradient(170deg,rgba(20,38,66,.85),rgba(10,18,34,.85));
  border:1px dashed var(--line2);
}
.shotph span{color:#fff;font-family:var(--head);font-size:.95rem;line-height:1.2}
.shotph em{color:var(--slate);font-style:normal;font-size:.72rem;letter-spacing:.4px;text-transform:uppercase}

/* The product mark at the top of a product hero. Orbit's logo appeared nowhere on
   its own page, which Sean spotted. It sits where the eyebrow used to, so the hero
   keeps the same rhythm: a small identifying thing, then the headline.

   NOT lazy loaded, deliberately: it is above the fold and the first thing the page
   should establish, so it loads eagerly like any hero image. */
/* The mark sits ON THE SAME LINE as the headline rather than above it, the two
   centring as one lockup. A flex row rather than an absolutely positioned mark,
   because the headline wraps to two or three lines depending on width and anything
   positioned against a fixed offset drifts out of alignment the moment it rewraps. */
.herolock{display:flex;align-items:center;justify-content:center;gap:24px}
.herolock h1{margin:0}
.heromark{
  width:208px;height:208px;border-radius:var(--r-xl);display:block;flex:none;
  border:1px solid var(--line2);box-shadow:var(--e-3);
}
/* ON A WIDE SCREEN IT MOVES OUT INTO THE PAGE GUTTER, which is the plain background
   to the left of the content column and is otherwise dead space. Sean's call.

   The arithmetic, because it is not obvious: .wrap is capped at 1200px and centred,
   so the gutter is (100% - 1200px) / 2 on each side. Pulling the mark fully into it
   means offsetting by half the viewport, minus half the content width, minus the
   mark and its gap. Below about 1420px that result goes negative and the logo would
   hang off the left edge of the window, so the gutter placement is gated on a width
   where the gutter actually exists. Everywhere narrower it stays in the flow above
   the headline, which is what the two rules below do. */
/* The stack point rises with the mark. At 208px it needs considerably more room
   beside a wrapping headline than it did at 104, so the lockup breaks to a column
   earlier and the mark steps down rather than crowding the words. */
@media(max-width:1100px){
  .herolock{flex-direction:column;gap:16px}
  .heromark{width:160px;height:160px}
}
@media(max-width:620px){.heromark{width:124px;height:124px}}
/* The gutter placement that used to live here is gone with the 1200px cap: there is
   no gutter to put anything in any more. The mark sits at the left of the hero column,
   which is now hard against the page padding anyway, so it lands in the same place the
   gutter version was aiming for. */

/* The hero laptop screen holds the admin console, cycling the same way the phone
   does. Same height as the .dash it replaced, so the laptop keeps its proportions. */
.lcycle{position:relative;height:280px;overflow:hidden;background:#0b1424}
.lscreen{position:absolute;inset:0;opacity:0}
.lscreen[data-i="0"]{opacity:1}
.lscreen img,.lscreen .shotph{width:100%;height:100%;object-fit:cover;object-position:top left;border:0;aspect-ratio:auto}

/* The hero phone shows the member app and cycles through the same screens. With
   placeholders in it cycles placeholders, so the motion is right before the artwork
   is. theme/hero.js drives it; with no JS, screen one simply stays put. */
.phone-live{overflow:hidden}
.pcycle{position:relative;width:100%;height:100%}
.pscreen{position:absolute;inset:0;opacity:0}
.pscreen[data-i="0"]{opacity:1}
.pscreen img,.pscreen .shotph{width:100%;height:100%;object-fit:cover;border:0;aspect-ratio:auto}
.pscreen .shotph{border:0}

/* ---------- pricing commitments -----------------------------------------
   "What it costs" was 140 words in two paragraphs whose real content was a list
   of promises. Six rows, two columns, each a tick-sized icon and one line. The
   honest "an app may be the wrong answer" paragraph stays underneath as a note,
   because it is the most persuasive thing in the section and shortening it would
   have cost it its point. */
.pledges{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:26px}
.pledge{
  display:grid;grid-template-columns:42px 1fr;gap:14px;align-items:start;text-align:left;
  background:rgba(255,255,255,.03);border:1px solid var(--line);
  border-radius:var(--r);padding:18px 20px;
}
.pledge .pic{
  width:42px;height:42px;border-radius:var(--r-sm);flex:none;display:flex;
  align-items:center;justify-content:center;
  background:rgba(40,215,238,.14);border:1px solid rgba(40,215,238,.26);
}
.pledge .pic svg{width:20px;height:20px;color:var(--cyan)}
.pledge b{display:block;color:#fff;font-size:1rem;margin-bottom:4px}
.pledge span{display:block;color:var(--slate);font-size:.88rem}
@media(max-width:760px){.pledges{grid-template-columns:1fr}}

/* ---------- CTA --------------------------------------------------------- */
.cta{
  position:relative;border-radius:var(--r-xl);overflow:hidden;border:1px solid var(--line2);
  background:linear-gradient(140deg,rgba(20,40,72,.8),rgba(10,18,38,.8));
  padding:34px 32px;text-align:center;backdrop-filter:blur(8px);
}
.cta .glow{position:absolute;width:600px;height:300px;left:50%;transform:translateX(-50%);bottom:-160px;border-radius:50%;filter:blur(90px);background:radial-gradient(circle,rgba(40,215,238,.4),transparent 60%);pointer-events:none}
.cta > *{position:relative}
.cta h2{font-size:clamp(1.8rem,3.6vw,3rem)}
.cta p{color:var(--slate);max-width:520px;margin:10px auto 20px}
.cta .actions{display:flex;gap:14px;flex-wrap:wrap;justify-content:center}
@media(max-width:620px){.cta{padding:26px 20px}}

/* ---------- footer ------------------------------------------------------ */
footer{position:relative;z-index:3;border-top:1px solid var(--line);margin-top:28px;padding:28px 0 18px}
.fcols{display:grid;grid-template-columns:1.7fr 1fr 1fr 1fr 1fr;gap:32px}
.fcols h4{color:#fff;font-family:var(--body);font-size:.78rem;letter-spacing:1px;text-transform:uppercase;margin-bottom:14px}
.fcols a{display:block;font-size:.88rem;padding:5px 0;color:var(--slate)}
.fcols a:hover{color:#fff}
.fbrand p{font-size:.88rem;margin-top:12px;max-width:250px;color:var(--slate)}
.fbrand .vat{margin-top:10px;font-size:.8rem}
.fbot{border-top:1px solid var(--line);margin-top:38px;padding-top:22px;display:flex;justify-content:space-between;font-size:.82rem;color:var(--slate);flex-wrap:wrap;gap:8px}
@media(max-width:820px){.fcols{grid-template-columns:1fr 1fr}footer{margin-top:24px}}
@media(max-width:520px){.fcols{grid-template-columns:1fr}}

/* ---------- back to top (NEW) ------------------------------------------ */
.totop{
  position:fixed;right:20px;bottom:20px;z-index:50;width:46px;height:46px;border-radius:var(--r);
  background:rgba(15,24,44,.85);border:1px solid var(--line2);backdrop-filter:blur(10px);
  color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;
  opacity:0;visibility:hidden;transform:translateY(8px);transition:.22s;
}
.totop.show{opacity:1;visibility:visible;transform:none}
.totop:hover{background:rgba(40,215,238,.2);border-color:var(--cyan)}
.totop svg{width:19px;height:19px}

/* ---------- scroll reveal ----------------------------------------------
   EVERY HIDDEN START STATE ON THIS SITE IS GATED BEHIND html.jsm, and that is
   the single most important rule in this file. The class is added by the inline
   boot script in the document head and removed again by its own watchdog if the
   motion layer never reports ready. So the page's resting state, the one you get
   with JavaScript off, with a vendor file 404ing, with a script blocked by an
   extension or with a parse error anywhere, is FULLY VISIBLE CONTENT.

   It used to be the other way round: `.reveal{opacity:0}` unconditionally, with
   nothing but JavaScript to undo it. Twenty elements on the homepage, which was
   effectively the whole page below the hero, depended on a script completing.
   See the long note on MOTION_BOOT in build2.py. */
.reveal{transition:opacity .6s ease,transform .6s ease}
html.jsm .reveal{opacity:0;transform:translateY(18px)}
html.jsm .reveal.in{opacity:1;transform:none}

/* ---------- Stage 1A: the declarative reveal API ------------------------
   One opt-in attribute per element, and the start state lives here rather than
   in JavaScript so there is no frame where the element is visible before being
   hidden. theme/motion.js reads the same attribute to pick the matching tween.
   Durations, distances, easings and staggers are all defined once, at the top of
   that file, and referenced by name.

   data-reveal           the default rise
   data-reveal="rise"    TRANSFORM ONLY, no opacity, for text that must paint
                         immediately. The hero lead is the mobile LCP element and
                         the h1 is the desktop one, so fading either would defer
                         Largest Contentful Paint by exactly the length of the
                         fade. Rising without fading costs LCP nothing.
   data-reveal="mask"    a clip-path wipe, for section headings
   data-reveal="lines"   per-line masked rise, the hero headline only
   data-reveal="depth"   opacity, rise and a slight scale, for cards
   data-reveal="calm"    a very small rise, where the section should stay quiet
   data-stagger          marks a CONTAINER whose revealable children arrive as one
                         sequence. The container is never itself hidden, so a
                         container that also carries .reveal for the no-GSAP
                         fallback is explicitly put back to visible here. */
/* No transition:none here, deliberately. A CSS transition on transform or opacity
   WOULD fight the tween, because GSAP writes an inline value every frame and the
   transition then eases towards each one, which smears the movement. But killing
   it in the stylesheet kills it for ever, and several of these elements want their
   transition back the moment they have arrived: .sv and .plan both ease a hover
   lift, and with it suppressed they snapped. theme/motion.js therefore sets
   transition:none inline for the LIFETIME OF THE TWEEN and clears it on complete,
   which hands the element back to this stylesheet. */
html.jsm [data-reveal]{opacity:0}
/* The .in class is theme/app.js's IntersectionObserver fallback, used when GSAP
   is absent. It has to be able to undo every start state above, including the
   clip and the hero's line offsets, or the fallback reveals some elements and
   leaves others behind. The transition is restored here because in this path
   there is no tween to fight with: CSS is doing the animating. */
html.jsm [data-reveal].in{
  opacity:1;transform:none;clip-path:none;
  transition:opacity .6s ease,transform .6s ease
}
html.jsm .hero h1[data-reveal].in .hmask > *{transform:none;transition:transform .6s ease}
html.jsm [data-reveal="rise"]{opacity:1;transform:translateY(32px)}
html.jsm [data-reveal="mask"]{opacity:1;clip-path:inset(0 100% 0 0)}
html.jsm [data-reveal="lines"]{opacity:1}
html.jsm [data-stagger]{opacity:1;transform:none;clip-path:none;transition:none}

/* The hero headline's line masks. overflow:hidden on a line box clips descenders,
   and this h1 runs line-height 1.05, so the mask box is extended downward by a
   little over a tenth of an em and the extension is then removed from the layout
   with a matching negative margin. Without that pair the tail of the "y" in "your"
   is cut off for the whole animation and stays cut off in the finished state. */
   The display:block pair is NOT gated, on purpose: it keeps the h1's box tree
   identical whether the motion layer is running or not. .tw-wrap is itself a
   block, and leaving it inside an inline wrapper would have the browser generate
   anonymous block boxes around it, which is a different layout arrived at by a
   different route. Only the clipping is conditional. */
.hero h1 .hmask{display:block}
.hero h1 .hmask > *{display:block}
html.jsm .hero h1 .hmask{overflow:hidden;padding-bottom:.14em;margin-bottom:-.14em}
html.jsm .hero h1 .hmask > *{transform:translateY(110%)}

/* ======================================================================
   SURFACES
   ----------------------------------------------------------------------
   The single biggest lever on how expensive this site looks, because it is
   a card-heavy layout: three pillars, four service cards, three plans,
   three why-cards, two area panels and the closing call to action. They
   were flat panels with a one-pixel border, which is a 2021 surface.

   Two things change, and both are the dominant premium signal in 2026 dark
   B2B design: a light that follows the pointer across the card, and an edge
   that lights up where the pointer is nearest. Together they make a flat
   rectangle read as a physical surface catching light, which is the whole
   difference between "a div" and "a product".

   HOW IT IS DONE, AND WHY IT COSTS ALMOST NOTHING. Two custom properties,
   --mx and --my, hold the pointer position as a percentage within the card.
   One delegated pointermove per card writes them; everything else is CSS
   painting a radial gradient at that point. No per-frame JavaScript, no
   layout, no new dependency, and it degrades to the existing card exactly
   as it is when the properties are unset.

   @property is what makes the two gradients animate smoothly rather than
   jumping, and it is also why the fallback is clean: a browser without it
   simply gets the static card. */
/* inherits:true IS LOAD-BEARING AND inherits:false SILENTLY BREAKS THIS. The
   properties are written by JavaScript onto the CARD, but they are read by the
   card's ::before and ::after. A registered property declared non-inheriting
   resolves to its initial value on every descendant, pseudo-elements included,
   so the card knew where the pointer was and the light did not: --spot was 1 on
   the element and 0 on the thing whose opacity it controls. No error, no
   warning, just an effect that never appeared. */
@property --mx { syntax: '<percentage>'; initial-value: 50%; inherits: true }
@property --my { syntax: '<percentage>'; initial-value: 50%; inherits: true }
@property --spot { syntax: '<number>'; initial-value: 0; inherits: true }

html.jsm [data-surface]{
  position:relative;
  isolation:isolate;
  transition:transform .4s var(--ease-soft),box-shadow .4s var(--ease-soft),
             border-color .4s var(--ease-soft);
}

/* The light ON the card. Sits above the card's own background and below its
   content, clipped to the card's radius, and scaled by --spot so it fades in
   and out rather than snapping on at the edge of the card. */
html.jsm [data-surface]::after{
  content:"";position:absolute;inset:0;border-radius:inherit;z-index:-1;
  pointer-events:none;opacity:var(--spot);
  transition:opacity .45s var(--ease-soft);
  background:radial-gradient(420px circle at var(--mx) var(--my),
    rgba(40,215,238,.16), rgba(47,123,246,.09) 38%, transparent 68%);
}
/* THE CONTENT IS NOT TOUCHED, and the first version of this broke the page.
   It used `[data-surface] > * { position:relative; z-index:1 }` to lift content
   above the wash, which also overrode position:absolute on the decorative .glow
   div inside every pillar tile. A 280px blurred circle stopped being a free
   layer and became a real flex item: a huge empty block above the text, the
   card stretched to twice its height, and the copy squashed into what was left.

   Negative z-index on the pseudo-elements does the same job without reaching
   into the markup at all. Inside the isolated stacking context the card already
   creates, a negative layer paints above the card's own background and below
   every child, which is exactly where a light on a surface belongs. Nothing
   about any child's position, layout or stacking is altered. */

/* The EDGE. A gradient border is not a border property, it is a second layer
   masked to a one-pixel ring: border-image cannot do rounded corners and an
   outline cannot be a gradient. This is the standard two-mask composite, and
   it is the detail that separates the look from a plain glow. */
html.jsm [data-surface]::before{
  content:"";position:absolute;inset:0;border-radius:inherit;padding:1px;
  z-index:-1;pointer-events:none;opacity:var(--spot);
  transition:opacity .45s var(--ease-soft);
  background:radial-gradient(260px circle at var(--mx) var(--my),
    rgba(40,215,238,.85), rgba(124,108,255,.45) 42%, transparent 70%);
  -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;
}

/* The lift is small on purpose. The light is doing the work; a card that also
   jumps four pixels reads as a button rather than as a surface. */
@media(hover:hover) and (pointer:fine){
  html.jsm [data-surface]:hover{
    transform:translateY(-2px);
    border-color:var(--line2);
    box-shadow:var(--e-2);
  }
}

/* A sweep across the primary actions. One pass, on entry only, and it does not
   repeat: a button that shines continuously is a casino, a button that catches
   the light once as your pointer arrives is a finish. */
html.jsm .btn-g{position:relative;overflow:hidden;isolation:isolate}
html.jsm .btn-g::after{
  content:"";position:absolute;top:0;bottom:0;width:38%;z-index:0;
  left:-45%;pointer-events:none;
  background:linear-gradient(100deg,transparent,rgba(255,255,255,.42),transparent);
  transform:skewX(-18deg);
}
@media(hover:hover) and (pointer:fine){
  html.jsm .btn-g:hover::after{animation:btn-sheen .72s var(--ease-soft) 1}
}
@keyframes btn-sheen{to{left:118%}}

/* Headings arrive word by word, out of a blur. The blur is the part that makes
   it read as focusing rather than as sliding: four pixels, cleared over the
   same ease as the movement. Set here rather than in JavaScript so there is no
   frame where a split heading is visible unstyled. */
html.jsm .wsplit{display:inline-block;will-change:transform,opacity,filter}

/* ======================================================================
   FITARA: the pinned product story
   ----------------------------------------------------------------------
   The device stays beside the copy while the reader moves through the
   features, and the screen inside it changes to match whichever feature
   they are on. Member phone first, then the trainer desk arrives alongside
   it, then both together.

   EVERY RULE HERE IS GATED THREE TIMES: html.jsm, the data-pin attribute,
   and a 981px floor. Below that width, under reduced motion, or with no
   JavaScript, none of it applies and the section renders exactly as it did
   before: full width, centred heading, the .shots grid of real captures and
   the cards underneath. That is not a degraded version, it is the page as
   it shipped, which is why there is no risk to mobile in any of this.
   ====================================================================== */
/* 1100px, not 981. The two-column split only earns its place when BOTH columns
   are comfortable: at 1024 the copy column fell under 400px, which is about
   350px of text once a card's padding is taken off, and cramping the body copy
   to make room for a picture is a bad trade. Below this the section is the
   stacked page, which is a perfectly good layout and the one it shipped with. */
/* THE STAGE IS ONLY EVER VISIBLE WHEN IT IS BEING POSITIONED. Every rule that gives
   it a size, a position or a device shape lives in the min-width:1100px block below
   and is gated on html.jsm, so outside that it was a plain block: the phone and the
   console screens laid out at full column width, one under another. Measured at 900px
   before this rule existed, Orbit's phone stack alone was 837x6580, sitting directly
   under the .shots grid that already shows the same screens properly. It is decorative
   and aria-hidden, and the grids carry the content, so it does not render unless the
   stage is live. Affected both product pages from 8 October. */
.pinstage{display:none}

@media(min-width:1100px){
  /* overflow-x:clip, NOT overflow:hidden, and the distinction is the whole
     reason the pin still works. The desk panel starts off-stage to the right so
     it can arrive from the opposite side to the phone, and off-stage means past
     the column edge: measured at 46px of horizontal document overflow at 1440
     and 58px at 1100, which is a sideways scrollbar on the whole page.
     overflow:hidden would fix it and would also silently kill position:sticky,
     because an overflow ancestor becomes the sticky element's scroll container.
     overflow-x:clip creates no scroll container, so it trims the bleed and
     leaves the stage stuck where it should be. */
  html.jsm .pinwrap[data-pin]{overflow-x:clip}
  html.jsm .pinwrap[data-pin] .pingrid{
    /* THE DEVICE IS THE SUBJECT, SO IT GETS THE ROOM. The first cut gave the
       copy a 1fr column and the stage a 480px one, which at 1440 is 830px of
       large bordered cards against a 210px phone: the cards dominated and the
       thing the section is actually about was a thumbnail beside them. The copy
       is now the narrower column and the stage the wider one. */
    display:grid;grid-template-columns:minmax(0,0.88fr) minmax(0,1.12fr);
    gap:48px;align-items:start;
  }
  /* The grid of captures is what the stage replaces, so it would be the same
     seven images twice. It stays in the markup for every other context. */
  html.jsm .pinwrap[data-pin] .shots{display:none}
  /* In a column the centred heading reads as misaligned against the cards
     below it, which are left-aligned text. Only inside the pinned layout. */
  html.jsm .pinwrap[data-pin] .shead,
  html.jsm .pinwrap[data-pin] .shead.c{text-align:left;margin-left:0}
  html.jsm .pinwrap[data-pin] .cards{grid-template-columns:1fr;gap:16px}
  html.jsm .pinwrap[data-pin] section{padding:0}
  /* Gap between the two halves of a pinned story. Was #trainer, which is
     Fitara's id, so Orbit's #admin inherited none of it and the two sections
     ran together. Generic now: whichever section follows the first one. */
  html.jsm .pinwrap[data-pin] .pinflow section + section{margin-top:90px}

  /* Sticky rather than a ScrollTrigger pin. It is one line, it cannot get the
     page height wrong, it needs no spacer element, and it cannot strand the
     reader if the script fails: without JavaScript the stage simply sticks and
     shows its first screen. ScrollTrigger then drives only what is INSIDE it. */
  html.jsm .pinwrap[data-pin] .pinstage{
    display:block;position:sticky;top:104px;height:min(80vh,720px);
  }
  html.jsm .pinwrap[data-pin] .pinstage-in{
    position:relative;width:100%;height:100%;
    display:flex;align-items:center;justify-content:center;
    perspective:1400px;
  }

  /* The two devices occupy the same space and are moved apart by the timeline,
     so neither reserves room the other could use. */
  html.jsm .pdev{position:absolute;will-change:transform,opacity}

  html.jsm .pdev-phone{
    width:290px;aspect-ratio:520/1125;border-radius:var(--r-device);
    border:1px solid var(--line2);background:#0b1424;overflow:hidden;
    box-shadow:var(--e-3);z-index:2;
  }
  html.jsm .pdev-desk{
    width:600px;aspect-ratio:16/10;border-radius:var(--r-sm);
    border:1px solid var(--line2);background:#0b1424;overflow:hidden;
    box-shadow:var(--e-3);z-index:1;opacity:0;
  }
  html.jsm .pdev-chrome{
    height:28px;display:flex;align-items:center;gap:6px;padding:0 10px;
    background:rgba(255,255,255,.05);border-bottom:1px solid var(--line);
  }
  html.jsm .pdev-chrome i{width:7px;height:7px;border-radius:50%;background:var(--line2)}

  html.jsm .pdev-screens{position:relative;width:100%;height:100%}
  html.jsm .pdev-desk .pdev-screens{height:calc(100% - 28px)}
  html.jsm .pdev .pscr{position:absolute;inset:0;opacity:0}
  html.jsm .pdev .pscr:first-child{opacity:1}
  html.jsm .pdev .pscr img{width:100%;height:100%;object-fit:cover;display:block}
  html.jsm .pdev .pscr .shotph{width:100%;height:100%;aspect-ratio:auto}
  /* The phone deliberately overlaps the left of the desk panel, which is the
     layered composition the reference shows. While the desk is a placeholder,
     that overlap sits straight on top of its centred caption, so the caption
     moves into the part of the panel the phone does not cover. A real capture
     needs none of this and is unaffected by it. */
  html.jsm .pdev-desk .shotph{align-items:flex-end;padding-right:22px;text-align:right}

  /* The card the reader is on is the one the device is showing, so it says so.
     Without this the stage changes for no visible reason. */
  html.jsm .pinwrap[data-pin] .pinsteps .card{
    transition:opacity .45s var(--ease-soft),border-color .45s var(--ease-soft);
  }
  /* .58 rather than .42. The handover requires the feature copy to stay
     readable, and three of four cards are inactive at any moment: dimmed far
     enough to say "not this one" and no further. */
  html.jsm .pinwrap[data-pin] .pinsteps .card:not(.on){opacity:.58}
  html.jsm .pinwrap[data-pin] .pinsteps .card.on{opacity:1;border-color:var(--line2)}
}

/* ======================================================================
   Stage 1A interaction states
   ----------------------------------------------------------------------
   Everything here is gated behind html.jsm, so none of it is part of the
   page's resting appearance, and every one of these effects degrades to the
   existing design rather than to a broken one.
   ====================================================================== */

/* TWO SYSTEMS WERE WRITING transform TO THE SAME ELEMENTS. .tile and .card carry
   `transition:.25s`, which is every animatable property, plus a :hover rule that
   sets a translateY. theme/motion.js drives rotationX, rotationY and y on the
   same elements through GSAP. GSAP writes an inline transform, so the :hover rule
   never applied on a pointer device, and worse, the blanket transition then
   re-eased GSAP's per-frame writes: the tilt arrived through a second 250ms ease
   it did not ask for, which is what made it feel smeared rather than attached to
   the cursor.

   Narrowing the transition on tilt targets hands transform to GSAP alone. Only
   under a fine pointer, which is the only place GSAP binds the tilt, so a touch
   device keeps the plain CSS hover exactly as it is. */
@media(hover:hover) and (pointer:fine){
  html.jsm [data-tilt]{transition:border-color .25s ease,box-shadow .25s ease}
}

/* .sv carries BOTH its own class and .reveal, and .reveal's 0.6s transition is
   later in this file, so it wins on source order and the service cards' hover
   lift would ease over 600ms: sluggish, and not what the .sv rule asks for. The
   reveal transition is only ever needed by theme/app.js's no-GSAP fallback, so
   once an element has arrived it should be back on its own timing. This states
   that for the one element where the two rules actually disagree about something
   a visitor can feel. */
@media(hover:hover) and (pointer:fine){
  html.jsm [data-reveal].sv{transition:transform .2s ease,border-color .2s ease}
}

/* The marquee's nearest item to the pointer. Emphasis only: colour and the dot's
   existing glow token. No scale and no movement, because ten items that each jump
   as the pointer passes is the "animation demo" failure, and the strip is already
   moving on its own. */
html.jsm .marq span{transition:color .25s ease}
html.jsm .marq span .d{transition:box-shadow .25s ease,background .25s ease}
html.jsm .marq span.near{color:#fff}
html.jsm .marq span.near .d{box-shadow:var(--glow-accent)}

/* Service card icon micro-animations. CSS rather than GSAP on purpose: these are
   hover-only, they reverse for free, and they cost nothing until hovered. One
   keyframe set per card, keyed off data-micro, and all four run inside the 46px
   icon tile rather than moving the card. */
html.jsm .sv .ic{position:relative;overflow:hidden}
html.jsm .sv .ic::after{content:"";position:absolute;pointer-events:none;opacity:0}
/* CCTV: a scan line crosses the tile once */
html.jsm .sv[data-micro="cctv"] .ic::after{
  left:0;right:0;top:0;height:2px;opacity:0;
  background:linear-gradient(90deg,transparent,var(--cyan),transparent);
}
html.jsm .sv[data-micro="cctv"]:hover .ic::after{animation:mi-scan 1.5s ease-in-out infinite}
@keyframes mi-scan{0%{opacity:0;transform:translateY(2px)}15%{opacity:.9}85%{opacity:.9}100%{opacity:0;transform:translateY(42px)}}
/* Automation: a node travels the path, suggesting one step handing to the next */
html.jsm .sv[data-micro="auto"] .ic::after{
  width:5px;height:5px;border-radius:50%;background:var(--cyan);left:6px;top:50%;
  box-shadow:var(--glow-accent);
}
html.jsm .sv[data-micro="auto"]:hover .ic::after{animation:mi-node 1.6s ease-in-out infinite}
@keyframes mi-node{0%{opacity:0;transform:translate(0,-50%)}20%{opacity:1}80%{opacity:1}100%{opacity:0;transform:translate(32px,-50%)}}
/* Network: a connection pulse expanding from the centre */
html.jsm .sv[data-micro="net"] .ic::after{
  left:50%;top:50%;width:10px;height:10px;margin:-5px 0 0 -5px;border-radius:50%;
  border:1px solid var(--cyan);
}
html.jsm .sv[data-micro="net"]:hover .ic::after{animation:mi-pulse 1.8s ease-out infinite}
@keyframes mi-pulse{0%{opacity:.8;transform:scale(.4)}100%{opacity:0;transform:scale(3.4)}}
/* PC: a power/status light settling */
html.jsm .sv[data-micro="pc"] .ic::after{
  right:5px;bottom:5px;width:5px;height:5px;border-radius:50%;background:var(--lime);
}
html.jsm .sv[data-micro="pc"]:hover .ic::after{animation:mi-status 1.4s steps(1,end) infinite}
@keyframes mi-status{0%,45%{opacity:.25}50%,100%{opacity:1}}

/* The four step flow becomes a progression rather than four blocks. .fnode::after
   is the existing connector and stays exactly as it is, as the unlit track; the
   fill is a second pseudo-element over the same geometry, scaled from 0 to 1 by
   --fp, which theme/motion.js advances as each step is reached. The numbers start
   dimmed and light as their step activates.

   Both only under html.jsm, so with no JavaScript every step is already lit and
   the connectors are already drawn, which is today's appearance. */
html.jsm .flow .fnode::before{
  content:"";position:absolute;top:25px;left:calc(50% + 36px);right:calc(-50% + 36px);height:2px;
  background:linear-gradient(90deg,var(--cyan),rgba(90,123,255,.5));
  transform:scaleX(var(--fp,0));transform-origin:left center;
}
html.jsm .flow .fnode:last-child::before{display:none}
html.jsm .flow .fnum{
  filter:saturate(.25) brightness(.7);opacity:.75;
  transition:filter .5s ease,opacity .5s ease,box-shadow .5s ease;
}
html.jsm .flow .fnode.lit .fnum{filter:none;opacity:1;box-shadow:0 0 0 7px rgba(40,215,238,.12)}
@media(max-width:820px){
  html.jsm .flow .fnode::before{
    top:56px;bottom:4px;left:25px;right:auto;width:2px;height:auto;
    background:linear-gradient(180deg,var(--cyan),rgba(90,123,255,.4));
    transform:scaleY(var(--fp,0));transform-origin:top center;
  }
}

/* The header compacts on the way down and returns on the way up.

   THE MARGIN IS NOT DECORATION, IT IS WHAT MAKES THIS FREE. nav is sticky, so it
   reserves its height in normal flow and every section below sits on top of that
   reservation. Shrinking the bar by 10px would therefore pull the whole page up
   by 10px, which is a layout shift on first scroll and would land in CLS. The
   10px comes off the padding and goes straight back on as margin, so the space
   reserved in flow is unchanged to the pixel and only the painted bar is shorter.
   Verified by measurement, not by eye: see the Stage 1A notes. */
html.jsm nav{transition:background .25s,border-color .25s,margin-bottom .28s ease}
html.jsm nav .nav-in{transition:padding .28s ease}
html.jsm nav.compact .nav-in{padding-top:10px;padding-bottom:10px}
html.jsm nav.compact{margin-bottom:10px}
html.jsm nav.past-hero{background:rgba(8,13,26,.94);border-bottom-color:var(--line2);box-shadow:0 1px 0 rgba(255,255,255,.04),0 12px 28px rgba(0,0,0,.28)}
@media(max-width:1100px){
  html.jsm nav.compact .nav-in{padding-top:8px;padding-bottom:8px}
  html.jsm nav.compact{margin-bottom:8px}
}

/* The closing call to action's existing glow drifts, slowly. Nothing new on the
   page, and the blanket animation:none in the reduced-motion block below stops it. */
html.jsm [data-drift]{animation:cta-drift 16s ease-in-out infinite alternate}
@keyframes cta-drift{from{transform:translate(0,0) scale(1)}to{transform:translate(-70px,38px) scale(1.12)}}

/* ---------- reduced motion ---------------------------------------------
   D's own guard, kept verbatim in spirit and widened to cover the reveal and
   the smooth scroll. theme/bg.js checks matchMedia itself and draws a single
   canvas frame before cancelling, which is D's behaviour too.

   The boot script already refuses to add html.jsm under this preference, so the
   start states above never apply in the first place. These rules are the belt to
   that braces: they win on specificity if the class is ever present anyway. */
@media(prefers-reduced-motion:reduce){
  *{animation:none!important;transition:none!important}
  html{scroll-behavior:auto}
  .reveal{opacity:1;transform:none}
  html.jsm .reveal,html.jsm [data-reveal],html.jsm [data-stagger]{
    opacity:1;transform:none;clip-path:none
  }
  html.jsm .hero h1 .hmask > *{transform:none}
  html.jsm .flow .fnode::before{transform:none}
  html.jsm .flow .fnum{filter:none;opacity:1}
  html.jsm [data-surface]::before,html.jsm [data-surface]::after{display:none}
  html.jsm .btn-g::after{display:none}
  html.jsm .wsplit{transform:none;opacity:1;filter:none}
}

/* ---------- print ------------------------------------------------------- */
@media print{
  .bg,#net,nav,.mobnav,.totop,.marq-wrap,footer .fcols{display:none}
  body{background:#fff;color:#000}
  h1,h2,h3,h4{color:#000}
  .wrap{max-width:none;padding:0}
}
