/* SCALUS — own-brand concept system (sc.css)
   Black iOS-26 glass · Scalus green #32cf75 · orange #FF4D00 accent */
:root{
  --bg:#070808; --bg-2:#0c0e0d; --panel:rgba(var(--wa),.045);
  --line:rgba(var(--wa),.09); --line-2:rgba(var(--wa),.16);
  --ink:#f5f7f6; --ink-soft:#b8c0bc; --ink-mute:#79827d;
  --green:#32cf75; --green-soft:rgba(50,207,117,.14); --orange:#FF4D00;
  --wa:255,255,255; --logo-swap:none;
  --glass-hi:rgba(255,255,255,.26); --glass-sheen:rgba(255,255,255,.12);
  --r:22px; --r-sm:14px; --blur:blur(26px) saturate(1.4);
  /* The small chip material. Everything that floats over content, a tag on a
     shot, a control on a video, a pill on a mock screen, used to be a bare
     blur(8px): a frosted grey rectangle, the thing the iOS 26 material exists
     to replace. Saturation and a brightness lift keep the colour underneath
     alive through the blur, and --rim-sm gives the edge a light source rather
     than an even outline. Built from --glass-hi and --glass-sheen, never from
     --wa, because a specular highlight is white in both themes while --wa
     flips to the dark ink in light mode. */
  --glass-sm:blur(14px) saturate(200%) brightness(1.14);
  --rim-sm:inset 0 1px 0 var(--glass-hi),
    inset 1px 1px 1.5px -.5px var(--glass-hi),
    inset -1px -1px 1.5px -.5px var(--glass-sheen);
  --font:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
}

/* ===========================================================================
   CONCENTRIC CORNERS  (house standard, Tim 11 Sep 2026)
   SDT reference: @guhmp4 "Improve your UI/UX" day 3.

   When one rounded box sits inside another, the two corners must share a
   centre point so their curves stay parallel. That is one equation:

       inner radius  =  outer radius  −  the gap between them

   where the gap is the parent's padding PLUS its border width, because both
   radii are drawn on their own border box.

   Get it wrong in either direction and it reads as sloppy without anyone
   being able to say why. Inner too small and the child looks square sitting
   in a round pocket. Inner too big and its curve crowds the parent's, so the
   ring of space between them pinches at the corner and swells along the edge.

   Three ways to satisfy it, all legitimate:

     1. Derive the child. Keep the parent's radius and padding, write the
        child as calc(). This is the default and it is what most of the fixes
        below do, because it self maintains: change the padding token and
        every nested radius follows.

     2. Derive the parent. When the child's radius is the fixed thing (a phone
        mockup has to look like a phone), set outer = inner + gap instead.

     3. Clear the curve. A child inset FURTHER than the parent's radius sits
        against two straight edges, so there is no curve to be concentric with
        and it is free to be shaped however it likes. Floating overlay tiles
        live here. Nudging the padding out past the radius is a real fix, not
        a dodge.

   Exempt: capsules (border-radius:999px) and circles. They are shapes in
   their own right and pinning them to a parent would just un-round them.

   MEASURE IT, never eyeball it:  node ~/bin/concentric-check.js <url>
   It exits non zero on a violation, so it gates a deploy the same way sra does.
   =========================================================================== */
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
/* The nav is position:fixed at top:14px and about 60px tall, so before this an
   in-page jump (#book, #price, #faq) parked the target's heading underneath it
   — you landed on a section whose title was hidden behind the bar. */
[id]{scroll-margin-top:100px}
body{background:var(--bg);color:var(--ink);font-family:var(--font);
  -webkit-font-smoothing:antialiased;line-height:1.6}
img{max-width:100%;height:auto}
a{color:var(--green)}
::selection{background:var(--green);color:#04130a}

/* ===========================================================================
   MOBILE APP-LOCK  (standard, Tim 25 Jul 2026 · zoom-out lock added 10 Aug 2026)
   On a phone the site must feel locked like an app: it can never drift or pan
   sideways, and it can never sit zoomed out with everything tiny. Three layers:
     1) clip stray horizontal overflow WITHOUT creating a scroll container
        (overflow-x:clip, not hidden, so the fixed/sticky nav still works);
     2) on touch, allow vertical scroll + pinch-zoom only, never a sideways drag;
     3) the viewport meta on every page carries minimum-scale=1, so the browser
        can't shrink the page to fit a stray wide element (zoom IN still works,
        which keeps it accessible).
   The real guarantee is still that no element is wider than the viewport — run
   `node ~/bin/mobile-overflow-check.js <url>` over every page before sign-off.
   Anything MEANT to scroll sideways (carousel, wide table, code block) opts
   back in with .scroll-x / [data-scroll-x]. .marquee already clips its own track.
   =========================================================================== */
html{overflow-x:clip}
body{overflow-x:clip;overscroll-behavior-x:none}
.scroll-x{overflow-x:auto;-webkit-overflow-scrolling:touch}
@media(max-width:1024px){
  html,body{touch-action:pan-y pinch-zoom}
  [data-scroll-x],.scroll-x,.carousel,.slider,.overflow-x-auto,pre,.table-wrap,.code{
    touch-action:pan-x pan-y pinch-zoom}
  /* never let a stray full-width media element cause a sliver of overflow */
  img,video,iframe,table,pre{max-width:100%}
  /* iOS Safari force-zooms into any focused field under 16px. With the page
     locked to minimum-scale 1 that reads as the layout jumping, so keep every
     input at 16px on touch sizes. */
  .form input,.form select,.form textarea,input,select,textarea{font-size:16px}
}
/* A grid/flex child defaults to min-width:auto, so one nowrap phrase deep inside
   can push the whole track wider than the screen (this is what made the
   onboarding form zoom out to 526px on a 390px phone). Opt every form child out. */
.form>*{min-width:0}

.wrap{max-width:1140px;margin:0 auto;padding:0 22px}
.wrap-n{max-width:880px;margin:0 auto;padding:0 22px}
h1,h2,h3{font-weight:800;letter-spacing:-.02em;line-height:1.08}
h1{font-size:clamp(2.5rem,6vw,4.4rem)}
h2{font-size:clamp(1.7rem,3.6vw,2.6rem)}
h3{font-size:1.18rem;font-weight:700}
.lead{color:var(--ink-soft);font-size:clamp(1rem,1.6vw,1.2rem);line-height:1.65}
.green{color:var(--green)}
/* The one word the whole offer hangs on. A slow colour drift through the brand
   green and back, clipped to the letterforms — the headline is the first thing
   on the page and this is the only motion in it, so it stays slow enough to
   read as a sheen rather than a flashing sign. */
/* Stays inside the brand's green-to-mint neighbourhood. It used to run the full
   rainbow through purple, pink and amber, which put six colours in the first
   line anyone reads (Tim, 25 Aug 2026). One colour, moving. */
.h1-flow{background-image:linear-gradient(100deg,#32cf75,#7df0ae,#b6f7d3,#5ce3ab,#32cf75);
  background-size:320% 100%;background-position:0 0;
  -webkit-background-clip:text;background-clip:text;
  color:transparent;-webkit-text-fill-color:transparent;
  animation:h1flow 11s linear infinite}
@keyframes h1flow{to{background-position:320% 0}}
@media(prefers-reduced-motion:reduce){.h1-flow{animation:none}}

/* .grad-flow — the H1's animated colour, laid out in normal flow.
   Two layers clipped to the same letterforms: the colour drifting underneath,
   a narrow white glint travelling across on top, so the shine happens INSIDE
   the type. Pair it with .line on the white half so the heading reads as two
   deliberate lines rather than one that happened to wrap.

   text-wrap:balance on both halves is the "never leave one word on a line" rule
   (Tim, 25 Aug 2026): if a line ever does get narrow enough to wrap, it splits
   evenly instead of dropping a single orphan word underneath.

   .h1-shine is the same treatment on the hero's first word. See below. */
.grad-flow,.h1-shine{
  display:block;text-wrap:balance;
  background-image:
    linear-gradient(100deg,transparent 40%,rgba(var(--wa),.92) 50%,transparent 60%),
    linear-gradient(100deg,#32cf75,#5ad1ff,#a97bff,#ff7ac2,#ffb347,#32cf75);
  background-size:200% 100%,300% 100%;
  background-position:120% 0,0% 0;
  background-repeat:no-repeat;
  -webkit-background-clip:text;background-clip:text;
  color:transparent;-webkit-text-fill-color:transparent;
  filter:drop-shadow(0 2px 18px rgba(90,209,255,.22));
  animation:h1rotShine 9s ease-in-out infinite;
}
.grad-flow + .line{display:block;text-wrap:balance;color:#fff}
@media(prefers-reduced-motion:reduce){
  .grad-flow,.h1-shine{animation:none;background-position:50% 0,0 0}
}

/* .h1-shine — "Websites", the one word in the headline that carries the colour.
   It replaced the rotating adjective on 31 Aug 2026 (Tim): the word kept
   changing, but the EFFECT was the thing anyone actually noticed, so the effect
   stayed and the rotation went. What that buys, beyond one less moving part:

     · the headline is now a fixed string, so it can be read by a crawler, an
       AI answer engine and a screen reader as the sentence it is. A word that
       swaps every 2.6s is none of those things.
     · no reserved slot, no sizer, no resize listener, no two layer handoff.
       That whole apparatus existed only to stop the rotation shoving the rest
       of the headline around.

   One size up from the rest of the H1 because it is the noun the entire page is
   about. line-height is pulled in tight so the bigger word does not open a gap
   between it and the two lines under it. */
.h1-shine{font-size:1.32em;line-height:1.02;margin-bottom:.04em}

/* It arrives WITH the rest of the headline rather than simply being there.
   sc.js splits the plain words of the H1 into .hw spans and staggers them in
   from .08s; this word is deliberately NOT split (splitting it would destroy the
   background box its colour is clipped to — see the guard in sc.js), so it wears
   the same keyframe itself and leads the stagger from 0s.

   Two animations on one element: the entrance runs once, the shine loops behind
   it forever. Scoped to html.js for exactly the reason .hw is — with no script
   there is no stagger to join, and a headline that starts at opacity:0 and waits
   for an animation is a headline that might never appear.

   The start state has to be declared here. The hw keyframe is a `to` block only,
   so its implied `from` is whatever the element already computes to, and without
   these two lines that is opacity:1 and no transform, which animates nothing.
   It fades in place like .hw, and deliberately WITHOUT .hw's blur. This is the
   one word whose colour is a background clipped to its own glyphs, and a filter
   on a background-clip:text element is exactly the combination that has made
   this word paint as nothing before. It is not worth re-testing on the live
   hero for a nine pixel blur nobody will catch. What it must share with .hw is
   the important half: no transform, so it never leaves the line it shares with
   "for". That is the reshuffle fixed on 16 Sep 2026, do not put travel back. */
html.js .h1-shine{opacity:0;
  animation:h1rotShine 9s ease-in-out infinite,hw .9s cubic-bezier(.22,1,.36,1) forwards}
@media(prefers-reduced-motion:reduce){
  html.js .h1-shine{opacity:1;transform:none;filter:none;animation:none;background-position:50% 0,0 0}
}

/* The h1rotShine timeline, kept under its original name because .grad-flow and
   .h1-shine have both used it since the day it was written. One timeline drives
   both layers, because background-position animates as a single list and the two
   layers cannot be given separate clocks.

   The percentages matter more than they look. A background wider than its box
   moves LEFT as the percentage climbs, and at 100% its right edge has already
   reached the right edge of the box, so anything past 100% drags the colour
   clean off the letters and the word paints transparent. An earlier version ran
   the colour to 320% and the word was invisible for two thirds of every cycle.
   The colour now travels 0 to 100 and back, so it is always covering, and the
   highlight (the only layer allowed off screen, because off screen is exactly
   where a glint should wait) crosses in the first third, holds off the edge,
   then jumps back at 99.9% where nothing can see it move. */
@keyframes h1rotShine{
  0%    {background-position:120% 0,0% 0}
  28%   {background-position:-20% 0,56% 0}
  50%   {background-position:-20% 0,100% 0}
  99.9% {background-position:-20% 0,0% 0}
  100%  {background-position:120% 0,0% 0}
}
/* ---------------------------------------------------------------------------
   .glass-phrase — the coloured phrase inside the hero's lead line.

   It used to sit inside a glass pill. Tim pulled the pill on 25 Aug 2026: the
   wording alone was doing the work and the chip was one more bordered box in a
   hero that already has two buttons and a form card. What is left is the type
   itself, a shade bolder than the sentence around it, carrying the same living
   gradient. The outer span keeps the sheen, the inner still owns the colour,
   because background-clip:text on one element cannot do both.

   The .glass-phrase>span rule stays weight 700 rather than inheriting: the lead
   paragraph is 400, and at this size the colour alone does not separate the
   phrase from the sentence. */
.glass-phrase{
  display:inline;
  -webkit-box-decoration-break:clone;box-decoration-break:clone;
  padding:0;margin:0;background:none;box-shadow:none;
  -webkit-backdrop-filter:none;backdrop-filter:none;
}
/* Shinier as of 25 Aug 2026: the phrase was a single colour drifting, which read
   as static at a glance. It now runs the same two layer trick as the H1 word, a
   narrow white highlight travelling across a moving colour bed, both clipped to
   the letterforms so the shine happens inside the type. The highlight crosses in
   the first third of the cycle and then waits off the edge, so it arrives as a
   glint every few seconds instead of scrolling non stop, and the glow behind the
   letters breathes on the same clock. */
.glass-phrase>span{
  font-weight:700;
  background-image:
    linear-gradient(100deg,transparent 42%,rgba(var(--wa),.95) 50%,transparent 58%),
    linear-gradient(100deg,#32cf75,#2fe3b4,#6ee7ff,#7df0ae,#39d98a,#32cf75);
  background-size:200% 100%,300% 100%;
  background-position:120% 0,0% 0;
  background-repeat:no-repeat;
  -webkit-background-clip:text;background-clip:text;
  color:transparent;-webkit-text-fill-color:transparent;
  animation:gp-shine 6s ease-in-out infinite,gp-glow 3.5s ease-in-out infinite;
}
/* Same bounds rule as the H1 word: the colour layer never goes past 100% or it
   slides off the letters and the phrase disappears. */
@keyframes gp-shine{
  0%    {background-position:120% 0,0% 0}
  26%   {background-position:-20% 0,52% 0}
  50%   {background-position:-20% 0,100% 0}
  99.9% {background-position:-20% 0,0% 0}
  100%  {background-position:120% 0,0% 0}
}
@keyframes gp-glow{
  0%,100%{filter:drop-shadow(0 0 8px rgba(50,207,117,.26))}
  50%    {filter:drop-shadow(0 0 16px rgba(110,231,255,.42))}
}
@media(prefers-reduced-motion:reduce){
  .glass-phrase>span{animation:none;background-position:50% 0,50% 0;
    filter:drop-shadow(0 0 8px rgba(50,207,117,.26))}
}

/* ---------------------------------------------------------------------------
   .glass-phrase.pot — "the power of emerging technology", on its own line, ALWAYS
   (Tim, 31 Aug 2026).

   It used to be the tail of the sentence above it, so where it broke was down
   to the viewport: on a wide screen it sat inline, at 390px it wrapped after
   "The Power of" and split the phrase across two lines. display:block takes the
   decision away from the layout — the sentence sets it up, the phrase lands
   underneath it, at every width.

   text-wrap:balance is what stops the block turning into a new orphan problem:
   if the phrase itself ever has to wrap it splits evenly rather than dropping
   "Technology" on a line of its own.

   box-decoration-break goes back to slice here. Clone exists to carry an inline
   background across a line break, and there is no longer a line break to carry
   anything across. */
.glass-phrase.pot{display:block;text-wrap:balance;margin-top:2px;
  -webkit-box-decoration-break:slice;box-decoration-break:slice}

/* The arrow mark that used to sign the phrase came off on 13 Sep 2026 (Tim):
   only the words "change" and "technology" carry the colour now. */

.eyebrow{display:inline-flex;align-items:center;gap:.6em;font-size:1.1rem;font-weight:700;
  letter-spacing:.22em;text-transform:uppercase;color:var(--green)}
.eyebrow::before{content:'' !important;display:none !important}
.eyebrow.line::before{content:none}
/* Glass eyebrow (Tim, 14 Sep 2026): a frosted panel, deliberately NOT a pill.
   Radius 12 against a ~40px chip, so the corners read as a cut rather than a
   capsule. The sheen and the lit top edge come from tokens, because a highlight
   must stay light on a light ground and rgba(var(--wa),a) flips to ink there. */
.eyebrow.glass{
  padding:10px 16px;border-radius:12px;font-size:.95rem;letter-spacing:.16em;
  background:
    linear-gradient(101deg,transparent 20%,var(--glass-sheen) 44%,transparent 68%),
    linear-gradient(168deg,rgba(var(--wa),.07),rgba(var(--wa),.01) 46%),
    rgba(var(--wa),.05);
  border:1px solid rgba(var(--wa),.13);
  backdrop-filter:var(--blur);-webkit-backdrop-filter:var(--blur);
  box-shadow:inset 0 1px 0 var(--glass-hi),inset 0 -1px 0 rgba(0,0,0,.10),0 10px 28px rgba(0,0,0,.12)}
@media(max-width:760px){.eyebrow.glass{padding:8px 13px;font-size:.85rem}}
section{padding:58px 0;position:relative}
@media(max-width:760px){section{padding:38px 0}.sect-head{margin-bottom:24px}}
/* text-wrap:balance on the heading, pretty on the paragraph: no line in a
   section head is ever allowed to end up carrying a single word on its own
   (Tim, 25 Aug 2026). "Tap any one to see how I built it." was dropping "it."
   onto a line by itself on every width. */
.sect-head{max-width:680px;margin:0 auto 34px;text-align:center}
.sect-head h2{text-wrap:balance}
.sect-head p{margin-top:14px;color:var(--ink-soft);text-wrap:pretty}
.lead{text-wrap:pretty}

/* ambient glow */
.glow{position:fixed;inset:0;z-index:-1;pointer-events:none;
  background:
   radial-gradient(900px 540px at 82% -10%, rgba(50,207,117,.13), transparent 60%),
   radial-gradient(760px 480px at -10% 38%, rgba(14,162,222,.08), transparent 60%),
   radial-gradient(680px 460px at 70% 105%, rgba(255,77,0,.07), transparent 60%)}

/* glass panel */
.panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);
  -webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur);
  box-shadow:0 24px 60px rgba(0,0,0,.45), inset 0 1px 0 rgba(var(--wa),.06)}
.panel:hover{border-color:var(--line-2)}

/* nav
   The top bar is the SAME piece of glass as the bottom dock, so the two read as
   one system: the brightness lifted backdrop, the rotating conic rim and the
   travelling specular hotspot are the same four layers documented in the LIQUID
   GLASS DOCK block near the bottom of this file. Read the TRAP note there before
   putting a transform on .nav or .nav .bar — a transformed ancestor silently
   kills backdrop-filter and the glass collapses to a flat translucent strip. */
.nav{position:fixed;top:14px;left:0;right:0;z-index:60}
.nav .bar{position:relative;isolation:isolate;
  max-width:1140px;margin:0 auto;padding:10px 20px;display:flex;align-items:center;gap:18px;
  background:linear-gradient(180deg,rgba(var(--wa),.12),rgba(var(--wa),.04));
  border:1px solid transparent;border-radius:999px;
  -webkit-backdrop-filter:blur(26px) saturate(200%) brightness(1.28);
  backdrop-filter:blur(26px) saturate(200%) brightness(1.28);
  box-shadow:0 22px 48px -16px rgba(0,0,0,.7),0 2px 10px rgba(0,0,0,.35),
    inset 0 1px 0 rgba(var(--wa),.16),inset 0 -1px 0 rgba(var(--wa),.05);
  width:calc(100% - 32px);transition:background .3s}
.nav.scrolled .bar{background:linear-gradient(180deg,rgba(var(--wa),.16),rgba(var(--wa),.06))}

/* the rim — a conic gradient painted on a 1px ring, rotating slowly, so the
   edge catches travelling light the way a moulded glass edge does */
.nav .bar::before{content:'';position:absolute;inset:0;border-radius:inherit;padding:1px;z-index:2;
  pointer-events:none;
  background:conic-gradient(from var(--sc-rim),
    rgba(var(--wa),.6),rgba(var(--wa),.05) 18%,
    rgba(var(--wa),.14) 38%,rgba(var(--wa),.04) 55%,
    rgba(var(--wa),.5) 76%,rgba(var(--wa),.08) 90%,rgba(var(--wa),.6));
  -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;
  animation:scDockRim 16s linear infinite}

/* the specular hotspot. The dock takes its position from the pointer, but the
   bar is a wide strip that the pointer mostly sits outside of, so here the
   highlight travels on its own: a slow sweep across the top edge, in step with
   the rim so the surface is never a static grey strip. */
@property --sc-nav{syntax:'<percentage>';inherits:false;initial-value:14%}
.nav .bar::after{content:'';position:absolute;inset:0;border-radius:inherit;z-index:1;
  pointer-events:none;
  background:radial-gradient(240px 84px at var(--sc-nav) -18%,
    rgba(var(--wa),.2),rgba(var(--wa),.05) 45%,rgba(var(--wa),0) 72%);
  animation:scNavSpec 16s ease-in-out infinite}
@keyframes scNavSpec{0%{--sc-nav:8%}50%{--sc-nav:92%}100%{--sc-nav:8%}}

/* everything in the bar sits above both pseudo layers. Listed one by one rather
   than as `>*` because the mobile menu is absolutely positioned and a blanket
   position:relative would drop it back into the row. */
.nav .bar>a,.nav .bar>button,.nav .bar>img{position:relative;z-index:3}
.nav .links{position:relative;z-index:3}

@media(prefers-reduced-motion:reduce){
  .nav .bar::before,.nav .bar::after{animation:none}
}

.nav img{height:30px;width:auto;flex:0 0 auto;display:block}
/* logo link is a flex box so the img has no inline baseline gap, so it sits dead centre of the bar */
.nav .bar>a:first-child{display:flex;align-items:center;flex:0 0 auto}
/* desktop bar order: logo · links · demo button */
.nav .bar>a:first-child,.nav img{order:0}
.nav .links{order:1;display:flex;gap:22px;margin-left:auto;align-items:center}
.nav .bar>.btn{order:2}
.nav .links a{color:var(--ink-soft);text-decoration:none;font-size:.86rem;font-weight:600}
.nav .links a:hover{color:var(--ink)}
@property --sc-pill-rim{syntax:'<angle>';inherits:false;initial-value:200deg}
/* SEO Check pill — the one nav item that sells something, so it gets a tick and
   its own piece of liquid glass rather than a flat hover colour. Three stacked
   background layers, no solid background-color anywhere, so the bar and the page
   behind actually show through:
     1  a narrow white specular band that sweeps across once per cycle
     2  the multi colour tint, green into teal, blue, violet and pink, kept at
        low alpha so it reads as light bending through glass, not as paint.
        Held at 90deg on purpose: the layer is 300% wide and repeats as it
        drifts, and an angled gradient does not tile seamlessly at that width,
        it leaves a hard vertical seam crossing the pill
     3  a faint white film that gives the glass its body
   backdrop-filter does the rest, blurring and over saturating whatever sits
   behind it. The layer stack lives in --glass so the mobile sheet hover rule can
   put it straight back after the generic nav hover shorthand wipes it.
   (Tim, 11 Sep 2026 — "multi colour gradient but super liquid glass transparent") */
.nav .links a.seochk{
  --glass:
    linear-gradient(105deg,rgba(var(--wa),0) 0%,rgba(var(--wa),.20) 46%,rgba(var(--wa),0) 100%),
    linear-gradient(90deg,
      rgba(46,236,132,.55) 0%,
      rgba(38,224,214,.5) 20%,
      rgba(92,168,255,.52) 40%,
      rgba(178,126,255,.52) 60%,
      rgba(255,132,196,.48) 80%,
      rgba(46,236,132,.55) 100%),
    linear-gradient(180deg,rgba(var(--wa),.07),rgba(var(--wa),.02));
  position:relative;isolation:isolate;
  display:inline-flex;align-items:center;gap:7px;padding:7px 15px;border-radius:999px;
  color:var(--ink);font-weight:700;border:1px solid transparent;
  background-color:transparent;background-image:var(--glass);
  background-size:40% 100%,300% 100%,auto;
  background-repeat:no-repeat,repeat,no-repeat;
  -webkit-backdrop-filter:blur(15px) saturate(220%) brightness(1.08);
  backdrop-filter:blur(15px) saturate(220%) brightness(1.08);
  animation:seochkGlass 11s linear infinite;
  box-shadow:inset 0 1px 0 rgba(var(--wa),.34),inset 0 -1px 0 rgba(var(--wa),.07),
    0 4px 16px rgba(0,0,0,.28),0 0 18px rgba(120,170,255,.28);
  transition:box-shadow .25s,transform .25s}
/* the rim, same mask-composite ring the dock uses, but painted with the full
   spectrum instead of white, and turning at its own pace. On a near black bar
   this is what actually carries the colour, the fill alone reads as grey. */
.nav .links a.seochk::before{content:'';position:absolute;inset:0;border-radius:inherit;padding:1px;z-index:2;
  pointer-events:none;
  background:conic-gradient(from var(--sc-pill-rim),
    rgba(46,236,132,.95),rgba(38,224,214,.9) 18%,rgba(92,168,255,.9) 36%,
    rgba(178,126,255,.9) 54%,rgba(255,132,196,.85) 72%,rgba(var(--wa),.75) 86%,
    rgba(46,236,132,.95));
  -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;
  animation:seochkRim 14s linear infinite}
.nav .links a.seochk:hover{color:var(--ink);transform:translateY(-1px);
  box-shadow:inset 0 1px 0 rgba(var(--wa),.5),inset 0 -1px 0 rgba(var(--wa),.1),
    0 8px 26px rgba(0,0,0,.36),0 0 24px rgba(120,190,255,.26)}
.nav .links a.seochk svg{width:14px;height:14px;flex:0 0 auto;display:block;position:relative;z-index:3;
  filter:drop-shadow(0 0 6px rgba(var(--wa),.5))}
/* the tint drifts a full width while the specular band crosses once */
@keyframes seochkGlass{
  from{background-position:-60% 50%,0% 50%,0 0}
  to{background-position:160% 50%,300% 50%,0 0}}
@keyframes seochkRim{to{--sc-pill-rim:560deg}}
@keyframes seochkFlow{from{background-position:0% 50%}to{background-position:300% 50%}}
/* Call is the brand green with the same slow glow as .btn-green (Tim, 6 Oct
   2026: "make call the same green as the Scalus green, have it glow"). The
   flat #32cf75 in both themes, never the darker light mode --green. */
.nav .links a.navcall{display:inline-flex;align-items:center;gap:7px;padding:7px 15px;border-radius:999px;
  color:#04130a;font-weight:800;border:1px solid transparent;background:#32cf75;
  animation:btnGlow 3.6s ease-in-out infinite;
  transition:background .25s,transform .25s}
.nav .links a.navcall:hover{color:#04130a;background:#3ddc82;transform:translateY(-1px)}
.nav .links a.navcall svg{width:14px;height:14px;flex:0 0 auto;display:block;color:#04130a}
@media(prefers-reduced-motion:reduce){.nav .links a.navcall{animation:none}}
@media(prefers-reduced-motion:reduce){
  .nav .links a.seochk{animation:none;background-position:120% 50%,50% 50%,0 0}
  .nav .links a.seochk::before{animation:none}
}
/* hamburger button — hidden on desktop */
.navtoggle{display:none;width:40px;height:40px;flex:0 0 auto;border:1px solid var(--line);
  border-radius:12px;background:rgba(var(--wa),.04);cursor:pointer;align-items:center;justify-content:center;padding:0}
.navtoggle span{display:block;position:relative;width:18px;height:2px;background:var(--ink);border-radius:2px;transition:.25s}
.navtoggle span::before,.navtoggle span::after{content:"";position:absolute;left:0;width:18px;height:2px;background:var(--ink);border-radius:2px;transition:.25s}
.navtoggle span::before{top:-6px}.navtoggle span::after{top:6px}
.nav.open .navtoggle span{background:transparent}
.nav.open .navtoggle span::before{top:0;transform:rotate(45deg)}
.nav.open .navtoggle span::after{top:0;transform:rotate(-45deg)}

/* nav labels never wrap onto two lines, whatever the width */
.nav .links a,.nav .bar>.btn{white-space:nowrap}
/* ...and the bar CTA never SHRINKS below its label either. It was flex:0 1 auto
   by default, so on a page with a long label (learn: "Book your session") the
   pill was squeezed to 120px against a 145px label and the text ran out through
   both rounded ends. Nothing under 1240px needs it to shrink: the media queries
   below already close the gaps and then hide the pill outright. */
.nav .bar>.btn{flex:0 0 auto}
/* between the hamburger point and 1400px the nav is tight, more so on pages with
   a CTA pill in the bar (learn-ads). Close the gaps first, drop the bar CTA under
   1240px where it can no longer fit, the page always has its own CTA in the hero. */
@media(max-width:1400px){
  .nav .links{gap:14px}
  .nav .links a{font-size:.82rem}
  .nav .links a.seochk,.nav .links a.navcall{padding:7px 12px}
  .nav .bar>.btn{padding:9px 14px;font-size:.8rem}
}
@media(max-width:1240px) and (min-width:1001px){
  .nav .bar>.btn.btn-sm{display:none}
}
/* The bar is max-width capped at 1140, so a page carrying a CTA pill is exactly
   as tight at 1920 as it is at 1400. The tightening above only fired UNDER
   1400, which is why /learn/ still crushed "Book your session" on a wide
   screen. Apply the same tightening above 1400, but only where the pill
   actually exists, so a nav without one keeps its roomier spacing. */
@media(min-width:1401px){
  .nav .bar:has(>.btn) .links{gap:14px}
  .nav .bar:has(>.btn) .links a{font-size:.82rem}
  .nav .bar:has(>.btn) .links a.seochk,.nav .bar:has(>.btn) .links a.navcall{padding:7px 12px}
  .nav .bar:has(>.btn)>.btn{padding:9px 14px;font-size:.8rem}
}
/* tighten link gaps + shrink the demo CTA before the nav crushes */
@media(max-width:1080px){
  .nav .links{gap:14px}
  .nav .links a{font-size:.82rem}
  .nav .bar>.btn{padding:9px 16px;font-size:.8rem}
}

/* collapse to hamburger well before the links overflow */
@media(max-width:1000px){
  .nav .bar{position:relative;justify-content:center}
  /* full logo lockup is ~5.5:1, so keep it shorter than the old mark on mobile */
  .nav img{height:30px;margin-right:auto;flex:0 0 auto}
  .nav .bar>a:first-child{margin-right:auto}
  .navtoggle{display:flex;order:3;margin-left:0}
  /* no demo button in the mobile bar — it lives in the menu + hero */
  .nav .bar>.btn.btn-sm{display:none}
  .nav .links{order:4;position:absolute;top:calc(100% + 10px);left:0;right:0;flex-direction:column;align-items:stretch;gap:0;
    margin:0;padding:8px;background:rgba(10,12,11,.94);border:1px solid var(--line);border-radius:22px;
    -webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur);box-shadow:0 16px 50px rgba(0,0,0,.5);
    opacity:0;transform:translateY(-8px);pointer-events:none;transition:opacity .25s,transform .25s}
  .nav.open .links{opacity:1;transform:none;pointer-events:auto}
  .nav .links a{padding:13px 16px;border-radius:14px;font-size:.98rem}
  .nav .links a:hover{background:rgba(var(--wa),.05)}
  /* in the mobile sheet the pill becomes a full width row, still green, still ticked */
  .nav .links a.seochk{justify-content:center;margin:6px 2px 2px;padding:13px 16px;border-radius:14px}
  /* the generic .nav .links a:hover shorthand above wipes background-image, so the
     glass stack goes straight back rather than the old flat background:none */
  .nav .links a.seochk:hover{background-color:transparent;background-image:var(--glass)}
  .nav .links a.navcall{justify-content:center;margin:6px 2px 2px;padding:13px 16px;border-radius:14px}
}

/* buttons */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:.55em;cursor:pointer;
  border-radius:999px;padding:14px 28px;font:inherit;font-size:.95rem;font-weight:700;
  text-decoration:none;border:1px solid transparent;
  transition:background .2s, color .2s, border-color .2s, transform .2s, box-shadow .25s}
/* THE GREEN BUTTON IS THE LOUDEST THING ON THE PAGE (Tim, 24 Aug 2026).
   Bigger type, a lit rim and a halo that breathes, so on a dark page the eye
   lands on it before anything else. The halo is a ::after ring rather than a
   box-shadow on the button, so it can sit outside the button's own overflow
   clip (the .btn base sets overflow:hidden for the old shine sweep). */
.btn-green{background:var(--green);color:#04130a;font-weight:800;letter-spacing:.005em;
  box-shadow:0 6px 20px rgba(50,207,117,.34),0 0 0 1px rgba(140,255,190,.45) inset;
  animation:btnGlow 3.6s ease-in-out infinite}
.btn-green:hover{background:var(--green-2, #2bb866);transform:translateY(-1px);
  box-shadow:0 10px 34px rgba(50,207,117,.55),0 0 0 1px rgba(160,255,205,.6) inset}
.btn-green:active{transform:translateY(0)}
@keyframes btnGlow{
  0%,100%{box-shadow:0 6px 20px rgba(50,207,117,.30),0 0 0 1px rgba(140,255,190,.42) inset}
  50%    {box-shadow:0 8px 30px rgba(50,207,117,.58),0 0 0 1px rgba(170,255,210,.62) inset}
}
@media(prefers-reduced-motion:reduce){
  .btn-green{animation:none}
  .btn-green:hover{transform:none}
}
.btn-ghost{background:rgba(var(--wa),.06);border-color:var(--line-2);color:var(--ink);
  -webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur)}
.btn-ghost:hover{background:rgba(var(--wa),.1)}
.btn-sm{padding:10px 20px;font-size:.84rem}
/* The SEO checker CTA. It used to be solid orange, which put a second loud
   colour next to the green button and made the hero shout twice (Tim, 25 Aug
   2026). Now it is clear glass: the black behind it shows through, a soft sheen
   drifts across the face the way the nav's SEO Check pill does, and the only
   colour left in the hero is the one green button that matters. */
.btn-flame{color:var(--ink);border-color:rgba(var(--wa),.2);
  background:
    linear-gradient(100deg,transparent 24%,rgba(var(--wa),.14) 47%,transparent 70%),
    linear-gradient(180deg,rgba(var(--wa),.075),rgba(var(--wa),.028));
  background-size:260% 100%,100% 100%;
  background-position:140% 0,0 0;
  -webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur);
  box-shadow:inset 0 1px 0 rgba(var(--wa),.18),0 6px 22px rgba(0,0,0,.34);
  animation:flameFlow 7s linear infinite;
  transition:transform .25s,box-shadow .25s,background-color .25s}
.btn-flame:hover{color:#fff;transform:translateY(-1px);
  border-color:rgba(var(--wa),.3);
  box-shadow:inset 0 1px 0 rgba(var(--wa),.24),0 10px 30px rgba(0,0,0,.42)}
@keyframes flameFlow{from{background-position:140% 0,0 0}to{background-position:-120% 0,0 0}}
@media(prefers-reduced-motion:reduce){.btn-flame{animation:none}}
/* homepage teaser of the checker's own score reveal, on example data */
.sh-demo{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:16px;
  max-width:660px;margin:4px auto 14px;text-align:left}
.sh-card{display:flex;align-items:center;gap:18px;padding:20px;border-radius:var(--r-sm);
  background:rgba(var(--wa),.035);border:1px solid var(--line)}
.sh-card svg{flex:0 0 78px;width:78px;height:78px;transform:rotate(-90deg)}
.sh-card circle{fill:none;stroke-width:9;stroke-linecap:round}
.sh-card .bg{stroke:rgba(var(--wa),.08)}
.sh-card .fg{stroke:var(--green);stroke-dasharray:239;stroke-dashoffset:239;
  transition:stroke-dashoffset 2.6s cubic-bezier(.3,0,.2,1)}
.sh-card .fg.amber{stroke:#ffc63d}
.sh-num{font-size:1.9rem;font-weight:800;letter-spacing:-.03em;line-height:1}
.sh-cap{font-size:.84rem;color:var(--ink);margin-top:5px;font-weight:700}
.sh-cap span{display:block;margin-top:3px;font-size:.76rem;color:var(--ink-mute);font-weight:400;line-height:1.45}
.sh-note{margin:0 0 22px;font-size:.72rem;color:var(--ink-mute)}
@media(prefers-reduced-motion:reduce){.sh-card .fg{transition:none}}

/* REQUEST A DEMO ----------------------------------------------------------
   The section head does the introducing, so the form inside it drops its own
   logo, headline and hello — that is all .hq-bare is. Everywhere else on the
   site the form still carries them, because everywhere else it turns up with
   no section head above it. */
.hq-bare > .hq-brand,.hq-bare > .hq-title,.hq-bare > .hq-face,.hq-bare > .hq-sub{display:none}

.demo-grid{display:grid;grid-template-columns:minmax(0,1.08fr) minmax(360px,.92fr);
  gap:44px;align-items:center;max-width:1120px;margin:0 auto}
.demo-card{padding:34px 30px}
@media(max-width:980px){
  .demo-grid{grid-template-columns:1fr;gap:34px;max-width:560px}
  .dmo{order:2}
  .demo-card{padding:28px 22px}
}

/* The laptop on the call. Borrows the lid, screen and base from the .mbp band
   so there is one laptop on this site rather than two that nearly match, and
   adds the video call furniture over the top of the shared screen: the meeting
   chip, my tile in the bottom right corner, and the controls. All of it is
   decoration over a real client capture, which scrolls the way a page does
   while somebody is being walked through it. */
.dmo{position:relative;width:100%;max-width:600px;margin:0 auto}
.dmo-screen{background:#0b0d0c}
/* the capture is shown, not faded in by sc.js, so it needs its own opacity.
   The only transition it is allowed is the handover dip below: a transform
   transition here would fight the dmoRead animation for the same property. */
.dmo-screen .dmo-page{opacity:1;transition:opacity .3s linear;
  animation:dmoRead 34s cubic-bezier(.5,0,.5,1) infinite}
/* One client handing the screen to the next. sc.js adds this at the seam of
   the scroll, swaps the src while the screen is dark and takes it off again,
   so the change reads as a shared screen switching rather than a slideshow
   cutting. Kept short: it sits inside the first hold of the scroll. */
.dmo-screen .dmo-page.dmo-turn{opacity:0}
@media(prefers-reduced-motion:reduce){.dmo-screen .dmo-page{animation:none}}
/* Long holds, short travels: the same read-a-page-then-turn rhythm the band
   uses, done in pure CSS because nothing here needs to know the image height. */
@keyframes dmoRead{
  0%,16%   {transform:translate3d(0,0,0)}
  22%,38%  {transform:translate3d(0,-13%,0)}
  44%,60%  {transform:translate3d(0,-26%,0)}
  66%,82%  {transform:translate3d(0,-39%,0)}
  92%,100% {transform:translate3d(0,0,0)}
}
.dmo-chip{position:absolute;left:10px;top:10px;z-index:4;display:flex;align-items:center;gap:7px;
  padding:5px 11px;border-radius:999px;background:rgba(12,14,13,.62);
  -webkit-backdrop-filter:var(--glass-sm);backdrop-filter:var(--glass-sm);box-shadow:var(--rim-sm);
  border:1px solid rgba(var(--wa),.1);font-size:.6rem;font-weight:700;letter-spacing:.04em;
  color:rgba(var(--wa),.86)}
.dmo-dot{width:6px;height:6px;border-radius:50%;background:#32cf75;box-shadow:0 0 8px rgba(50,207,117,.9)}
@media(prefers-reduced-motion:no-preference){.dmo-dot{animation:scDockLive 2.4s ease-in-out infinite}}
.dmo-tile{position:absolute;right:10px;bottom:10px;z-index:5;width:27%;max-width:150px;
  aspect-ratio:4/3;border-radius:10px;overflow:hidden;background:#111;
  border:1px solid rgba(var(--wa),.14);
  box-shadow:0 12px 30px -10px rgba(0,0,0,.9)}
/* position and opacity are both reset on purpose: .mbp-screen img is written
   for the full bleed capture, absolute and faded out until sc.js says on, and
   the corner tile is neither of those things. The video in the tile is a silent
   loop of me on an actual client call, so it takes the same box as the still it
   replaced and the poster frame holds the space until it plays. */
.dmo-screen .dmo-tile img,
.dmo-screen .dmo-tile video{position:static;width:100%;height:100%;object-fit:cover;display:block;
  opacity:1;transform:none;transition:none}
.dmo-name{position:absolute;left:7px;bottom:6px;font-size:.58rem;font-weight:700;
  color:#fff;text-shadow:0 1px 4px rgba(0,0,0,.9)}
.dmo-bar{position:absolute;left:50%;bottom:10px;transform:translateX(-50%);z-index:4;
  display:flex;gap:8px;padding:6px 9px;border-radius:999px;background:rgba(12,14,13,.62);
  -webkit-backdrop-filter:var(--glass-sm);backdrop-filter:var(--glass-sm);box-shadow:var(--rim-sm);
  border:1px solid rgba(var(--wa),.1)}
.dmo-key{width:24px;height:24px;border-radius:50%;display:grid;place-items:center;
  background:rgba(var(--wa),.1);color:rgba(var(--wa),.82)}
.dmo-key svg{width:12px;height:12px}
.dmo-key.dmo-end{background:#e5484d;color:#fff;width:34px;border-radius:999px}
@media(max-width:520px){
  .dmo-tile{width:31%}
  .dmo-chip{font-size:.55rem;padding:4px 9px}
  .dmo-bar{gap:6px;padding:5px 7px}
  .dmo-key{width:21px;height:21px}
  .dmo-key.dmo-end{width:30px}
}

/* The "Rather not fill in a form?" row with its Call and Email buttons came off
   the demo panel on 7 Sep 2026 (Tim: "remove this stuff"), so its rules went
   with it. The phone number still sits in the nav, the dock and the footer. */
/* Call button, used in the hero of every ad landing page. .btn has no generic
   svg rule, so without this the phone icon renders at the browser default box
   and swamps the label. Scoped to .btncall rather than added to .btn, because a
   blanket .btn svg rule would resize icons in buttons that already look right. */
.btn.btncall svg{width:15px;height:15px;flex:0 0 auto;color:var(--green)}
/* Send — the lead form's submit. Same drifting green gradient as the SEO Check
   pill in the nav (one shared keyframe), so the two things on the site that are
   actually asking for a click move the same way. Deliberately NOT .btn-green:
   that class's flat hover colour would kill the gradient on hover. */
.btn-send{color:#04130a;border-color:rgba(50,207,117,.55);
  background:linear-gradient(100deg,#1f9e57,#32cf75,#7bf0a8,#32cf75,#1f9e57);
  background-size:300% 100%;animation:seochkFlow 7s linear infinite;
  box-shadow:0 0 0 rgba(50,207,117,0);transition:box-shadow .25s,filter .25s}
.btn-send:hover{color:#04130a;box-shadow:0 8px 26px rgba(50,207,117,.32);filter:brightness(1.06)}
/* mid-send: the sweep speeds up so "Sending…" reads as work happening */
.btn-send:disabled{cursor:default;animation-duration:1.6s;box-shadow:none;filter:none;opacity:.9}
@media(prefers-reduced-motion:reduce){.btn-send{animation:none;background:var(--green)}}

/* checks / chips */
.checks{list-style:none;display:grid;gap:12px;margin:26px 0}
/* in the hero the ticks carry the rest of the promise, so they sit tight under
   the lead line and read at close to body size rather than as fine print */
.hero-checks{gap:10px;margin:18px 0 4px;font-size:1.02rem}
.hero-checks li{color:var(--ink);font-weight:600}
/* The three ticks sparkle one at a time, top to bottom, then the list rests.
   Each tick runs the same 9s loop with a 1.1s stagger, and the flare itself
   only occupies the first ~12% of that loop — so what you see is one tick
   catching the light, then the next, then a long quiet gap before it comes
   round again. Anything faster turns a checklist into a string of fairy
   lights. (Tim, 25 Aug 2026) */
.hero-checks svg{stroke-width:3;filter:drop-shadow(0 0 8px rgba(50,207,117,.45));
  animation:tickSparkle 9s ease-in-out infinite}
/* Held back past the entrance cascade below, so the first sparkle is not spent
   on a tick that has not arrived on screen yet. */
.hero-checks li:nth-child(1) svg{animation-delay:2.2s}
.hero-checks li:nth-child(2) svg{animation-delay:3.3s}
.hero-checks li:nth-child(3) svg{animation-delay:4.4s}
@keyframes tickSparkle{
  0%,100%{filter:drop-shadow(0 0 8px rgba(50,207,117,.45));transform:scale(1)}
  4%     {filter:drop-shadow(0 0 16px rgba(160,255,205,.95)) brightness(1.6);transform:scale(1.16)}
  12%    {filter:drop-shadow(0 0 8px rgba(50,207,117,.45));transform:scale(1)}
}
@media(prefers-reduced-motion:reduce){.hero-checks svg{animation:none}}
@media(max-width:560px){.hero-checks{font-size:.96rem;gap:9px}}

/* ---------------------------------------------------------------------------
   The hero checks arrive ONE BY ONE, after the headline (Tim, 31 Aug 2026).

   The promise under the headline used to be three lines that were simply there
   the instant the page painted, so the eye had nowhere to go and the whole
   column landed as one block. Now the headline lands on its own, then each tick
   lifts in under it in turn, and the reassurance line closes the group off.

   Deliberately CSS and not JS. This is above the fold and it is the first thing
   anyone sees, so it must not wait on a script that might be a second behind the
   paint — and there is nothing to observe here, the cascade runs once at load
   and never again.

   ~.62s per item on a ~.58s stagger, which is slow enough to read as "one, then
   the next" rather than a ripple, and done inside 2.4s so nobody is ever waiting
   on it. The buttons are left OUT of the cascade on purpose: a call to action
   that fades in late is a call to action somebody has already scrolled past.

   backwards fill matters as much as forwards: without it every item would flash
   at full opacity for the length of its own delay before dropping out to start
   the animation, which is the exact opposite of the effect. */
.hero-checks li,.hero-fine{opacity:0;
  animation:heroRiseIn .62s cubic-bezier(.22,.68,.34,1) both}
.hero-checks li:nth-child(1){animation-delay:.70s}
.hero-checks li:nth-child(2){animation-delay:1.28s}
.hero-checks li:nth-child(3){animation-delay:1.86s}
.hero-fine{margin-top:22px;font-size:.82rem;color:var(--ink-mute);animation-delay:2.44s}
@keyframes heroRiseIn{
  from{opacity:0;transform:translateY(9px)}
  to  {opacity:1;transform:none}
}
/* Reduced motion gets the content, not the choreography. Everything is on
   screen at first paint, which is also the safety net if the animation is
   dropped for any other reason. */
@media(prefers-reduced-motion:reduce){
  .hero-checks li,.hero-fine{opacity:1;animation:none;transform:none}
}
/* The tick sits in its own fixed column rather than in the text flow, and is
   centred on the FIRST LINE BOX (1.6em tall) rather than on the whole item, so
   a one line point and a three line point both put the tick beside the first
   word. The check glyph is drawn in the top two thirds of its 24 box, so the
   nudge below is what pulls its optical centre onto the line's. */
.checks li{display:grid;grid-template-columns:1.25em 1fr;gap:.7em;align-items:start;
  color:var(--ink-soft);font-weight:500}
.checks svg{width:1.25em;height:1.25em;flex:none;color:var(--green);
  margin-top:calc((1.6em - 1.25em) / 2 + .04em)}
.chip{display:inline-flex;align-items:center;gap:.5em;padding:7px 14px;border-radius:999px;
  background:var(--green-soft);border:1px solid rgba(50,207,117,.3);color:var(--green);
  font-size:.78rem;font-weight:700;letter-spacing:.04em}
.chip .dot{width:7px;height:7px;border-radius:50%;background:var(--green);animation:pulse 1.6s infinite}
.chip.chip-face{padding:5px 20px 5px 5px;gap:.7em}
.chip.chip-face img{width:46px;height:46px;border-radius:50%;object-fit:cover;flex:none;
  border:1px solid rgba(50,207,117,.45);display:block}
@media(max-width:600px){.chip.chip-face img{width:40px;height:40px}}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}

/* feature grid */
.fgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:18px}
.fcard{padding:30px 26px}

/* ===== Two-arms cards: equal height, CTA pinned to the bottom ===== */
.split.even{align-items:stretch}
.split.even .fcard{display:flex;flex-direction:column;height:100%;min-height:320px}
.split.even .fcard .btn{margin-top:auto;align-self:flex-start}

/* ===== "On another level" — premium animated enterprise card ===== */
.card-elevated{position:relative;isolation:isolate;overflow:visible;animation:elevFloat 7s ease-in-out infinite}
@keyframes elevFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
/* animated glow on scalus.io link inside the card */
.card-elevated h3{position:relative}
.card-elevated h3 a{position:relative;display:inline-block;padding:0 6px}
.card-elevated h3 a::before{content:"";position:absolute;inset:-3px -6px;border-radius:6px;
  background:radial-gradient(circle at 50% 50%, rgba(50,207,117,.25), rgba(50,207,117,.08) 60%);
  z-index:-1;animation:linkpulse 2.5s ease-in-out infinite;pointer-events:none}
@keyframes linkpulse{0%,100%{opacity:.4;transform:scale(1)}50%{opacity:.75;transform:scale(1.05)}}
/* rotating conic glow ring sitting just behind the card */
.card-elevated::before{content:"";position:absolute;inset:-2px;border-radius:inherit;z-index:-2;padding:2px;
  background:conic-gradient(from 0deg,transparent 0deg,rgba(50,207,117,.9) 70deg,rgba(95,227,154,.5) 110deg,transparent 180deg,transparent 250deg,rgba(255,122,60,.55) 320deg,transparent 360deg);
  -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask-composite:exclude;
  animation:elevSpin 6s linear infinite;opacity:.9}
@keyframes elevSpin{to{transform:rotate(360deg)}}
/* soft pulsing aura halo */
.card-elevated::after{content:"";position:absolute;inset:-1px;border-radius:inherit;z-index:-3;
  box-shadow:0 0 60px rgba(50,207,117,.35);animation:elevHalo 4s ease-in-out infinite}
@keyframes elevHalo{0%,100%{box-shadow:0 0 50px rgba(50,207,117,.28)}50%{box-shadow:0 0 90px rgba(50,207,117,.5),0 0 40px rgba(255,122,60,.22)}}
.card-elevated:hover::before{animation-duration:2.4s}
/* the "Enterprise" eyebrow chip above the title */
.elev-tag{display:inline-flex;align-items:center;gap:.5em;font-size:.7rem;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:var(--green);margin-bottom:10px}
.elev-tag .dot{width:6px;height:6px;border-radius:50%;background:var(--green);box-shadow:0 0 10px var(--green);animation:pulse 1.8s infinite}
/* shimmer sweep across the card surface */
.card-elevated .shimmer{position:absolute;inset:0;border-radius:inherit;overflow:hidden;pointer-events:none;z-index:0}
.card-elevated .shimmer::before{content:"";position:absolute;top:-50%;left:-60%;width:40%;height:200%;
  background:linear-gradient(115deg,transparent,rgba(var(--wa),.10),transparent);transform:rotate(8deg);
  animation:elevShimmer 5.5s ease-in-out infinite}
@keyframes elevShimmer{0%{left:-60%}55%,100%{left:130%}}
.card-elevated>*:not(.shimmer){position:relative;z-index:1}
@media(prefers-reduced-motion:reduce){.card-elevated,.card-elevated::before,.card-elevated::after,.card-elevated .shimmer::before,.elev-tag .dot{animation:none!important}}
.fcard .ic{width:46px;height:46px;border-radius:14px;display:grid;place-items:center;margin-bottom:18px;
  background:var(--green-soft);border:1px solid rgba(50,207,117,.3);color:var(--green)}
.fcard .ic svg{width:22px;height:22px}
/* one hue per card so the four features read apart at a glance */
.ic-green{background:rgba(50,207,117,.13)!important;border-color:rgba(50,207,117,.34)!important;color:#32cf75!important}
.ic-gold{background:rgba(255,198,61,.13)!important;border-color:rgba(255,198,61,.34)!important;color:#ffc63d!important}
.ic-blue{background:rgba(74,158,255,.13)!important;border-color:rgba(74,158,255,.34)!important;color:#4a9eff!important}
.ic-violet{background:rgba(167,120,255,.13)!important;border-color:rgba(167,120,255,.34)!important;color:#a778ff!important}
.ic-coral{background:rgba(255,122,97,.13)!important;border-color:rgba(255,122,97,.34)!important;color:#ff7a61!important}
.fcard h2,.fcard h3{margin-bottom:8px}
.fcard p{color:var(--ink-soft);font-size:.93rem}

/* ── recent updates, the homepage strip ───────────────────────────────────
   The same four across grid as the feature cards, revealing one at a time via
   .stagger, but carrying the real releases. Stamped between the HOME-UPDATES
   markers in index.html by scripts/seo/build-updates.mjs, so it can never
   disagree with /updates/. */
.upmini{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:18px;align-items:stretch}
.upcard{display:flex;flex-direction:column;padding:24px 22px;text-decoration:none;color:inherit;
  transition:border-color .3s}
.upcard:hover{border-color:var(--line-2)}
.upc-top{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:14px}
.upc-ver{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:1.15rem;font-weight:700;
  letter-spacing:-.02em;color:var(--green)}
.upcard.is-major .upc-ver{font-size:1.35rem}
.upc-tag{display:inline-flex;align-items:center;gap:.5em;padding:4px 10px;border-radius:999px;border:1px solid var(--line);background:rgba(var(--wa),.04);
  font-size:.68rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-mute);white-space:nowrap}
.upc-tag svg{width:12px;height:12px;flex:none;color:var(--green)}

/* THE NEWEST RELEASE GLOWS. Four cards side by side all read as equally recent,
   and the one that matters is the one that shipped last. A green rim and a slow
   breathing halo mark it without needing a "new" label to say so. */
.upcard.is-newest{border-color:rgba(50,207,117,.42);background:linear-gradient(165deg,rgba(50,207,117,.07),rgba(var(--wa),.02))}
.upcard.is-newest .upc-tag{border-color:rgba(50,207,117,.35);color:var(--green);background:var(--green-soft)}
.upcard.is-newest .upc-tag svg{color:var(--green)}
html.js .upcard.is-newest.in{animation:upGlow 4.2s ease-in-out 1.1s infinite}
@keyframes upGlow{
  0%,100%{box-shadow:0 0 0 1px rgba(50,207,117,.16),0 0 26px rgba(50,207,117,.10)}
  50%    {box-shadow:0 0 0 1px rgba(50,207,117,.40),0 0 46px rgba(50,207,117,.26)}
}
@media(prefers-reduced-motion:reduce){
  html.js .upcard.is-newest.in{animation:none;box-shadow:0 0 0 1px rgba(50,207,117,.3)}
}
.upcard h3{font-size:1.02rem;line-height:1.35;margin-bottom:8px}
.upcard p{color:var(--ink-soft);font-size:.88rem;line-height:1.55;
  display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}
.upc-date{display:block;margin-top:auto;padding-top:16px;font-size:.74rem;color:var(--ink-mute)}
.upc-more{text-align:center;margin-top:28px}

/* SCALUS UPDATES, the big title (Tim, 7 Oct 2026: "Scalus Updates, have
   updates in gradient animation effect, make big title"). .grad-flow is the
   hero's drifting colour, set inline here so it sits beside "Scalus". */
.upd-title{font-size:clamp(3rem,9vw,6.4rem);line-height:.98;letter-spacing:-.045em;margin-top:14px;text-wrap:balance}
.upd-title .grad-flow{display:inline-block;padding-bottom:.08em}
.upd-sub{font-size:clamp(1.15rem,2.2vw,1.45rem);font-weight:700;color:var(--ink);margin:18px 0 6px}

/* THE UPDATES STACK. Every release in one pile, iOS 27 style: the front card
   is clear glass with its whole summary, the two behind it peek out above,
   smaller and dimmer. Each turn the front card drops down and fades, and
   the pile steps forward. sc.js sets data-pos on each card; with no script
   only the first card shows, which is still the newest release.

   LIQUID GLASS OVER MOVING COLOUR (Tim, 9 Oct 2026: "add some more colour
   animation liquid glass super transparent and some overlays of the info of
   the updates"). Four soft colour blobs drift behind the pile (.ups-aura) and
   the cards are close to clear, so the colour moves THROUGH them. The first
   blob takes the colour of the front card's category (--ups-c, set by sc.js
   from data-c), so the whole pile changes hue as it turns. Only the front
   card blurs what is behind it: three blurred layers over moving colour is
   what makes a phone drop frames. The cards behind show their top row only,
   so their words never smear through the front card.
   The overlays: .upc-what is the strip that steps through what changed in
   that release, .upc-live says it is live, and the laptop animation above
   (.upd-anim) plays the same release, see further down. */
@property --ups-c{syntax:'<color>';inherits:true;initial-value:#32cf75}
.upstack{position:relative;max-width:660px;margin:0 auto;--ups-c:#32cf75;transition:--ups-c 1.2s ease}
.ups-aura{position:absolute;z-index:0;left:50%;top:-90px;bottom:-30px;width:min(1080px,100vw - 28px);
  transform:translateX(-50%);pointer-events:none}
.ups-aura i{position:absolute;border-radius:50%;will-change:transform;
  animation:upsDriftA 19s ease-in-out infinite alternate;animation-play-state:paused}
.upstack.is-seen .ups-aura i{animation-play-state:running}
.ups-aura i:nth-child(1){left:10%;top:4%;width:44%;height:74%;
  background:radial-gradient(closest-side,rgba(50,207,117,.5),transparent);
  background:radial-gradient(closest-side,color-mix(in srgb,var(--ups-c) 58%,transparent),transparent)}
.ups-aura i:nth-child(2){right:8%;top:-4%;width:42%;height:70%;animation-name:upsDriftB;animation-duration:24s;animation-delay:-7s;
  background:radial-gradient(closest-side,rgba(90,209,255,.46),transparent)}
.ups-aura i:nth-child(3){left:30%;bottom:-10%;width:42%;height:72%;animation-duration:28s;animation-delay:-12s;
  background:radial-gradient(closest-side,rgba(169,123,255,.46),transparent)}
.ups-aura i:nth-child(4){right:18%;bottom:2%;width:32%;height:56%;animation-name:upsDriftB;animation-duration:21s;animation-delay:-3s;
  background:radial-gradient(closest-side,rgba(255,122,194,.36),transparent)}
@keyframes upsDriftA{0%{transform:translate3d(-9%,-6%,0) scale(1)}50%{transform:translate3d(11%,9%,0) scale(1.2)}100%{transform:translate3d(-3%,13%,0) scale(.9)}}
@keyframes upsDriftB{0%{transform:translate3d(8%,7%,0) scale(1.1)}50%{transform:translate3d(-12%,-5%,0) scale(.9)}100%{transform:translate3d(5%,-11%,0) scale(1.22)}}
.ups-deck{position:relative;z-index:1;height:var(--ups-h,340px);padding-top:40px}
.upstack .upcard{position:absolute;left:0;right:0;top:40px;--upc-c:#32cf75;
  padding:28px 28px 24px;border-radius:28px;
  background:radial-gradient(120% 90% at 0% 0%,rgba(255,255,255,.13),transparent 44%),
    linear-gradient(160deg,rgba(var(--wa),.07),rgba(var(--wa),.015));
  border:1px solid rgba(var(--wa),.2);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.34),inset 0 -1px 0 rgba(255,255,255,.06),
    inset 0 0 30px rgba(255,255,255,.035),0 30px 70px -30px rgba(0,0,0,.6);
  transform-origin:50% 0;
  transition:transform .75s cubic-bezier(.2,.9,.25,1),opacity .55s ease,filter .55s ease;
  will-change:transform,opacity}
/* film.js rings every .upcard with .film-glow, which sets position:relative at
   higher specificity than the rule above. The deck has to win that. */
.upstack .ups-deck .upcard{position:absolute}
.upstack .upcard:not(:first-child){opacity:0;pointer-events:none}
.upstack .upcard[data-pos]{opacity:0;pointer-events:none;z-index:1;transform:translateY(-56px) scale(.82)}
.upstack .upcard[data-pos="0"]{opacity:1;pointer-events:auto;z-index:5;transform:none;
  -webkit-backdrop-filter:blur(16px) saturate(1.9) brightness(1.05);backdrop-filter:blur(16px) saturate(1.9) brightness(1.05)}
.upstack .upcard[data-pos="1"]{opacity:.8;z-index:4;transform:translateY(-24px) scale(.94);filter:saturate(.8)}
.upstack .upcard[data-pos="2"]{opacity:.5;z-index:3;transform:translateY(-46px) scale(.88);filter:saturate(.6)}
.upstack .upcard[data-pos="out"]{opacity:0;z-index:6;transform:translateY(90px) scale(1.03)}
.upstack .upcard > *{position:relative;transition:opacity .4s ease}
.upstack .upcard[data-pos]:not([data-pos="0"]) > :not(.upc-top){opacity:0}
.upstack .upc-ver{color:var(--upc-c);text-shadow:0 0 22px color-mix(in srgb,var(--upc-c) 55%,transparent)}
.upstack .upc-tag{border-color:rgba(var(--wa),.2);background:rgba(var(--wa),.06);color:var(--ink-soft)}
.upstack .upc-tag svg{color:var(--upc-c)}
.upstack .upcard h3{font-size:1.3rem;line-height:1.3;margin-bottom:10px}
.upstack .upcard p{font-size:.98rem;line-height:1.6;-webkit-line-clamp:3;color:rgba(var(--wa),.8)}
.upstack .upcard[data-pos="0"] p,.upstack .upcard.is-measuring p{-webkit-line-clamp:unset;display:block}
.upstack .upcard.is-newest{border-color:color-mix(in srgb,var(--upc-c) 55%,transparent);
  background:radial-gradient(120% 90% at 0% 0%,rgba(255,255,255,.14),transparent 44%),
    linear-gradient(160deg,color-mix(in srgb,var(--upc-c) 14%,transparent),rgba(var(--wa),.02))}
.upstack .upcard.is-newest .upc-tag{border-color:color-mix(in srgb,var(--upc-c) 40%,transparent);color:var(--upc-c);
  background:color-mix(in srgb,var(--upc-c) 14%,transparent)}
html.js .upstack .upcard.is-newest.in{animation:none}
.upstack .upcard:hover{border-color:rgba(var(--wa),.34)}
/* A MAJOR RELEASE (the first number: 2.0, 3.0). A special thing, so its card says so
   (Tim, 9 Oct 2026). The number is in the page's moving colour and bigger, the words
   Major release sit beside it, and its ring glows at full strength at the front. The
   class comes from the version number in build-updates.mjs, never from a flag. */
.upc-bigword{display:none}
.upstack .upcard.is-big .upc-top{justify-content:flex-start;gap:12px}
.upstack .upcard.is-big .upc-ver{display:inline-block;font-size:2.1rem;line-height:1;text-shadow:none;padding:0 .06em .08em 0}
.upstack .upcard.is-big .upc-bigword{display:inline-block;font-size:.68rem;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:var(--ink)}
.upstack .upcard.is-big .upc-tag{margin-left:auto}
.upstack .upcard.is-big{border-color:rgba(var(--wa),.34)}
body[data-film] .upstack .upcard.is-big[data-pos="0"]::before{opacity:1}
body[data-film] .upstack .upcard.is-big[data-pos="0"]::after{opacity:.6}
[data-theme="light"] .upstack .upcard.is-big .upc-ver{filter:none}
/* the colour ring film.js puts on every card wakes up on the front one */
body[data-film] .upstack .upcard[data-pos="0"]::before{opacity:.85}
body[data-film] .upstack .upcard[data-pos="0"]::after{opacity:.3}
/* a band of light crosses the front card every few seconds */
.upstack .upc-sheen{position:absolute;inset:0;border-radius:inherit;overflow:hidden;pointer-events:none;z-index:0}
.upstack .upc-sheen::before{content:"";position:absolute;top:-20%;bottom:-20%;left:-60%;width:46%;transform:skewX(-18deg);
  background:linear-gradient(90deg,transparent,rgba(255,255,255,.13),transparent);opacity:0}
.upstack .upcard[data-pos="0"] .upc-sheen::before{animation:upSheen 6.4s ease-in-out 1.2s infinite}
@keyframes upSheen{0%{left:-60%;opacity:0}8%{opacity:1}42%{left:120%;opacity:1}46%,100%{left:120%;opacity:0}}
/* WHAT CHANGED, one line at a time. sc.js steps through data-what while the
   card is at the front. One line with an ellipsis on a wide card, two fixed
   lines on a phone, so the card never changes height as the words change. */
.upc-what,.upc-live{display:none}
.upstack .upc-what{display:flex;align-items:center;gap:10px;margin-top:16px;padding:9px 13px 9px 9px;border-radius:14px;
  background:rgba(var(--wa),.055);border:1px solid rgba(var(--wa),.14);box-shadow:inset 0 1px 0 rgba(255,255,255,.14);
  font-size:.86rem;font-weight:600;line-height:1.35;color:var(--ink)}
.upstack .upc-what svg{flex:none;width:22px;height:22px;padding:5px;border-radius:50%;background:var(--upc-c);color:#04130a}
.upc-what-t{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:opacity .18s ease,transform .18s ease}
.upc-what.sw .upc-what-t{opacity:0;transform:translateY(5px)}
.upc-what-n{flex:none;font-size:.72rem;font-weight:600;color:var(--ink-mute);font-variant-numeric:tabular-nums}
.upstack .upc-date{display:flex;align-items:center;flex-wrap:wrap;gap:4px 12px;padding-top:14px}
.upstack .upc-live{display:inline-flex;align-items:center;gap:7px;color:var(--green);font-weight:700}
.upstack .upc-live i{width:7px;height:7px;border-radius:50%;background:var(--green)}
.upstack .upcard[data-pos="0"] .upc-live i{animation:upLive 2s ease-out infinite}
@keyframes upLive{0%{box-shadow:0 0 0 0 rgba(50,207,117,.6)}100%{box-shadow:0 0 0 8px rgba(50,207,117,0)}}
.ups-nav{position:relative;z-index:1;display:flex;align-items:center;justify-content:center;gap:16px;margin-top:22px}
.ups-btn{width:44px;height:44px;border-radius:50%;display:grid;place-items:center;cursor:pointer;
  color:var(--ink);background:rgba(var(--wa),.07);border:1px solid rgba(var(--wa),.16);
  -webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);transition:background .2s,transform .2s}
.ups-btn:hover{background:rgba(var(--wa),.14)}
.ups-btn:active{transform:scale(.92)}
.ups-btn svg{width:20px;height:20px}
.ups-count{font-size:.85rem;color:var(--ink-mute);min-width:64px;text-align:center;font-variant-numeric:tabular-nums}
.ups-count b{color:var(--ink)}
[data-theme="light"] .ups-aura{opacity:.55}
[data-theme="light"] .upstack .upcard{background:linear-gradient(160deg,rgba(255,255,255,.7),rgba(255,255,255,.42));
  border-color:rgba(0,0,0,.08);box-shadow:inset 0 1px 0 #fff,0 30px 60px -30px rgba(0,0,0,.28)}
[data-theme="light"] .upstack .upcard.is-newest{border-color:rgba(50,207,117,.55);
  background:linear-gradient(160deg,rgba(236,252,243,.78),rgba(255,255,255,.5))}
[data-theme="light"] .upstack .upcard[data-pos="1"],[data-theme="light"] .upstack .upcard[data-pos="2"]{
  background:linear-gradient(160deg,rgba(233,235,239,.9),rgba(223,226,231,.8));border-color:rgba(0,0,0,.1)}
[data-theme="light"] .upstack .upc-ver{text-shadow:none;filter:brightness(.72) saturate(1.3)}
[data-theme="light"] .upstack .upc-tag svg{filter:brightness(.72) saturate(1.3)}
[data-theme="light"] .upstack .upcard p{color:var(--ink-soft)}
[data-theme="light"] .upstack .upc-what{background:rgba(255,255,255,.6);border-color:rgba(0,0,0,.08)}
@media(max-width:560px){
  .upstack .upcard{padding:22px 20px 20px;border-radius:24px}
  .upstack .upcard h3{font-size:1.12rem}
  .upstack .upcard p{font-size:.92rem}
  .upstack .upc-what{align-items:flex-start;font-size:.84rem}
  .upc-what-t{white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;min-height:2.7em}
  .upc-what-n{padding-top:3px}
}
@media(prefers-reduced-motion:reduce){
  .upstack .upcard{transition:opacity .2s ease}
  .ups-aura i,.upstack .upcard[data-pos="0"] .upc-sheen::before,.upstack .upcard[data-pos="0"] .upc-live i{animation:none}
}
@media(max-width:760px){.upcard{padding:20px 18px}.upcard p{-webkit-line-clamp:3}}

/* pricing */
.tiers{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px;align-items:stretch}
h2.plan-group,.plan-group{text-align:center;font-size:.8rem;font-weight:800;letter-spacing:.22em;text-transform:uppercase;color:var(--ink-soft);margin:44px 0 22px}
.plan-group:first-of-type{margin-top:8px}
.plan-group-sub{text-align:center;color:var(--ink-mute);font-size:.9rem;margin:-14px 0 22px}
.tier{padding:34px 30px;display:flex;flex-direction:column;position:relative}
.tier.star{border-color:rgba(50,207,117,.5);box-shadow:0 30px 70px rgba(50,207,117,.12), inset 0 1px 0 rgba(var(--wa),.08)}
.tier .badge{position:absolute;top:-13px;left:50%;transform:translateX(-50%);white-space:nowrap;
  background:var(--green);color:#04130a;font-size:.7rem;font-weight:800;letter-spacing:.12em;
  text-transform:uppercase;padding:6px 16px;border-radius:999px}
.tier .tname{font-size:.8rem;font-weight:800;letter-spacing:.22em;text-transform:uppercase;color:var(--ink-soft)}
.tier .price{margin:14px 0 4px;font-size:2.6rem;font-weight:800;letter-spacing:-.03em}
.tier .price span{font-size:1rem;font-weight:600;color:var(--ink-mute)}
.tier .per{font-size:.8rem;color:var(--ink-mute);margin-bottom:20px}
.tier .checks{flex:1;gap:9px;font-size:.92rem}
.tier .btn{width:100%;margin-top:24px}

/* THE ONE PLAN, WIDE ------------------------------------------------------
   Everything included on the left in two columns, the price and the single
   action on the right behind a soft green wash. The divider is a border on the
   buy rail rather than a pseudo element, so it collapses on its own when the
   card stacks on a phone. Nine features and one price, and no second link
   anywhere near it: the card asks for exactly one thing. */
.tier-wide{display:grid;grid-template-columns:minmax(0,1.55fr) minmax(300px,.95fr);
  gap:0;max-width:1000px;margin:0 auto;overflow:hidden;
  border-color:rgba(50,207,117,.42);
  box-shadow:0 34px 80px rgba(50,207,117,.13), inset 0 1px 0 rgba(var(--wa),.08)}
.tier-wide .tw-feats{margin:0;padding:38px 38px 38px 36px;gap:14px;font-size:.95rem;
  display:grid;grid-template-columns:1fr 1fr;align-content:start;column-gap:26px}
.tier-wide .tw-buy{display:flex;flex-direction:column;justify-content:center;
  padding:38px 36px;border-left:1px solid rgba(var(--wa),.08);
  background:radial-gradient(120% 90% at 80% 0,rgba(50,207,117,.14),transparent 62%)}
.tier-wide .tname{font-size:.78rem;font-weight:800;letter-spacing:.22em;text-transform:uppercase;color:var(--ink-soft)}
.tier-wide .price{margin:14px 0 4px;font-size:3.1rem;font-weight:800;letter-spacing:-.035em;line-height:1}
.tier-wide .price span{font-size:1rem;font-weight:600;color:var(--ink-mute);letter-spacing:0}
.tier-wide .per{font-size:.82rem;color:var(--ink-mute);margin-bottom:26px}
.tier-wide .btn{width:100%}
/* Tim, 16 Sep 2026: the nine included things land one by one, .2s apart (sc.js
   sets that delay), then keep catching the light: sc.js lights one at random
   every couple of seconds and the tick and the words glow green for a beat. */
.tw-feats li{position:relative;border-radius:10px;transition:opacity .7s,transform .7s,color .5s}
.tw-feats li::before{content:"";position:absolute;inset:-4px -10px;border-radius:12px;z-index:-1;opacity:0;
  background:linear-gradient(90deg,rgba(50,207,117,.18),rgba(50,207,117,.04) 70%,transparent);
  transition:opacity .5s}
.tw-feats li.lit{color:var(--green)}
.tw-feats li.lit::before{opacity:1;animation:twLitSweep 1.5s ease-out}
.tw-feats li.lit svg{animation:twTick 1.5s ease-out}
@keyframes twLitSweep{from{transform:translateX(-14px);opacity:0}30%{opacity:1}to{transform:none}}
@keyframes twTick{0%{filter:drop-shadow(0 0 0 rgba(50,207,117,0))}35%{transform:scale(1.28);filter:drop-shadow(0 0 10px rgba(50,207,117,.9))}100%{transform:none;filter:drop-shadow(0 0 0 rgba(50,207,117,0))}}
/* The call back button breathes: a soft green glow that swells and settles,
   so the one action on the card is never just sitting there. */
.tier-wide .tw-buy .btn-green{animation:twGlow 2.8s ease-in-out infinite}
@keyframes twGlow{0%,100%{box-shadow:0 0 0 0 rgba(50,207,117,.55),0 8px 24px -8px rgba(50,207,117,.5)}
  50%{box-shadow:0 0 0 7px rgba(50,207,117,0),0 12px 36px -6px rgba(50,207,117,.85)}}
@media(prefers-reduced-motion:reduce){.tier-wide .tw-buy .btn-green,.tw-feats li.lit::before,.tw-feats li.lit svg{animation:none}}
.tier-wide .tw-note{margin:14px 0 0;font-size:.8rem;color:var(--ink-mute);line-height:1.5}
@media(max-width:900px){
  .tier-wide{grid-template-columns:1fr}
  .tier-wide .tw-feats{grid-template-columns:1fr;padding:30px 26px 26px}
  .tier-wide .tw-buy{border-left:0;border-top:1px solid rgba(var(--wa),.08);padding:28px 26px 32px}
  .tier-wide .price{font-size:2.7rem}
}

/* content shoots section */
.shoot-grid{display:grid;grid-template-columns:1.5fr 1fr;gap:34px;align-items:center}
.shoot-copy .tname{font-size:.8rem;font-weight:800;letter-spacing:.22em;text-transform:uppercase}
.shoot-copy p{color:var(--ink-soft);line-height:1.65}
.shoot-copy b{color:var(--ink);font-weight:700}
.shoot-price{padding:30px 28px;text-align:left}
.shoot-price .tname{font-size:.78rem;font-weight:800;letter-spacing:.2em;text-transform:uppercase}
.shoot-price .price{font-size:2.4rem;font-weight:800;letter-spacing:-.03em}
.shoot-price .price span{font-size:1rem;font-weight:600;color:var(--ink-mute)}
.shoot-price .per{font-size:.8rem;color:var(--ink-mute)}
.shoot-price .checks{font-size:.9rem;gap:11px}
@media(max-width:760px){.shoot-grid{grid-template-columns:1fr;gap:26px}}

/* product mock UI */
.ui{border-radius:var(--r);border:1px solid var(--line-2);overflow:hidden;
  background:linear-gradient(180deg,#101312,#0a0c0b);box-shadow:0 40px 90px rgba(0,0,0,.6)}
/* whole-box orange glow behind the panel when a new notification lands,
   fading back over 2s (re-armed on each insert by toggling .flash in JS) */
.ui.flash{animation:uiflash 2s ease-out}
@keyframes uiflash{
  0%   {box-shadow:0 40px 90px rgba(0,0,0,.6), 0 0 0 1px rgba(255,77,0,.55), 0 0 34px 4px rgba(255,77,0,.45)}
  100% {box-shadow:0 40px 90px rgba(0,0,0,.6), 0 0 0 1px rgba(255,77,0,0),   0 0 34px 4px rgba(255,77,0,0)}
}
@media(prefers-reduced-motion:reduce){.ui.flash{animation:none}}
.ui .ui-top{display:flex;gap:7px;padding:13px 16px;border-bottom:1px solid var(--line)}
.ui .ui-top i{width:10px;height:10px;border-radius:50%;background:rgba(var(--wa),.14)}
.ui .ui-top i:first-child{background:#ff5f57}.ui .ui-top i:nth-child(2){background:#febc2e}.ui .ui-top i:nth-child(3){background:#28c840}
.ui-row{display:flex;gap:12px;align-items:center;padding:13px 18px;border-bottom:1px solid var(--line);font-size:.86rem}
.ui-row:last-child{border-bottom:0}
.ui-row .av{width:34px;height:34px;border-radius:50%;flex:none;display:grid;place-items:center;
  font-weight:800;font-size:.78rem;background:var(--green-soft);color:var(--green);border:1px solid rgba(50,207,117,.3)}
.ui-row .av.o{background:rgba(255,77,0,.14);color:var(--orange);border-color:rgba(255,77,0,.3)}
.ui-row .av.b{background:rgba(14,162,222,.14);color:#5ec5ec;border-color:rgba(14,162,222,.3)}
.ui-row .av.p{background:rgba(160,110,255,.14);color:#b79bff;border-color:rgba(160,110,255,.3)}
.ui-row .av.y{background:rgba(255,193,60,.14);color:#ffcf6b;border-color:rgba(255,193,60,.3)}
.ui-row .av.t{background:rgba(45,212,191,.14);color:#5eead4;border-color:rgba(45,212,191,.3)}
.ui-row b{display:block;font-size:.85rem}
.ui-row small{color:var(--ink-mute);display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:340px}
.ui-row .src{margin-left:auto;flex:none;font-size:.66rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
  padding:4px 10px;border-radius:999px;background:rgba(var(--wa),.06);border:1px solid var(--line);color:var(--ink-soft)}
/* Fixed-size window: the panel NEVER changes height. JS pins the feed to its
   exact 5-row height and clips it, so a 6th (incoming) row can't grow the box —
   the new row slides in the top as the bottom one is pushed out of the clip at
   the same rate, like a new iMessage. overflow:hidden is what holds the size. */
[data-feed]{position:relative;overflow:hidden;will-change:transform}
/* a freshly arrived row glows orange gently, then fades back to green, then transparent */
.ui-row.new{animation:newflash 6.8s ease-out forwards}
@keyframes newflash{
  0%   {background:rgba(255,77,0,.15); box-shadow:inset 0 0 24px rgba(255,77,0,.22)}
  8%   {background:rgba(255,77,0,.14); box-shadow:inset 0 0 22px rgba(255,77,0,.20)}
  28%  {background:rgba(255,77,0,.08); box-shadow:inset 0 0 18px rgba(255,77,0,.12)}
  60%  {background:rgba(50,207,117,.06); box-shadow:inset 0 0 14px rgba(50,207,117,.10)}
  100% {background:transparent; box-shadow:inset 0 0 0 rgba(50,207,117,0)}
}
@media(prefers-reduced-motion:reduce){.ui-row.new{animation:none}}

/* stats strip */
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:16px;text-align:center}
.stat .n{font-size:clamp(1.9rem,4vw,2.8rem);font-weight:800;letter-spacing:-.03em;color:var(--green)}
.stat .l{font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-mute);font-weight:700}

/* steps */
.steps{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:18px;counter-reset:step}
.step{padding:30px 26px;counter-increment:step;position:relative}
.step::before{content:"0" counter(step);position:absolute;top:28px;right:26px;font-size:1.5rem;font-weight:800;color:var(--ink-mute);opacity:.5;letter-spacing:-.03em}
.step .ic{width:46px;height:46px;border-radius:14px;display:grid;place-items:center;margin-bottom:16px}
.step .ic svg{width:22px;height:22px}
.step h3{margin-bottom:8px}
.step p{color:var(--ink-soft);font-size:.92rem}
.step small{display:inline-block;margin-top:12px;color:var(--ink-mute);font-size:.76rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase}

/* logos */
.logos-head{text-align:center;font-size:.72rem;font-weight:700;letter-spacing:.22em;text-transform:uppercase;color:var(--ink-mute);margin-bottom:30px}
.logos{display:flex;flex-wrap:wrap;gap:28px 56px;align-items:center;justify-content:center}
.logo-cell{display:flex;align-items:center;justify-content:center;width:180px;height:54px}
.logos img{max-height:42px;max-width:100%;width:auto;height:auto;object-fit:contain;opacity:.95;transition:.3s}
.logo-cell:hover img{opacity:1}
@media(max-width:560px){.logo-cell{width:140px;height:46px}.logos img{max-height:34px}}

/* testimonials */
.quotes{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:18px}
.quote{padding:30px 28px}
.quote .stars{color:var(--green);letter-spacing:.15em;margin-bottom:14px}
.quote p{color:var(--ink-soft);font-size:.95rem}
.quote footer{margin-top:18px;font-weight:700;font-size:.88rem}
.quote footer span{display:block;color:var(--ink-mute);font-weight:500;font-size:.78rem}

/* FAQ */
.faq{display:grid;gap:12px}
.faq details{border:1px solid var(--line);border-radius:var(--r-sm);background:var(--panel);
  -webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur);overflow:hidden}
.faq summary{cursor:pointer;list-style:none;padding:18px 22px;font-weight:700;display:flex;justify-content:space-between;gap:14px;align-items:center}
.faq summary::-webkit-details-marker{display:none}
.faq summary::after{content:"+";color:var(--green);font-size:1.3rem;font-weight:600;transition:transform .3s}
.faq details[open] summary::after{transform:rotate(45deg)}
.faq .a{padding:0 22px 20px;color:var(--ink-soft);font-size:.93rem}

/* forms */
.form{display:grid;gap:14px}
/* Shorter fields (Tim, 11 Sep 2026: "these are too high"). They were 58px tall:
   15px of padding on a 16px font inheriting the body's 1.6 line height. Padding
   came down to 12px and the line height is pinned at 1.25, which lands them at
   46px. That is visibly shorter and still clears the 44px thumb target the phone
   standard asks for, so measure before cutting any further. */
.form input,.form select,.form textarea{width:100%;padding:12px 18px;border-radius:var(--r-sm);font:inherit;
  line-height:1.25;background:rgba(var(--wa),.08);border:1px solid var(--line-2);color:var(--ink)}
.form input:focus,.form select:focus,.form textarea:focus{outline:none;border-color:var(--green)}
.form ::placeholder{color:var(--ink-mute)}

/* ── progressive reveal form (.form.fr) ──────────────────────────────────
   Fields arrive in steps: name first, then contact, then business details,
   then the submit. Every collapse rule is scoped to html.js, so a visitor
   with no JS just sees the whole form at once and can still submit it. */
.fr-legend{display:block;font-size:.68rem;font-weight:700;letter-spacing:.11em;text-transform:uppercase;
  color:var(--green);margin:0}
/* A group of fields that is NEVER collapsed — the short form variant
   (partials/lead-form-short.html) opens with its fields already on screen, so
   they cannot sit in a .fr-step. Grid gap is safe here precisely because this
   group is always visible, which is the reason the collapsing steps above use
   per-child margin instead. */
.fr-fields{display:grid;gap:14px;min-width:0}
/* Spacing is per-child margin, not grid gap: a gap still applies around a
   collapsed step, so a hidden step would leave a 14px hole. Every rule below
   is scoped `form.fr > …` so they all carry the same specificity — with the
   collapsed rule written any weaker, `form.fr > *` wins and the hidden steps
   keep their margin (three of them = 42px of dead space under the name field). */
html.js form.fr{gap:0}
html.js form.fr > *{margin-bottom:14px}
form.fr > .fr-step{display:grid;gap:14px;min-width:0}
html.js form.fr > .fr-step{overflow:hidden;visibility:hidden;max-height:0;opacity:0;
  margin-bottom:0;transition:max-height .45s ease,opacity .3s ease,margin-bottom .45s ease}
html.js form.fr > .fr-step.fr-in{visibility:visible;max-height:600px;opacity:1;margin-bottom:14px}
/* The desktop rotating quiz (Tim, 2 Sep 2026). The stage after the one being
   answered peeks FADED underneath, clickable as one target; an answered stage
   gets .fr-done and dissolves to nothing so the next slides up into its place.
   Both states only ever exist on desktop, sc.js never sets them on phones. */
html.js form.fr > .fr-step.fr-peek{visibility:visible;max-height:600px;opacity:.7;
  margin-bottom:14px;cursor:pointer}
html.js form.fr > .fr-step.fr-peek > *{pointer-events:none}
html.js form.fr > .fr-step.fr-done{visibility:hidden;max-height:0;opacity:0;margin-bottom:0}
/* The saved answers strip (Tim, 16 Sep 2026): every answered stage as a small
   chip above the active field, label then value, and each chip is a button
   that reopens its stage for editing. sc.js builds it, desktop only. */
html.js form.fr > .fr-saved{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}
html.js form.fr > .fr-saved[hidden]{display:none}
.fr-chip{display:inline-flex;align-items:center;gap:8px;min-width:0;max-width:100%;padding:6px 12px 6px 11px;
  border-radius:999px;border:1px solid var(--line-2);background:rgba(var(--wa),.06);color:var(--ink);
  font:inherit;font-size:.8rem;line-height:1.2;cursor:pointer;text-align:left;
  transition:border-color .2s,background .2s}
.fr-chip i{font-style:normal;font-size:.66rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;
  color:var(--ink-mute);white-space:nowrap}
.fr-chip b{font-weight:600;min-width:0;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fr-chip::after{content:"";width:12px;height:12px;flex:none;opacity:.55;
  background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 20h9'/%3E%3Cpath d='M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z'/%3E%3C/svg%3E") center/contain no-repeat;
  mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 20h9'/%3E%3Cpath d='M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z'/%3E%3C/svg%3E") center/contain no-repeat}
.fr-chip:hover,.fr-chip:focus-visible{border-color:var(--green);background:rgba(50,207,117,.1);outline:none}
.fr-chip:hover::after,.fr-chip:focus-visible::after{opacity:1;color:var(--green)}
html.js form.fr > :last-child{margin-bottom:0}
/* The percentage bar over the fields. Out on 7 Sep 2026, back on 13 Sep 2026,
   and out again on 14 Sep 2026 with the rest of the panel head (Tim: "remove it
   all. Just have the form."). UNUSED as it stands — no SWF markup carries
   .swf-progress any more — and kept only so bringing it back is putting the
   three spans back in partials/lead-form.html. sc.js still fills it from
   [data-swf-fill] if it ever finds one. Desktop only, and JS only. */
.swf-progress{display:none;align-items:center;gap:10px;margin-bottom:14px}
.swf-bar{flex:1;height:6px;border-radius:999px;background:rgba(var(--wa),.12);overflow:hidden}
.swf-bar i{display:block;height:100%;width:0;background:var(--green);border-radius:999px;
  transition:width .5s ease}
.swf-pct{font-size:.7rem;font-weight:700;letter-spacing:.02em;color:var(--ink-soft);white-space:nowrap}
@media(min-width:901px){
  html.js .swf-progress{display:flex}
  /* Full bleed rotator on desktop: the photos line up with the card's own
     edges (Tim, 2 Sep 2026), pulled out over the .hq-panel padding with the
     panel's 1px border left showing. */
  .hq-panel .swf-slides{margin:-25px -23px 0;border:0;box-shadow:none;
    border-radius:calc(var(--r) - 2px) calc(var(--r) - 2px) 0 0}
}
/* "I don't have one" — a rounded glass pill, not a raw checkbox. The native box
   is kept in the DOM (still focusable, still the thing JS reads) but hidden off
   the pill, and .fr-tick draws the state. `:has()` styles it for every current
   browser; sc.js also mirrors the state onto .on/.focus so older engines and any
   programmatic toggle still light it up. */
.fr-check{display:inline-flex;align-items:center;justify-self:start;gap:9px;margin:0;
  padding:9px 16px 9px 12px;border-radius:999px;font-size:.85rem;font-weight:600;
  color:var(--ink-soft);background:rgba(var(--wa),.06);border:1px solid var(--line-2);
  -webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur);
  cursor:pointer;user-select:none;transition:background .2s,border-color .2s,color .2s}
.fr-check:hover{background:rgba(var(--wa),.1);color:var(--ink)}
.fr-check input{position:absolute;width:1px;height:1px;padding:0;margin:0;opacity:0;pointer-events:none}
.fr-tick{width:18px;height:18px;flex:0 0 18px;display:grid;place-items:center;border-radius:50%;
  border:1px solid var(--line-2);background:rgba(var(--wa),.05);color:#04130a;
  transition:background .2s,border-color .2s,transform .2s}
.fr-tick svg{width:11px;height:11px;opacity:0;transform:scale(.5);transition:opacity .2s,transform .2s}
.fr-check.on,.fr-check:has(input:checked){background:var(--green-soft);border-color:rgba(50,207,117,.45);color:var(--green)}
.fr-check.on .fr-tick,.fr-check:has(input:checked) .fr-tick{background:var(--green);border-color:var(--green)}
.fr-check.on .fr-tick svg,.fr-check:has(input:checked) .fr-tick svg{opacity:1;transform:none}
.fr-check.focus,.fr-check:has(input:focus-visible){border-color:var(--green);color:var(--ink)}
/* APP 5 collection notice — one quiet line under Send, deliberately small so it
   informs without becoming the wall of text it replaced. */
.fr-note{margin:2px 0 0;font-size:.68rem;line-height:1.5;text-align:center;color:var(--ink-mute)}
/* The Privacy Policy link is brand green, not the grey of the note around it
   (Tim, 7 Oct 2026: "make sure this is more seen, colour"). The deeper green
   on light ground keeps it readable on a white card. */
.fr-note a{color:#32cf75;font-weight:600;text-decoration:underline;text-underline-offset:2px}
.fr-note a:hover{color:#7df0ae}
[data-theme="light"] .fr-note a{color:#12a155}
[data-theme="light"] .fr-note a:hover{color:#0c7f42}
[data-theme="light"] .hero:has(.hero-wall) .fr-note a{color:#32cf75}
[data-theme="light"] .hero:has(.hero-wall) .fr-note a:hover{color:#7df0ae}
/* .fr-note-white — the APP 5 collection notice in Tim's own words (7 Sep 2026).
   It tells people exactly how he will contact them and how to stop it, so it is
   set in white rather than the muted ink the quiet legal line used to wear. */
.fr-note-white{color:#fff;margin-top:10px}
.fr-note-white + .fr-note{margin-top:6px}
.form .fr-step input:disabled{opacity:.4}
@media(prefers-reduced-motion:reduce){html.js .fr-step{transition:none}
  .fr-check,.fr-tick,.fr-tick svg{transition:none}}

/* ── the quiz form (.hq) ─────────────────────────────────────────────────
   EVERY lead form on the site is this: two tapped questions, then a tailored
   result with the contact fields under it. The markup lives in
   partials/lead-form.html and is stamped out by `npm run form`.

   .hq is the mechanics only — steps, progress, options, result, footer. The
   liquid-glass CARD is a separate .hq-panel modifier, because only the
   homepage hero needs one: every other form on the site already sits inside a
   .panel or the modal sheet, and a card in a card reads as a mistake.

   No-JS safe the same way form.fr is: every step-hiding rule is scoped to
   html.js, so without the script all three steps render as ordinary
   fieldsets and the form still submits. */
.hq{display:block}

/* the hero's own glass card */
.hq-panel{position:relative;border-radius:var(--r);padding:26px 24px 22px;
  background:linear-gradient(165deg,rgba(var(--wa),.075),rgba(var(--wa),.025));
  border:1px solid var(--line-2);overflow:hidden;
  -webkit-backdrop-filter:blur(30px) saturate(165%);backdrop-filter:blur(30px) saturate(165%);
  box-shadow:0 24px 60px rgba(0,0,0,.45),inset 0 1px 0 rgba(var(--wa),.14)}
/* specular sheen from the top left, the house glass treatment */
.hq-panel::before{content:"";position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(120% 90% at 0% 0%,rgba(var(--wa),.12),transparent 55%)}
/* green rim along the bottom edge */
.hq-panel::after{content:"";position:absolute;inset:0;pointer-events:none;border-radius:inherit;
  padding:1px;background:linear-gradient(180deg,transparent 55%,rgba(50,207,117,.5));
  -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  mask-composite:exclude}
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .hq-panel{background:#0d1512}
}
.hq-panel > *{position:relative;z-index:1}

/* Who is asking, above the first field. A number is handed to a person, not to
   a form, so the person is shown before the ask rather than after it. */
/* Whose form this is, before whose face it is. The mark sits above the intro
   row so the card is identifiably Scalus at a glance (Tim, 25 Aug 2026). */
/* THE HEAD OF THE FORM (Tim, 31 Aug 2026): mark, photograph, caption, ask.
   Centred as a group, because the mark is centred and a centred logo over a
   left aligned name reads as a mistake. The FIELDS below stay left aligned —
   centred labels over left aligned inputs is the actual mistake.

   What this replaced: a 76px circular avatar in a flex row beside three lines of
   copy, with a separate short version of that copy for phones. All of it is
   gone. See partials/lead-form.html for why. */
.hq-brand{display:block;width:auto;height:26px;margin:0 auto 14px;opacity:.92}

/* The photograph, near the full width of the card. It is the reassurance the
   three lines of copy used to be, so it is allowed to take the room they did.

   aspect-ratio + object-fit, not a free height: the source is 16:9 and the card
   is not, so without a declared box the image sets its own height and the form
   below it jumps when it loads. 16/9 is also the NATIVE ratio here, so nothing
   is cropped — the rule is a contract about the space, not a crop.
   (See feedback_never_crop_faces_out_of_images: pick a ratio the source already
   is, and there is no chance of taking a face off the edge of it.) */
.hq-face{margin:0 0 12px}
.hq-face img{display:block;width:100%;height:auto;aspect-ratio:16/9;object-fit:cover;
  border-radius:calc(var(--r) - 8px);border:1px solid var(--line-2);
  box-shadow:0 10px 30px rgba(0,0,0,.35)}
.hq-face figcaption{margin-top:12px;text-align:center;font-size:.86rem;font-weight:700;
  letter-spacing:-.01em;line-height:1.35;color:var(--ink)}

.hq-title{text-align:center;font-size:1.22rem;font-weight:800;letter-spacing:-.02em;
  line-height:1.25;color:var(--ink);margin:0 0 16px}
/* .hq-sub — the one line under "Request a call" saying what the call is.
   Only the short variant carries it; the title's 16px gap moves below the sub. */
.hq-title:has(+ .hq-sub){margin-bottom:6px}
.hq-sub{text-align:center;font-size:.9rem;line-height:1.5;color:var(--ink-soft);
  margin:0 auto 16px;max-width:34ch}
/* SWF photo rotator, the fading stack of Tim with clients at the top of the
   Scalus Website Form. The slides box owns the card look the single photo used
   to carry, each slide fades via .on from sc.js, and the first slide ships
   with .on hardcoded so the form is a normal single photo without JS. Every
   image is PRE CROPPED to 1200x675 with faces verified in frame. */
.swf-slides{position:relative;aspect-ratio:16/9;overflow:hidden;
  border-radius:calc(var(--r) - 8px);border:1px solid var(--line-2);
  box-shadow:0 10px 30px rgba(0,0,0,.35)}
.swf-slide{position:absolute;inset:0;opacity:0;transition:opacity .9s ease}
.swf-slide.on{opacity:1}
/* A site wall: four phones side by side. Tim, 17 Sep 2026: "these all need to
   start at the top of the websites" and "ken burns effects". They used to pan
   down the capture on a stagger, which meant three of the four opened halfway
   down a page with no header in sight. Now every phone opens on the top of its
   site and pushes slowly in, origin at the top edge so the header stays in
   frame the whole way. The stagger lives in the duration and the negative
   delays, so the four never breathe in step. Only the slide on screen moves. */
.swf-wall{display:flex;gap:4.2%;padding:5% 6% 0;align-items:flex-start;
  background:radial-gradient(120% 90% at 50% 0%,rgba(50,207,117,.16),transparent 62%),#0b100d}
.swf-ph{flex:1 1 0;min-width:0;aspect-ratio:9/17.5;border-radius:13px;overflow:hidden;
  background:#111;box-shadow:0 0 0 3px #1c1f1e,0 0 0 4px rgba(255,255,255,.12),0 18px 30px -14px rgba(0,0,0,.8)}
/* The frame crops these with object-fit:cover (see .hq-face .swf-slide img
   below, which outranks this rule), so the crop has to be anchored to the top
   of the capture or the phone opens on the middle of the page. That is what it
   was doing: object-position defaults to centre, and the middle of a 2769px
   capture is a long way from anybody's header. */
.swf-ph img{display:block;width:100%;height:auto;object-position:50% 0;transform-origin:50% 0;
  animation:swfKen 22s ease-in-out infinite alternate;animation-play-state:paused}
.swf-slide.on .swf-ph img{animation-play-state:running}
.swf-ph:nth-child(2) img{animation-duration:26s;animation-delay:-6s}
.swf-ph:nth-child(3) img{animation-duration:19s;animation-delay:-13s}
.swf-ph:nth-child(4) img{animation-duration:29s;animation-delay:-3s}
@keyframes swfKen{from{transform:scale(1) translate3d(0,0,0)}to{transform:scale(1.17) translate3d(0,-1.1%,0)}}
@media(prefers-reduced-motion:reduce){.swf-ph img{animation:none}}
.swf-slide picture{display:block;width:100%;height:100%}
/* Live site loops over the full screen site stills (Tim, 1 Oct 2026). */
.swf-live{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:50% 0;display:block}
.hq-face .swf-slide img{width:100%;height:100%;aspect-ratio:auto;object-fit:cover;
  border:0;border-radius:0;box-shadow:none}
.hq-face .swf-wall .swf-ph img{object-position:50% 0}
/* .swf-cap — what the photo actually shows (Tim, 11 Sep 2026), sitting on a
   gradient that pulls down into the bottom of the frame. It replaces the 2 Sep
   chip (.swf-tag), which he had taken off for being a floating pill; a scrim
   reads as part of the photo instead of a badge stuck on it. The gradient is
   tall enough that the text never lands on bare image, whatever the slide. */
.swf-cap{position:absolute;left:0;right:0;bottom:0;z-index:1;
  padding:34px 14px 11px;font-size:.76rem;font-weight:600;line-height:1.3;
  color:#fff;text-shadow:0 1px 3px rgba(0,0,0,.55);
  background:linear-gradient(to top,rgba(3,9,6,.88),rgba(3,9,6,.55) 42%,rgba(3,9,6,0))}
@media(max-width:640px){.swf-cap{font-size:.72rem;padding:30px 12px 10px}}
@media(prefers-reduced-motion:reduce){.swf-slide{transition:none}}

/* ── .swf-film — the welcome video, the top layer of the SWF frame (Tim, 14 Sep
   2026). It sits INSIDE .swf-slides, so it inherits the frame's radius, border
   and shadow and there is nothing to keep in sync: one frame, two things that
   can be in it. It starts on top of the photos and, once it has been played
   through with sound, fades out and hands the frame back to them for good.
   object-fit:cover for the same reason the photos use it — the source is 16:9
   and so is the frame, so nothing is ever cropped off a face. */
.swf-film{position:absolute;inset:0;z-index:2;background:#03090a;
  opacity:1;transition:opacity .9s ease}
.swf-film.is-out{opacity:0;pointer-events:none}
/* The whole film is the unmute control now (Tim, 15 Sep 2026), so it has to
   look like something you can press. The button inside it stays: it is what
   carries the label and the keyboard focus. */
.swf-film{cursor:pointer}
.hq-face .swf-film video{display:block;width:100%;height:100%;aspect-ratio:auto;
  object-fit:cover;border:0;border-radius:0;box-shadow:none;background:#03090a}
/* The Unmute control. TOP right, and it has to stay there: the film carries its
   own burned in captions and its "Tim, Local Website Developer" tag across the
   MIDDLE band of the frame, and .swf-cap owns the bottom of every photo slide.
   The top strip is the only part of this frame that is clear for the whole 23
   seconds, and on a phone, where the frame is short, a pill at the bottom lands
   straight on the name tag. Glass over the film rather than solid, with the
   house green on the icon so it reads as the one thing worth touching. */
.swf-sound{position:absolute;right:10px;top:10px;z-index:3;
  display:inline-flex;align-items:center;gap:7px;
  padding:8px 14px 8px 10px;border-radius:999px;
  font-family:inherit;font-size:.78rem;font-weight:700;letter-spacing:-.01em;
  color:#fff;background:rgba(3,9,6,.58);border:1px solid rgba(255,255,255,.24);
  -webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur);
  -webkit-appearance:none;appearance:none;cursor:pointer;
  box-shadow:0 6px 20px rgba(0,0,0,.35);
  transition:background .2s,border-color .2s,transform .2s}
.swf-sound:hover{background:rgba(3,9,6,.74);border-color:rgba(50,207,117,.5);transform:translateY(-1px)}
.swf-sound:focus-visible{outline:2px solid var(--green);outline-offset:3px}
.swf-sound-i{display:grid;place-items:center;width:22px;height:22px;flex:none;
  border-radius:50%;background:var(--green);color:#04130a}
.swf-sound-i svg{display:block}
@media(max-width:640px){
  .swf-sound{right:8px;top:8px;padding:7px 12px 7px 9px;font-size:.74rem}
  .swf-sound-i{width:20px;height:20px}
}
@media(prefers-reduced-motion:reduce){.swf-film{transition:none}.swf-sound{transition:none}}

/* ── .swf-enquire — the standing Enquire button (Tim, 14 Sep 2026: "Enquire
   button greyed out at the bottom at all times ... so they know its to
   enquire"). It replaced Send and it lives outside the .fr-step stack, so the
   desktop rotating quiz can never collapse it: the bottom of the card always
   says what the card is for. .is-off is the grey resting state; sc.js drops the
   class and the disabled flag the moment first name and mobile are answered,
   and the button lights up as the normal green .btn-send. It ships WITH .is-off
   and WITHOUT disabled, so a visitor running no JS sees the same grey button
   and it still submits. */
.swf-enquire{margin-top:14px}
.swf-enquire.is-off{background:rgba(var(--wa),.09);background-size:100% 100%;
  animation:none;color:var(--ink-mute);border-color:var(--line-2);
  box-shadow:none;filter:none;cursor:default}
.swf-enquire.is-off:hover{background:rgba(var(--wa),.09);color:var(--ink-mute);
  box-shadow:none;filter:none}

/* .hq-cred — the credential lines under the photo on the SWF, deliberately
   quieter than the old single bold white caption (Tim, 2 Sep 2026). Since
   11 Sep 2026 the first of them is the form's heading: the green Enquire pill
   came out and "Enquire with Tim" took its place, so .hq-cred-lead carries the
   ink and the size the pill used to, with "Website developer" and the suburb
   staying quiet underneath it. */
.hq-face figcaption.hq-cred{text-align:left;color:var(--ink-soft);font-weight:600;
  font-size:.88rem;line-height:1.5;letter-spacing:0}
.hq-face figcaption.hq-cred .hq-cred-lead{display:flex;align-items:center;gap:11px;
  margin-bottom:2px;
  color:var(--ink);font-size:1.62rem;font-weight:800;letter-spacing:-.02em;
  line-height:1.2}
/* The green circle arrow beside "Enquire" (Tim, 13 Sep 2026). Same mark, same
   green and same dark ink as the "Click to enquire" card on the reels, so a
   viewer who came off a video meets the shape they just tapped. Decorative
   only, the heading is not a control, so the svg is aria-hidden. */
.hq-face figcaption.hq-cred .hq-cred-arrow{flex:none;display:grid;place-items:center;
  width:32px;height:32px;border-radius:50%;background:var(--green);
  box-shadow:0 0 18px rgba(50,207,117,.42),inset 0 1px 0 rgba(var(--wa),.45)}
.hq-face figcaption.hq-cred .hq-cred-arrow svg{display:block}
/* The signature and the ABN under the promise line (Tim, 13 Sep 2026). Both are
   their own block so the caption reads as three steps down: the offer, who is
   making it, and the proof the business is real. The ABN is the quietest thing
   in the block on purpose, it reassures and it does not compete. */
.hq-face figcaption.hq-cred .hq-cred-sig{display:block;margin-top:7px;
  color:var(--ink);font-weight:700}
.hq-face figcaption.hq-cred .hq-cred-abn{display:block;margin-top:1px;
  color:var(--ink-soft);opacity:.72;font-size:.78rem;font-weight:600}
/* With the pill and its sub line gone the caption now runs straight into the
   first field, so the photo block carries the air those two used to give. */
html.js form.fr > .hq-face,form.fr > .hq-face{margin-bottom:20px}
/* .hq-pill — "Enquire" sits bigger inside a green pill (Tim, 2 Sep 2026).
   SOLID green with black ink since 3 Sep 2026: on a phone this pill is the
   control that opens the fields, so it has to read as a button and not as a
   tinted heading. It ships as a real <button>, hence the font and appearance
   reset — a button does not inherit either. */
.hq-title.hq-pill{display:block;width:fit-content;justify-self:center;
  margin:4px auto 12px;padding:12px 26px;border-radius:999px;
  background:var(--green);border:1px solid var(--green);color:#04130a;
  font-family:inherit;font-size:1.42rem;font-weight:800;letter-spacing:-.02em;
  -webkit-appearance:none;appearance:none;cursor:pointer;
  box-shadow:0 10px 30px -14px rgba(50,207,117,.6);
  transition:filter .2s,box-shadow .2s;
  /* "Enquire about a new website" (Tim, 7 Sep 2026) is a sentence, not a word, so
     the pill has to be able to wrap inside the card instead of pushing past it. */
  max-width:100%;text-align:center;line-height:1.25;text-wrap:balance}
.hq-title.hq-pill:hover{filter:brightness(1.06);box-shadow:0 12px 34px -12px rgba(50,207,117,.7)}
.hq-title.hq-pill:focus-visible{outline:2px solid var(--green);outline-offset:3px}
.hq-title.hq-pill:has(+ .hq-sub){margin-bottom:12px}
/* The shut phone form (Tim, 3 Sep 2026). Everything under the sub line stays
   out of the layout until Enquire is tapped, so the first screen is the photo,
   who I am and one green button rather than six fields. sc.js sets .swf-shut
   at boot on phones only, and never on the form inside the modal sheet.
   display:none rather than the step transition, so nothing can peek through
   the collapse and no hidden field can be tabbed into. */
@media(max-width:900px){
  form.fr.swf-shut > .fr-step,
  form.fr.swf-shut > .swf-progress{display:none}
  form.fr.swf-shut > .hq-sub{margin-bottom:0}
}
@media(max-width:640px){
  .hq-brand{height:22px;margin-bottom:12px}
  .hq-face figcaption{font-size:.82rem}
  .hq-title{font-size:1.12rem;margin-bottom:14px}
  .hq-title:has(+ .hq-sub){margin-bottom:6px}
  .hq-sub{font-size:.86rem;margin-bottom:14px}
  .hq-face figcaption.hq-cred{font-size:.84rem}
  .hq-face figcaption.hq-cred .hq-cred-lead{font-size:1.42rem;gap:9px}
  .hq-face figcaption.hq-cred .hq-cred-arrow{width:28px;height:28px}
  .hq-face figcaption.hq-cred .hq-cred-arrow svg{width:13px;height:13px}
  .hq-title.hq-pill{font-size:1.08rem;padding:11px 20px;margin-bottom:10px}
}

/* Stage two receipt: the name and mobile they already gave, plus Edit. */
.hq-you[hidden]{display:none}
.hq-you{display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:var(--r-sm);
  background:var(--green-soft);border:1px solid rgba(50,207,117,.28)}
.hq-you-tick{flex:none;display:grid;place-items:center;width:20px;height:20px;border-radius:50%;
  background:var(--green);color:#04130a}
.hq-you-tick svg{width:12px;height:12px}
.hq-you-v{min-width:0;flex:1;display:flex;flex-wrap:wrap;align-items:baseline;gap:.15em .6em;
  font-size:.86rem;line-height:1.4}
.hq-you-v b{font-weight:700;color:var(--ink)}
.hq-you-v i{font-style:normal;color:var(--ink-soft)}
.hq-you-edit{margin:0;padding:0;border:0;background:none;cursor:pointer;font:inherit;flex:none;
  font-size:.78rem;font-weight:700;color:var(--green);text-decoration:underline;
  text-underline-offset:3px;transition:color .2s}
.hq-you-edit:hover{color:var(--ink)}

.hq-top{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}
.hq-eyebrow{display:inline-flex;align-items:center;gap:.55em;font-size:.72rem;font-weight:700;
  letter-spacing:.12em;text-transform:uppercase;color:var(--green)}
.hq-eyebrow .dot{width:7px;height:7px;border-radius:50%;background:var(--green);animation:pulse 1.6s infinite}
.hq-count{font-size:.74rem;color:var(--ink-mute);font-weight:600;white-space:nowrap}
.hq-bar{height:3px;border-radius:999px;background:rgba(var(--wa),.1);overflow:hidden;margin-bottom:20px}
.hq-bar i{display:block;height:100%;width:25%;border-radius:inherit;background:var(--green);
  transition:width .45s cubic-bezier(.22,1,.36,1)}

.hq-step{border:0;margin:0;padding:0;min-width:0}
.hq-q{display:block;padding:0;margin:0 0 4px;font-size:1.18rem;font-weight:800;line-height:1.3;color:var(--ink)}
.hq-hint{margin:0 0 16px;font-size:.84rem;color:var(--ink-soft)}

/* Option rows. The radio itself is hidden but still the thing that's checked,
   so keyboard and no-JS both keep working. */
.hq-opt{display:flex;align-items:center;gap:12px;padding:14px 16px;margin-bottom:10px;
  border-radius:var(--r-sm);cursor:pointer;font-size:.95rem;font-weight:600;color:var(--ink-soft);
  background:rgba(var(--wa),.05);border:1px solid var(--line-2);
  transition:background .18s,border-color .18s,color .18s,transform .18s}
.hq-opt:last-of-type{margin-bottom:0}
.hq-opt:hover{background:rgba(var(--wa),.1);color:var(--ink)}
.hq-opt input{position:absolute;width:1px;height:1px;opacity:0;margin:0;padding:0;pointer-events:none}
/* the ring/dot that shows the choice */
.hq-opt span{position:relative;padding-left:30px;display:block;min-width:0}
.hq-opt span::before{content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);
  width:18px;height:18px;border-radius:50%;border:1px solid var(--line-2);background:rgba(var(--wa),.05);
  transition:border-color .18s,background .18s}
.hq-opt span::after{content:"";position:absolute;left:5px;top:50%;width:8px;height:8px;border-radius:50%;
  background:#04130a;transform:translate(0,-50%) scale(.4);opacity:0;transition:opacity .18s,transform .18s}
.hq-opt:has(input:checked){background:var(--green-soft);border-color:rgba(50,207,117,.45);color:var(--green)}
.hq-opt:has(input:checked) span::before{background:var(--green);border-color:var(--green)}
.hq-opt:has(input:checked) span::after{opacity:1;transform:translate(0,-50%) scale(1)}
.hq-opt:has(input:focus-visible){border-color:var(--green)}
/* .on/.focus mirror the same states for engines without :has (sc.js sets them) */
.hq-opt.on{background:var(--green-soft);border-color:rgba(50,207,117,.45);color:var(--green)}
.hq-opt.on span::before{background:var(--green);border-color:var(--green)}
.hq-opt.on span::after{opacity:1;transform:translate(0,-50%) scale(1)}
.hq-opt.focus{border-color:var(--green)}

/* last step: the tailored result, then the contact fields */
.hq-last{display:grid;gap:14px}
.hq-result{padding:16px 18px;border-radius:var(--r-sm);background:var(--green-soft);
  border:1px solid rgba(50,207,117,.28)}
.hq-badge{display:inline-block;padding:5px 12px;border-radius:999px;background:rgba(50,207,117,.2);
  color:var(--green);font-size:.68rem;font-weight:800;letter-spacing:.1em;text-transform:uppercase}
.hq-rtitle{margin:12px 0 8px;font-size:1.16rem;font-weight:800;line-height:1.3;color:var(--ink)}
.hq-rlead{margin:0;font-size:.88rem;line-height:1.6;color:var(--ink-soft)}
/* Who is actually going to call them, under the answer. A face here does the
   job the old four point list was doing badly: it says a person is on the
   other end, right where they are deciding whether to hand over a number. */
.hq-by{display:flex;align-items:center;gap:11px;margin-top:14px}
.hq-by img{width:44px;height:44px;flex:0 0 44px;border-radius:50%;object-fit:cover}
.hq-by strong{display:block;font-size:.9rem;font-weight:700;color:var(--ink);line-height:1.35}
.hq-by span{display:block;font-size:.82rem;color:var(--ink-soft);line-height:1.35}
.hq .form-legend,.hq .fr-legend{margin-top:2px}
.hq input[type=text],.hq input[type=tel],.hq input[type=email]{width:100%;padding:15px 18px;
  border-radius:var(--r-sm);font:inherit;background:rgba(var(--wa),.08);
  border:1px solid var(--line-2);color:var(--ink)}
.hq input:focus{outline:none;border-color:var(--green)}
.hq ::placeholder{color:var(--ink-mute)}

/* Back on the left, skip on the right. The skip is the escape hatch for anyone
   who does not want to answer anything: the questions sharpen the callback, they
   were never meant to be a gate in front of the contact fields. */
.hq-foot{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:16px;min-height:20px}
.hq-back,.hq-skip{margin:0;padding:0;border:0;background:none;cursor:pointer;font:inherit;
  font-size:.82rem;font-weight:600;color:var(--ink-mute);transition:color .2s}
.hq-back:hover,.hq-skip:hover{color:var(--ink)}
.hq-skip{margin-left:auto;text-decoration:underline;text-underline-offset:3px}

/* ── the quiz IS the progressive form (.form.fr.hq) ──────────────────────
   The contact step keeps exactly the .fr behaviour it had as a standalone
   form: name, then mobile, then business details and Send together. .fr's own
   rules are scoped `form.fr > .fr-step`, and here those steps sit one level
   deeper inside .hq-last, so they are restated for it.

   Same gap:0 + per-child-margin reasoning as .fr above: a grid gap still
   applies around a collapsed step, so a hidden step would leave a 14px hole.
   The top row and progress bar keep their own tighter spacing, which the
   blanket `form.fr > *` margin would otherwise override. */
html.js form.fr.hq > .hq-brand{margin-bottom:14px}
html.js form.fr.hq > .hq-title{margin-bottom:16px}
html.js form.fr.hq > .hq-face{margin-bottom:12px}
html.js form.fr.hq > .hq-top{margin-bottom:12px}
html.js form.fr.hq > .hq-bar{margin-bottom:20px}
html.js form.fr > .hq-last{gap:0}
html.js form.fr > .hq-last > *{margin-bottom:14px}
html.js form.fr > .hq-last > .fr-step{display:grid;gap:14px;overflow:hidden;visibility:hidden;max-height:0;opacity:0;
  margin-bottom:0;transition:max-height .45s ease,opacity .3s ease,margin-bottom .45s ease}
html.js form.fr > .hq-last > .fr-step.fr-in{visibility:visible;max-height:600px;opacity:1;margin-bottom:14px}
html.js form.fr > .hq-last > :last-child{margin-bottom:0}

/* Stage one is a plain pair of fields, so it needs the grid + gap the .fr steps
   already carry — without it the name and mobile inputs sit flush together. */
html.js form.fr > .hq-fields{gap:0}
html.js form.fr > .hq-fields > *{margin-bottom:14px}
html.js form.fr > .hq-fields > :last-child{margin-bottom:0}

/* one step at a time, JS only */
html.js .hq > .hq-step{display:none}
html.js .hq > .hq-step.hq-in{display:block}
html.js .hq > .hq-step.hq-in.hq-last,
html.js .hq > .hq-step.hq-in.hq-fields{display:grid}
html.js .hq > .hq-step.hq-in{animation:hqIn .38s cubic-bezier(.22,1,.36,1) both}
@keyframes hqIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
@media(prefers-reduced-motion:reduce){html.js .hq > .hq-step.hq-in{animation:none}
  .hq-opt,.hq-bar i{transition:none}}

@media(max-width:900px){
  .hq-panel{padding:22px 18px 18px}
  .hq-q{font-size:1.06rem}
  .hq-opt{padding:13px 14px;font-size:.9rem}
  /* On phones the quiz stacks under the hero copy, and the hero's twin CTAs are
     hidden: "Get in contact" is exactly what the quiz is, and leaving both there
     pushed the quiz ~200px further down. The SEO check is still one tap away in
     the nav sheet, so nothing becomes unreachable. */
  [data-quiz-hero] .cta-row{display:none}
  [data-quiz-hero] .lead{margin-bottom:6px}
}

.thanks{text-align:center;padding:30px 10px}
.thanks .seal{width:62px;height:62px;border-radius:50%;margin:0 auto 18px;display:grid;place-items:center;
  background:var(--green-soft);border:1px solid rgba(50,207,117,.4);color:var(--green)}
.thanks .seal svg{width:28px;height:28px}

/* Sent confirmation: the drawn-on green tick from the onboarding form splash,
   reused as the lead form's success state. The circle draws, the check strokes
   in behind it, then the words fade up. Since the enquirer is no longer texted
   an auto-reply, THIS is the receipt, so it has to land properly. */
.thanks .tick{width:74px;height:74px;margin:0 auto 18px;display:block}
.thanks .tick svg{width:100%;height:100%;transform:scale(.6);opacity:0;
  animation:scTickPop .45s cubic-bezier(.2,1.3,.4,1) forwards .1s}
.thanks .tick-c{fill:none;stroke:var(--green);stroke-width:2.5;
  stroke-dasharray:151;stroke-dashoffset:151;animation:scTickDraw .6s ease forwards .25s}
.thanks .tick-p{fill:none;stroke:var(--green);stroke-width:3.6;stroke-linecap:round;stroke-linejoin:round;
  stroke-dasharray:40;stroke-dashoffset:40;animation:scTickDraw .4s cubic-bezier(.65,0,.45,1) forwards .7s}
.thanks .t-in{opacity:0;animation:scThanksIn .5s ease forwards}
.thanks h3.t-in{animation-delay:.85s}
.thanks p.t-in{animation-delay:1.05s}
/* Tim's face plus his line, so the confirmation comes from a person and not
   from a system. Photo left, words right, the pair centred as one block. */
.thanks .from-tim{display:inline-flex;align-items:center;gap:14px;text-align:left;
  margin-top:2px;animation-delay:1.05s;max-width:100%}
.thanks .from-tim img{width:56px;height:56px;border-radius:50%;flex:0 0 56px;object-fit:cover;
  border:2px solid rgba(50,207,117,.55);box-shadow:0 6px 18px rgba(0,0,0,.35)}
.thanks .from-tim .ft-msg{display:block;color:var(--ink);font-weight:600;line-height:1.35}
.thanks .from-tim .ft-who{display:block;margin-top:3px;font-size:.85rem;color:var(--ink-mute)}
@media(max-width:420px){
  .thanks .from-tim{gap:12px}
  .thanks .from-tim img{width:48px;height:48px;flex:0 0 48px}
  .thanks .from-tim .ft-msg{font-size:.95rem}
}
@keyframes scTickPop{to{transform:scale(1);opacity:1}}
@keyframes scTickDraw{to{stroke-dashoffset:0}}
@keyframes scThanksIn{from{opacity:0;transform:translateY(9px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion: reduce){
  .thanks .tick svg{animation:none;transform:none;opacity:1}
  .thanks .tick-c,.thanks .tick-p{animation:none;stroke-dashoffset:0}
  .thanks .t-in{animation:none;opacity:1}
}

/* ── booking picker (on-brand date + time → GHL calendar) ───────────────── */
.bk{display:block}
.bk-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}
.bk-month{font-size:1.05rem;font-weight:700;color:var(--ink)}
.bk-nav{display:flex;gap:8px}
.bk-arrow{width:38px;height:38px;border-radius:11px;border:1px solid var(--line-2);
  background:rgba(var(--wa),.05);color:var(--ink);font-size:1.2rem;line-height:1;cursor:pointer;
  display:grid;place-items:center;transition:.2s}
.bk-arrow:hover:not(:disabled){border-color:var(--green);color:var(--green)}
.bk-arrow:disabled{opacity:.3;cursor:default}
.bk-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:6px}
.bk-dow{text-align:center;font-size:.68rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
  color:var(--ink-mute);padding:4px 0 8px}
.bk-pad{aspect-ratio:1}
.bk-day{aspect-ratio:1;border:1px solid var(--line);border-radius:11px;background:rgba(var(--wa),.03);
  color:var(--ink-soft);font:inherit;font-size:.9rem;font-weight:600;cursor:pointer;transition:.16s;
  display:grid;place-items:center}
.bk-day:hover:not(:disabled){border-color:var(--green);color:var(--ink);background:var(--green-soft)}
.bk-day.is-today{box-shadow:inset 0 0 0 1px var(--green-soft)}
.bk-day.sel{background:var(--green);border-color:var(--green);color:#04130a;font-weight:800;
  box-shadow:0 8px 22px rgba(50,207,117,.34)}
.bk-day.off,.bk-day:disabled{opacity:.28;cursor:default;background:transparent}
.bk-day.off:hover,.bk-day:disabled:hover{border-color:var(--line);color:var(--ink-soft);background:transparent}
.bk-slots{margin-top:22px;display:none}
.bk-slots.show{display:block;animation:bkfade .35s ease}
@keyframes bkfade{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.bk-slots-h{font-size:.82rem;font-weight:700;color:var(--ink-soft);margin-bottom:12px}
.bk-slots-h span{color:var(--green)}
.bk-slot-row{display:grid;grid-template-columns:repeat(auto-fill,minmax(92px,1fr));gap:9px}
.bk-slot{padding:11px 8px;border:1px solid var(--line-2);border-radius:11px;background:rgba(var(--wa),.04);
  color:var(--ink-soft);font:inherit;font-size:.86rem;font-weight:600;cursor:pointer;transition:.16s;text-align:center}
.bk-slot:hover{border-color:var(--green);color:var(--ink);background:var(--green-soft)}
.bk-slot.sel{background:var(--green);border-color:var(--green);color:#04130a;font-weight:800;
  box-shadow:0 8px 22px rgba(50,207,117,.34)}
.bk-loading{display:flex;align-items:center;gap:10px;color:var(--ink-mute);font-size:.88rem;padding:8px 0}
.bk-spin{width:16px;height:16px;border-radius:50%;border:2px solid var(--line-2);border-top-color:var(--green);
  animation:bkspin .7s linear infinite}
@keyframes bkspin{to{transform:rotate(360deg)}}
.bk-none{color:var(--ink-mute);font-size:.88rem;padding:6px 0 2px}
.bk-tz{font-size:.72rem;color:var(--ink-mute);margin:14px 0 0}
.bk-pre{display:grid;gap:18px;margin-bottom:26px}
.bk-opt-h{font-size:.82rem;font-weight:700;color:var(--ink-soft);margin-bottom:10px}
/* someone tried to pick a day without saying what the call is about */
.bk-pre.needs .bk-opt-grp:first-child .bk-opt-h{color:#ff6b5e}
.bk-pre.needs .bk-opt-row.wide{animation:bkNudge .45s ease}
@keyframes bkNudge{0%,100%{transform:none}25%{transform:translateX(-5px)}75%{transform:translateX(5px)}}
.bk-opt-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(78px,1fr));gap:9px}
.bk-opt-row.wide{grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}
.bk-opt{padding:13px 12px;border:1px solid var(--line-2);border-radius:11px;background:rgba(var(--wa),.04);
  color:var(--ink-soft);font:inherit;font-size:.88rem;font-weight:600;cursor:pointer;transition:.16s}
.bk-opt:hover{border-color:var(--green);color:var(--ink);background:var(--green-soft)}
.bk-opt.sel{background:var(--green);border-color:var(--green);color:#04130a;font-weight:800;
  box-shadow:0 6px 18px rgba(50,207,117,.28)}
/* two line topic button: who it is for on top, what the call is underneath */
.bk-opt-top{display:block;font-size:.7rem;font-weight:800;letter-spacing:.09em;text-transform:uppercase;
  color:var(--ink-mute);margin-bottom:5px;transition:color .16s}
.bk-opt-main{display:block;font-size:.92rem}
.bk-opt.sel .bk-opt-top{color:rgba(4,19,10,.72)}
/* the two doors carry their own colour from the start, orange for a new client
   and Scalus green for someone already with us, so the choice reads at a glance */
.bk-opt.tone-orange{border-color:rgba(255,77,0,.55)}
.bk-opt.tone-orange:hover{border-color:var(--orange);background:rgba(255,77,0,.12);color:var(--ink)}
.bk-opt.tone-orange .bk-opt-top{color:var(--orange)}
.bk-opt.tone-green{border-color:rgba(50,207,117,.55)}
.bk-opt.tone-green:hover{border-color:var(--green);background:var(--green-soft);color:var(--ink)}
.bk-opt.tone-green .bk-opt-top{color:var(--green)}
.bk-opt.tone-green.sel .bk-opt-top{color:rgba(4,19,10,.72)}
.bk-opt.tone-orange.sel{background:var(--orange);border-color:var(--orange);color:#1a0600;
  box-shadow:0 6px 18px rgba(255,77,0,.32)}
.bk-opt.tone-orange.sel .bk-opt-top{color:rgba(26,6,0,.75)}
.bk-sum-len{margin-left:auto;font-size:.78rem;font-weight:700;color:var(--green);
  border:1px solid rgba(50,207,117,.35);border-radius:999px;padding:3px 10px;white-space:nowrap}
.bk-confirm{margin-top:22px}
.bk-summary{display:flex;align-items:center;gap:10px;padding:14px 16px;border-radius:var(--r-sm);
  background:var(--green-soft);border:1px solid rgba(50,207,117,.3);color:var(--ink);font-size:.92rem;margin-bottom:14px}
.bk-summary b{color:var(--ink);font-weight:700}
.bk-cal-ic{color:var(--green);flex:none;display:grid;place-items:center}
.bk-cal-ic svg{width:18px;height:18px}
.bk-fields{display:grid;gap:10px;margin-bottom:14px}
.bk-in{width:100%;padding:14px 16px;border-radius:var(--r-sm);font:inherit;
  background:rgba(var(--wa),.08);border:1px solid var(--line-2);color:var(--ink)}
.bk-in:focus{outline:none;border-color:var(--green)}
.bk-in::placeholder{color:var(--ink-mute)}
textarea.bk-in{min-height:84px;resize:vertical;line-height:1.5}
/* /call: only the bottom padding is set here, the top comes from the house
   standard in the HERO TOP AIR block below. */
.hero-call{padding-bottom:60px}
@media(max-width:560px){.hero-call{padding-bottom:40px}}
/* /call: "what I can cover" list with Tim's photo in a rounded rectangle beside it */
.call-cover{display:flex;align-items:center;gap:22px}
.call-cover-txt{flex:1;min-width:0}
/* width is pinned to the photo so the caption wraps under it instead of
   stretching the column */
.call-cover-who{flex:none;width:120px;margin:0;text-align:center}
.call-cover-face{display:block;width:120px;height:132px;object-fit:cover;object-position:50% 22%;
  border-radius:18px;border:1px solid rgba(50,207,117,.35);
  box-shadow:0 18px 40px rgba(0,0,0,.45)}
.call-cover-who figcaption{margin-top:9px;font-size:.72rem;font-weight:700;letter-spacing:.02em;
  color:var(--ink-soft);white-space:nowrap}
@media(max-width:600px){
  .call-cover{gap:14px}
  .call-cover-who{width:92px}
  .call-cover-face{width:92px;height:104px;border-radius:14px}
  .call-cover-who figcaption{margin-top:7px;font-size:.66rem;line-height:1.35;white-space:normal}
}
/* "build me a concept" tick box on the confirm step */
.bk-check{position:relative;display:flex;align-items:flex-start;gap:12px;padding:14px 16px;border-radius:var(--r-sm);
  background:rgba(var(--wa),.05);border:1px solid var(--line-2);cursor:pointer;
  transition:border-color .2s,background .2s}
.bk-check:hover{border-color:rgba(50,207,117,.45)}
.bk-check.on{background:var(--green-soft);border-color:rgba(50,207,117,.5)}
.bk-check input{position:absolute;opacity:0;width:0;height:0}
.bk-check-box{flex:none;width:22px;height:22px;border-radius:7px;margin-top:1px;
  border:1px solid var(--line-2);background:rgba(var(--wa),.06);display:grid;place-items:center;
  color:#04130a;transition:background .2s,border-color .2s}
.bk-check-box svg{width:14px;height:14px;opacity:0;transform:scale(.6);transition:opacity .18s,transform .18s}
.bk-check.on .bk-check-box{background:var(--green);border-color:var(--green);
  box-shadow:0 6px 18px rgba(50,207,117,.28)}
.bk-check.on .bk-check-box svg{opacity:1;transform:none}
.bk-check-txt{display:block;font-size:.9rem;line-height:1.45}
.bk-check-txt b{display:block;color:var(--ink);font-weight:700}
.bk-check-txt i{display:block;margin-top:3px;font-style:normal;font-size:.82rem;color:var(--ink-mute)}
.bk-check input:focus-visible + .bk-check-box{outline:2px solid var(--green);outline-offset:2px}
.bk-go{width:100%}
.bk-err{color:#ff6b5e;font-size:.82rem;margin:12px 0 0}
.bk-done{padding:18px 6px}

/* booking modal (pops up after the onboarding form is submitted) */
.bk-modal{position:fixed;inset:0;z-index:1000;display:none;align-items:flex-start;justify-content:center;
  padding:40px 18px;overflow-y:auto;background:rgba(4,6,5,.72);backdrop-filter:blur(8px);
  -webkit-backdrop-filter:blur(8px);opacity:0;transition:opacity .3s}
.bk-modal.open{display:flex;opacity:1}
.bk-modal-card{width:100%;max-width:560px;margin:auto 0;background:var(--bg-2);border:1px solid var(--line-2);
  border-radius:var(--r);padding:34px 30px;position:relative;transform:translateY(18px) scale(.98);
  transition:transform .35s cubic-bezier(.2,.8,.2,1);box-shadow:0 40px 120px rgba(0,0,0,.6)}
.bk-modal.open .bk-modal-card{transform:none}
.bk-modal-x{position:absolute;top:16px;right:16px;width:34px;height:34px;border-radius:50%;
  border:1px solid var(--line-2);background:rgba(var(--wa),.05);color:var(--ink-soft);font-size:1.1rem;
  line-height:1;cursor:pointer;display:grid;place-items:center;transition:.2s}
.bk-modal-x:hover{border-color:var(--green);color:var(--green)}
.bk-modal-head{text-align:center;margin-bottom:26px}
.bk-modal-head .seal{width:54px;height:54px;border-radius:50%;margin:0 auto 16px;display:grid;place-items:center;
  background:var(--green-soft);border:1px solid rgba(50,207,117,.4);color:var(--green)}
.bk-modal-head .seal svg{width:26px;height:26px}
.bk-modal-head h3{margin:0 0 8px;font-size:1.3rem}
.bk-modal-head p{margin:0;color:var(--ink-soft);font-size:.92rem;max-width:380px;margin:0 auto}
@media(max-width:560px){.bk-modal{padding:0}.bk-modal-card{border-radius:0;min-height:100vh;padding:28px 20px}
  .bk-slot-row{grid-template-columns:repeat(auto-fill,minmax(80px,1fr))}}


/* picker (hub) */
.pick{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px;margin-top:52px}
.pcard{display:flex;flex-direction:column;text-decoration:none;color:var(--ink);overflow:hidden;transition:transform .35s, box-shadow .35s}
.pcard:hover{transform:translateY(-6px);box-shadow:0 36px 80px rgba(0,0,0,.6)}
.pbody{padding:28px 26px;display:flex;flex-direction:column;gap:10px;flex:1}
.pcard .tag{font-size:.7rem;font-weight:800;letter-spacing:.22em;text-transform:uppercase;color:var(--ink-mute)}
.pcard .best{align-self:flex-start;font-size:.68rem;font-weight:800;letter-spacing:.14em;text-transform:uppercase;
  background:var(--green);color:#04130a;padding:5px 12px;border-radius:999px}
.pcard p{color:var(--ink-soft);font-size:.92rem;flex:1}
.pcard .go{display:inline-flex;align-items:center;gap:.5em;color:var(--green);font-weight:700;font-size:.9rem;margin-top:6px}
.pimg-top{aspect-ratio:16/9;background:linear-gradient(135deg,#0e1411,#101a14);display:grid;place-items:center;overflow:hidden;border-bottom:1px solid var(--line)}
.pimg-top img{width:78%;height:auto;border-radius:10px;box-shadow:0 16px 40px rgba(0,0,0,.5)}

/* hero split */
/* align-items was center. In an 82vh box a hero shorter than the viewport got
   floated down the middle, which is the dead air under the bar Tim called out
   on 9 Sep 2026. The box stays 82vh, the content just starts at the top. The
   two columns still line up with each other, that is .hero-grid's own centring.

   11 Sep 2026, the tablet half of the same bug. Bare 82vh is a viewport share,
   so on a tall portrait screen it kept growing while the content did not. iPad
   Pro 13 portrait (1032x1376) made the box 1128px for content that needs 811,
   which is 317px of dead air between the Google badge and the reviews band, and
   every screen taller than about 1024px had a smaller version of it. The box is
   now capped at 760px, which is what the two column hero actually measures at
   its tallest, so it still fills the fold on a normal laptop and can never run
   away on a tall one. */
.hero{min-height:min(82vh,760px);display:flex;align-items:flex-start;padding:144px 0 44px}

/* ---- the wordmark in the hero --------------------------------------------
   Tim, 19 Sep 2026: "we want people to see the brand more now as its becoming
   more recognisable". The top bar no longer sits over the page at the top
   (CHROME ON DEMAND, further down this file), so the hero carries the brand
   itself, at 52px instead of the 30px it had in the bar.

   It sits inside the hero's 144px top padding rather than adding to it, so the
   headline holds the position it already had. The block below the mark is the
   air Tim asked for, and it never crowds the h1 on a narrow column because the
   mark is sized off the viewport too. */
/* The 144px of top padding on .hero was clearance for the fixed bar. The bar
   is not there at the top of the page any more, so a hero carrying the mark
   spends most of that room on the mark instead and the headline lands within a
   few pixels of where it has always sat. */
.hero:has(.hero-brand){padding-top:86px}
/* OPTICAL ALIGNMENT, the bit that is invisible until it is wrong: the master
   PNG carries 151px of clear space down its left edge out of 355px of height,
   so dropped in as is the mark sits 20px right of the W below it and the two
   left edges read as a mistake. The height is held in --hb so the pull back is
   that same 151/355 = 0.4254 of whatever the mark is currently rendering at,
   which keeps the edges flush at every width. Re-trim the PNG and this number
   is wrong: remeasure the alpha bounding box before touching it.
   SIZE (Tim, 6 Oct 2026: "at least same font size as the heading"): the
   letters stand as tall as the headline's at every width. Inter 800 caps are
   0.7275 of the font size and the S is 211 of the PNG's 355px, so the mark is
   the h1 size x 1.224, taken to 1.26 for the S's round overshoot. This is the
   default .hero h1 clamp further down times 1.26. A page that sizes its own
   h1 sets its own --hb the same way (the homepage, /meta-00x, /55, /start). */
.hero-brand{--hb:clamp(2.52rem,5.292vw,4.095rem);
  position:relative;display:block;width:max-content;max-width:100%;
  margin:0 0 34px calc(var(--hb) * -0.4254);
  line-height:0;text-decoration:none;
  transition:opacity .25s ease,transform .25s cubic-bezier(.22,1,.36,1)}
.hero-brand img{display:block;height:var(--hb);width:auto}
.hero-brand:hover{opacity:.86;transform:translateY(-1px)}
/* The hero wall is a dark band in BOTH themes (see the light mode block at the
   end of this file, which keeps the headline white on light). So the wordmark
   over it stays the WHITE one in light mode: the site wide swap is undone here
   rather than narrowed, so a hero that really is light still gets the black
   mark from that one rule. */
[data-theme="light"] .hero:has(.hero-wall) .hero-brand img{
  content:url("/assets/img/scalus/scalus-long-white.png")}

/* ---- the shine on the mark ------------------------------------------------
   Tim, 19 Sep 2026: the logo comes up first "with a shine on it as well". A
   white glint is no use here, because the mark is already white: white light on
   white ink paints nothing. So the glint is CHROMATIC, the same trick the word
   "Websites" underneath it has always used, a band of the brand spectrum
   travelling across the glyphs with a white hot core. The band is masked by the
   wordmark's own PNG, so it only ever lights the ink, and the drop shadow lets
   the glow bloom a few pixels past it the way real light would.

   The mask is stretched 100%/100% rather than sized `contain`: the anchor is
   width:max-content around an image with line-height:0, so the box IS the image
   and an exact stretch can never drift half a pixel out of register.

   One pass every 9 seconds, the same clock h1rotShine runs on, so the mark and
   the headline glint in the same rhythm instead of two effects talking over
   each other. */
.hero-brand::after{content:'';position:absolute;inset:0;pointer-events:none;z-index:1;
  background:linear-gradient(100deg,transparent 44%,
    rgba(50,207,117,.95) 47.5%,rgba(255,255,255,.98) 50%,rgba(169,123,255,.95) 52.5%,
    transparent 56%);
  background-size:280% 100%;background-repeat:no-repeat;background-position:100% 0;
  -webkit-mask:url("/assets/img/scalus/scalus-long-white.png") 0 0/100% 100% no-repeat;
          mask:url("/assets/img/scalus/scalus-long-white.png") 0 0/100% 100% no-repeat;
  filter:drop-shadow(0 0 10px rgba(90,209,255,.45));
  opacity:0}
/* LINEAR on purpose. An ease over a 9s cycle eases the whole 9 seconds, not
   the 1.4 the glint is actually on screen, which bunched the whole pass into a
   third of a second somewhere in the middle and left the rest of the cycle
   crawling. Linear makes a percentage a time, so the numbers below are the
   seconds they look like: the band crosses over 1.44s and the mark then rests
   for seven and a half.

   THE RANGE IS 100% TO 0%, AND IT IS NOT ARBITRARY. On a layer 280% as wide as
   the box, one percentage point of background-position is 1.8% of the box, so
   100 points is the whole crossing and nothing more. An earlier pass ran 180%
   to -80% on the reasoning that wider must mean slower, and did the opposite:
   the band spent most of the cycle off screen either side and tore across the
   glyphs in a third of a second. Widen these numbers and the glint gets FASTER,
   not slower. Change the duration instead. */
@keyframes brandShine{
  0%      {opacity:0;background-position:100% 0}
  3%      {opacity:1}
  16%     {opacity:1;background-position:0% 0}
  20%,100%{opacity:0;background-position:0% 0}
}

@media(max-width:900px){
  /* On a phone the mark keeps the size rule above: the default h1 clamp holds
     at 2rem down here, so the mark holds at 2.52rem with it. */
  .hero:has(.hero-brand){padding-top:80px}
  .hero-brand{margin:0 0 24px calc(var(--hb) * -0.4254)}
}
/* BOTH WORDMARKS STAY. Scrolling up from just below the fold
   brings the bar back while the hero is still in view, and the bar's own logo
   lands directly on top of this one: two Scalus marks stacked, which reads as a
   bug. So they used to take turns, the hero mark standing down whenever the bar
   was on. NOT ANY MORE (Tim, 7 Oct 2026: "when the logo of scalus is there on
   page load it disappears keep it there", then "also keep the logo here in the
   menu"). Both marks stay: the hero one never hides, the bar keeps its own. */
@media(prefers-reduced-motion:reduce){.hero-brand{transition:none}}
/* ---- the hero wall --------------------------------------------------------
   Every client site, tilted and softened, behind the promise. The angle, the
   blur and the tiling are baked into one webp by scripts/build-hero-wall.mjs,
   so this is a single decode and no filter work on any frame.

   The veil is part of the same background stack rather than a second element:
   one paint, and the wall can never rise above the words no matter how the
   hero is nested. It leans out to the right, where the form sits over a solid
   card, and stays heaviest on the left where the headline has to read. */
.hero:has(.hero-wall){position:relative;isolation:isolate;overflow:hidden}
.hero-wall{position:absolute;inset:0;z-index:-1;pointer-events:none;overflow:hidden;
  -webkit-mask-image:linear-gradient(180deg,#000 0%,#000 62%,transparent 100%);
          mask-image:linear-gradient(180deg,#000 0%,#000 62%,transparent 100%)}
/* Tim, 16 Sep 2026: brighter, more colourful, less blurred, and it scrolls,
   very slowly. The wall is now two layers inside the one span: ::before is the
   picture, oversized and drifting along the tilt of the columns on a transform
   (compositor only, no repaint), ::after is the veil, which stays put so the
   headline side never lightens as the wall moves. The drift is a slow
   alternate, about one pixel a second, so there is no seam to hide and no
   edge can ever come into view. The veil alphas came down from .97/.93/.8/.7
   so the sites carry the hero; the headline side is still the darkest end. */
.hero-wall::before,.hero-wall::after{content:"";position:absolute}
.hero-wall::before{inset:-14%;background:url("/assets/img/hero/site-wall.webp") center/cover no-repeat;
  animation:hero-wall-drift 46s ease-in-out infinite alternate;will-change:transform}
.hero-wall::after{inset:0;background:
    linear-gradient(97deg,rgba(7,8,8,.87) 0%,rgba(7,8,8,.78) 34%,rgba(7,8,8,.52) 62%,rgba(7,8,8,.38) 100%)}
/* Ken Burns on the wall (Tim, 17 Sep 2026: "all these images need ken burns
   effect"). The wall is one baked webp, so the sites push in together rather
   than each on its own timing; at this blur and this veil that reads as the
   whole wall breathing, which is what the drift was already half doing. Scale
   rides on the same transform as the translate, so it is still one compositor
   property and no repaint. `alternate` takes it back out over the next 46s, so
   there is no jump at the loop and nothing ever crops in. */
@keyframes hero-wall-drift{
  from{transform:translate3d(0,0,0) scale(1)}
  to{transform:translate3d(-2.6%,-9%,0) scale(1.14)}}
/* On a phone the 97deg sweep never reaches its light end, so the whole wall
   sat under the darkest part of the veil and read as fog. The words there are
   over the top of the wall, not beside it, so the veil goes top to bottom:
   heavy behind the headline, lighter under it where the form card takes over. */
@media(max-width:900px){
  /* The phone hero is about 1450px tall and 390 wide, so cover blew one tile
     up to the width of the screen and the wall was a smear. Paint it at a
     fixed width instead, from the top, and let the mask fade take the bottom
     edge. No drift on a phone: the seam would walk into view, and it is a
     battery cost for a section that is scrolled past in a second. */
  .hero-wall::before{inset:0 0 -12% 0;background-size:1800px auto;background-position:center top;background-repeat:repeat-y;
    animation:hero-wall-drift-y 40s ease-in-out infinite alternate}
  .hero-wall::after{background:linear-gradient(180deg,rgba(7,8,8,.84) 0%,rgba(7,8,8,.68) 40%,rgba(7,8,8,.46) 100%)}
  /* Light mode's phone veil is the dark one too now (Tim, 17 Sep 2026). See
     the note at the foot of this file: the light hero column went white, so
     the ground under it has to be dark at every width, not only on desktop. */
  [data-theme="light"] .hero-wall::after{background:linear-gradient(180deg,rgba(7,8,8,.80) 0%,rgba(7,8,8,.66) 40%,rgba(7,8,8,.50) 78%,rgba(7,8,8,.42) 100%)}
}
@media(prefers-reduced-motion:reduce){.hero-wall::before{animation:none}}
/* ---- the home entrance (Tim, 16 Sep 2026) --------------------------------
   One sequence, top to bottom, the way the eye reads the fold: the menu bar
   slides down from above, the headline lands word by word, the video card
   appears, then the form under it. All CSS with backwards fill, for the same
   reason the checks are: this is the first paint and it cannot wait on a
   script. Scoped to [data-intro], which only the homepage hero carries, so the
   meta landing pages and every other hero keep their quick ripple.
   Tim, 19 Sep 2026: "make the scalus logo animate in first thing to come up,
   with a shine on it as well, and then everything". So the wordmark now opens
   the sequence and everything behind it moved back a quarter second. The bar
   no longer drops in at all, because the bar is not on screen at the top of the
   page any more: the mark took that beat.
     0.05  the wordmark rises in (.62s), and the shine crosses it from 0.5
     0.75  "Websites" leads the headline, the plain words follow at .11
     1.55  lead line
     1.80  form card, then its video at 2.0
     2.50  the fields
     3.15  the checks are done, then the buttons at 3.55, fine print 3.85 and
           the Google score at 4.05 close the column off. */
/* The wall is simply THERE, softened, from the first paint (Tim, 16 Sep 2026:
   "just leave the background"). It briefly landed sharp and pulled into blur as
   the words arrived; a change of focus under a headline that is itself moving
   was one thing too many, so the wall holds still and only drifts. */
@keyframes hero-wall-drift-y{
  from{transform:translate3d(0,0,0) scale(1)}
  to{transform:translate3d(0,-4%,0) scale(1.1)}}
/* A slow glow breathing on the two coloured words in the lead line. The
   scattered twinkles that used to sit over the wall with this are gone
   (Tim, 16 Sep 2026): the drifting wall was already carrying the movement. */
[data-intro] .glass-phrase > span{animation:wordGlow 5.5s ease-in-out infinite}
[data-intro] .glass-phrase.pot > span{animation-delay:2.75s}
@keyframes wordGlow{0%,100%{text-shadow:0 0 0 rgba(50,207,117,0)}50%{text-shadow:0 0 14px rgba(50,207,117,.75),0 0 30px rgba(90,209,255,.35)}}
@media(prefers-reduced-motion:reduce){[data-intro] .glass-phrase > span{animation:none}}
/* navDrop went with the bar on 19 Sep 2026. Nothing is on screen at the top of
   the page to drop in any more, so the sequence opens on the wordmark instead. */
/* THE MARK OPENS THE PAGE. `backwards` and not `both` on purpose: the fill has
   to let go the moment the entrance is over, or it would pin opacity:1 on the
   element for the rest of the visit and the bar could never take its turn. */
html.js [data-intro] .hero-brand{animation:heroRiseIn .62s cubic-bezier(.22,.68,.34,1) .05s backwards}
html.js [data-intro] .hero-brand::after{animation:brandShine 9s linear .45s infinite}
html.js [data-intro] .h1-shine{animation-delay:0s,.75s}
html.js [data-intro] .lead{opacity:0;animation:heroRiseIn .7s cubic-bezier(.22,.68,.34,1) 1.55s both}
html.js [data-intro] .hero-checks li:nth-child(1){animation-delay:2.15s}
html.js [data-intro] .hero-checks li:nth-child(2){animation-delay:2.65s}
html.js [data-intro] .hero-checks li:nth-child(3){animation-delay:3.15s}
html.js [data-intro] .cta-row{opacity:0;animation:heroRiseIn .7s cubic-bezier(.22,.68,.34,1) 3.55s both}
html.js [data-intro] .hero-fine{animation-delay:3.85s}
html.js [data-intro] .gq-score{opacity:1;transform:none;transition:none;animation:heroRiseIn .7s cubic-bezier(.22,.68,.34,1) 4.05s both}
/* THE HEADLINE WRITES ON (Tim, 7 Oct 2026: "animate in websites for small
   businesses, like a write on"). Each line (.wl) is uncovered left to right by
   a soft edged mask, one line after the other, the way a pen would cross it.
   A mask and not a clip-path so the leading edge is a feather, not a cut.

   The mask sits on the LINE, never on .h1-shine itself: that word's colour is
   a background clipped to its glyphs, and an effect on that same element is
   what has painted it blank before (see the note on html.js .h1-shine). On an
   ancestor the whole line composites as one group and the clip is untouched.

   The fill is `backwards`, so the mask exists only while the line is being
   written and is gone the moment it lands. The padding/negative margin pair
   gives the mask a little room past the glyph box so the W's overhang and the
   cap tops are not trimmed mid write; it adds nothing to the layout.

   Mask maths: the layer is 250% of the line, opaque to 40%, feathered to 48%.
   At 100% the line sees 60-100% of the layer (clear), at 0% it sees 0-40%
   (solid), so nothing is ever half shown at either end. */
html.js [data-intro] h1[data-write] .wl{display:inline-block;padding:.12em .1em;margin:-.12em -.1em;
  animation:writeOn .72s cubic-bezier(.45,.05,.35,1) .55s backwards}
html.js [data-intro] h1[data-write] .wl:nth-of-type(2){animation-duration:.78s;animation-delay:1.12s}
html.js [data-intro] h1[data-write] .wl:nth-of-type(3){animation-duration:.82s;animation-delay:1.74s}
@keyframes writeOn{
  from{-webkit-mask-image:linear-gradient(90deg,#000 40%,transparent 48%);mask-image:linear-gradient(90deg,#000 40%,transparent 48%);
       -webkit-mask-size:250% 100%;mask-size:250% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
       -webkit-mask-position:100% 0;mask-position:100% 0}
  to  {-webkit-mask-image:linear-gradient(90deg,#000 40%,transparent 48%);mask-image:linear-gradient(90deg,#000 40%,transparent 48%);
       -webkit-mask-size:250% 100%;mask-size:250% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
       -webkit-mask-position:0% 0;mask-position:0% 0}
}
/* "Websites" is uncovered by its line now, so it drops its own fade in and
   keeps only the shine loop. */
html.js [data-intro] h1[data-write] .h1-shine{opacity:1;animation:h1rotShine 9s ease-in-out infinite}
@media(prefers-reduced-motion:reduce){html.js [data-intro] h1[data-write] .wl{animation:none}
  html.js [data-intro] h1[data-write] .h1-shine{animation:none;background-position:50% 0,0 0}}
/* The scroll reveal wrapper would fade the whole card at once the moment the
   script ran, over the top of this. It steps aside on the intro hero. */
html.js [data-intro] .reveal{opacity:1;transform:none;transition:none}
html.js [data-intro] .hq-panel{opacity:0;animation:heroRiseIn .75s cubic-bezier(.22,.68,.34,1) 1.8s both}
html.js [data-intro] .hq-face{opacity:0;animation:heroRiseIn .7s cubic-bezier(.22,.68,.34,1) 2s both}
html.js [data-intro] .hq-panel > :not(.hq-face){opacity:0;animation:heroRiseIn .7s cubic-bezier(.22,.68,.34,1) 2.5s both}
@media(prefers-reduced-motion:reduce){
  html.js [data-intro] .hero-brand,html.js [data-intro] .lead,html.js [data-intro] .hq-panel,
  html.js [data-intro] .hq-face,html.js [data-intro] .hq-panel > :not(.hq-face),html.js [data-intro] .cta-row,html.js [data-intro] .gq-score{opacity:1;animation:none;transform:none}
  html.js [data-intro] .hero-brand::after{display:none}
}
/* The hero headline is three deliberate lines (<word> Websites / for Local
   Melbourne / Small Businesses) with the breaks in the markup. At the site wide
   h1 size the middle line wrapped inside its own column and the three became
   five, so the hero gets its own slightly smaller scale. Everything else keeps
   the big one.

   The rotating word made this tighter again (25 Aug 2026): line one now has to
   hold the longest word in the list plus "Websites", and "Forward Compatible
   Websites" is a lot wider than the "Websites" that used to sit there. So the
   scale came down another notch and the two places that must never break are
   pinned. Line one is ALLOWED to wrap on the longest words — that reads fine,
   because the word and the noun stay together as a unit. What is not allowed is
   "for Local / Melbourne", which orphans a suburb. */
.hero h1{font-size:clamp(2rem,4.2vw,3.25rem);text-wrap:balance}
.hero h1 .green{white-space:nowrap}
.hero-grid{display:grid;grid-template-columns:1.05fr .95fr;gap:56px;align-items:center}
@media(max-width:900px){.hero-grid{grid-template-columns:1fr;gap:40px}}
@media(max-width:560px){.hero-grid{gap:32px}.hero{padding:146px 0 34px}}

/* HERO TOP AIR — one number for the whole site, measured against the bar.
   History, both ends of it (9 Sep 2026). The heroes first sat at 140px top
   padding AND centred inside an 82vh box, which floated the headline miles
   down: /packages 265px of dead air under the bar, /about 236px, /guides
   241px. Dropping to 96px and pinning align-items to flex-start went too far
   the other way and left only 12px to 20px, so the headline read as jammed
   under the bar. The number now clears the bar by about 70px on every page. It
   was 44px until 13 Sep 2026, when Tim asked for more room under the menu bar
   on every page. Remeasured the same way: the home hero went 118 to 144, the
   text pages 118 to 150 (their bar ends 6px lower), the phone 120 to 146, and
   the case study hero with them.

   Watch the two bar heights. The phone bar is 62px tall (the 40px menu button,
   not the 30px logo), ends at 76px, so the phone needs 120px for the air. That
   is why the two numbers are different, do not "tidy" them into one.

   DESKTOP MOVED TO 118px, 11 Sep 2026 (Tim: "too close to the top"). The bar
   was 52px tall ending at 66px when 110px was set. It now measures 74px at the
   bottom, so 110px was leaving only 36px of air while the phone still had its
   44px. Measured in headless Chromium at 1440 and 390, never eyeballed: bar
   getBoundingClientRect().bottom against the top of the first element that
   actually paints. Remeasure before touching these numbers again.

   This rule is the text page half. A hero with no .hero-grid drops the 82vh
   box entirely and pins one top padding for every page. The padding is forced
   because most of these pages carry their own inline padding, and it has to be
   one number everywhere or they drift apart again. Bottom padding is left
   alone, each page still sets its own. Keep display:flex, the .wrap child
   sizes off it. */
.hero:not(:has(.hero-grid)){min-height:0;align-items:flex-start;padding-top:150px !important}
@media(max-width:560px){
  .hero:not(:has(.hero-grid)){padding-top:146px !important}
}
.cta-row{display:flex;gap:14px;flex-wrap:wrap;margin-top:30px}

/* reveal */
.js-anim .reveal{opacity:0;transform:translateY(26px);transition:opacity .8s, transform .8s}
.js-anim .reveal.in{opacity:1;transform:none}

/* footer — lit top edge, glass link pills, fine print sitting quietly underneath */
footer.site{position:relative;padding:76px 0 48px;border-top:0;text-align:center;
  background:radial-gradient(130% 150% at 50% 0%,rgba(50,207,117,.07),transparent 62%)}
footer.site::before{content:"";position:absolute;top:0;left:0;right:0;height:1px;
  background:linear-gradient(90deg,transparent,rgba(50,207,117,.45),transparent)}
footer.site .wrap{position:relative}
footer.site p{color:var(--ink-mute);font-size:.85rem}
footer.site > .wrap > a:first-child img{filter:drop-shadow(0 8px 22px rgba(0,0,0,.55))}
footer.site > .wrap > p:first-of-type{color:var(--ink-soft);font-size:.82rem;letter-spacing:.03em}
/* the row of section links becomes glass pills. The raw dots between the
   anchors are text nodes, so the row zeroes its own font size and each pill
   sets its own — the separators vanish without touching the markup. */
/* the inline font-size on those paragraphs is why the zeroing needs the weight */
footer.site p:has(a + a){display:flex;flex-wrap:wrap;justify-content:center;gap:8px;
  font-size:0 !important;margin-top:22px}
footer.site p:has(a + a) a{font-size:.78rem;font-weight:600;line-height:1;color:var(--ink-soft);
  text-decoration:none;padding:9px 15px;border-radius:999px;border:1px solid var(--line-2);
  background:rgba(var(--wa),.04);
  -webkit-backdrop-filter:var(--glass-sm);backdrop-filter:var(--glass-sm);box-shadow:var(--rim-sm);
  transition:color .18s,border-color .18s,background .18s,transform .18s}
footer.site p:has(a + a) a:hover{color:var(--ink);border-color:rgba(50,207,117,.5);
  background:var(--green-soft);transform:translateY(-2px)}
footer.site p:has(a + a) ~ p{margin-top:20px;font-size:.72rem;opacity:.75}
@media(max-width:600px){
  footer.site{padding:60px 0 40px}
  footer.site p:has(a + a){gap:7px}
  footer.site p:has(a + a) a{padding:8px 13px;font-size:.74rem}
}
.disclaimer{max-width:680px;margin:18px auto 0;font-size:.72rem;color:var(--ink-mute);opacity:.8}

/* Google preferred sources button. Injected by sc.js into the footer wrap on
   every page, because the footers differ page to page and the widget is Google
   script driven anyway, so there is nothing to render without JS. The button
   itself is drawn by news.google.com, so we only own the frame around it. */
.ps-badge{margin:26px 0 8px;display:flex;flex-direction:column;align-items:center;gap:10px}
footer.site .ps-badge p{margin:0;font-size:.72rem;color:var(--ink-mute);opacity:.75}
.ps-badge > div{min-height:36px;display:flex;justify-content:center}
@media(max-width:600px){ .ps-badge{margin:22px 0 6px} }

/* misc */
.split{display:grid;grid-template-columns:1fr 1fr;gap:48px;align-items:center}
@media(max-width:880px){.split{grid-template-columns:1fr}}
.kbd{font-size:.78rem;border:1px solid var(--line-2);border-radius:6px;padding:2px 8px;color:var(--ink-soft)}

/* ===== v2 additions — scalus.io-style showcase ===== */
.grad{color:#fff}
/* The orange lobe came out on 25 Aug 2026 (Tim, "too much colour"). It was a
   maroon wash across the lower right of every screen, and because it is fixed
   it followed you down the whole page: every dark section had a red corner in
   it, competing with whatever that section was actually showing. Green only
   now, which is the brand, and the page reads as one colour with black. */
.glow.io{background:
   radial-gradient(1100px 640px at 78% -12%, rgba(50,207,117,.18), transparent 62%),
   radial-gradient(700px 520px at 8% 16%, rgba(50,207,117,.07), transparent 60%),
   linear-gradient(180deg,#081009,#070808 40%)}

/* ---- clients ticker: real phone previews of every site, newest signup first ----
   Cards are rendered from /assets/data/clients.json (regenerated from Stripe by
   ~/.scalus/clients-ticker/build.mjs), so a new signup joins the ticker without
   anyone editing HTML. The pill markup in the page is the no-JS fallback. */
.cl-tick .track{gap:14px;align-items:stretch}
.cl-card{display:flex;align-items:center;gap:14px;flex:0 0 auto;
  padding:12px 20px 12px 12px;border:1px solid var(--line-2);border-radius:18px;
  background:rgba(var(--wa),.02);color:inherit}
/* no hover lift: the cards aren't clickable, so they shouldn't invite a click */
/* the phone: a real bezel so the screenshot reads as a device, not a thumbnail */
.cl-phone{position:relative;flex:none;width:64px;height:128px;border-radius:12px;
  background:#000;border:2px solid #23262a;overflow:hidden;box-shadow:0 8px 22px rgba(0,0,0,.55)}
.cl-phone img{width:100%;height:100%;object-fit:cover;object-position:top center;display:block}
/* multi page cards: stack the shots and crossfade the top one away. The stagger
   is set per card in JS so the whole ticker does not flip in unison. */
.cl-phone[data-cl-fade] img{position:absolute;inset:0;opacity:0;transition:opacity .9s ease}
.cl-phone[data-cl-fade] img.on{opacity:1}
@media(prefers-reduced-motion:reduce){
  .cl-phone[data-cl-fade] img{transition:none}
}
.cl-phone::after{content:"";position:absolute;top:4px;left:50%;transform:translateX(-50%);
  width:22px;height:3px;border-radius:3px;background:#23262a}
.cl-meta{display:flex;flex-direction:column;gap:3px;min-width:0}
.cl-meta b{font-size:.95rem;font-weight:700;white-space:nowrap}
.cl-cat{font-size:.72rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
  color:var(--ink-mute);white-space:nowrap}
.cl-when{display:inline-flex;align-items:center;gap:6px;margin-top:3px;
  font-size:.72rem;font-weight:600;color:#32cf75;white-space:nowrap}
.cl-when::before{content:"";width:6px;height:6px;border-radius:50%;background:#32cf75;
  box-shadow:0 0 0 3px rgba(50,207,117,.18)}
/* the review line: Google style stars plus the words that client actually sent.
   Kept small so it rides under the join date without stretching the card, and
   clamped to two lines so one long quote can't blow the marquee's rhythm out.
   Only rendered for a client we hold a real quote from — see QUOTES in the
   renderer. No quote on record means no stars, so the strip never puts words or
   a rating on a business that never gave one. */
.cl-rev{display:flex;flex-direction:column;gap:3px;margin-top:5px;max-width:236px}
/* The stars twinkle left to right on a loop rather than sitting still. The
   marquee is already moving, so this is deliberately small: a lift, a brighten
   and a glow that runs along the row like light crossing a rating, one star at
   a time. Staggered per star so it reads as a wave, not five things blinking. */
.cl-stars{display:flex;gap:1.5px}
.cl-stars svg{width:11px;height:11px;display:block;fill:#fbbc04;transform-origin:50% 55%;
  animation:clStar 3.2s ease-in-out infinite both}
.cl-stars svg:nth-child(2){animation-delay:.13s}
.cl-stars svg:nth-child(3){animation-delay:.26s}
.cl-stars svg:nth-child(4){animation-delay:.39s}
.cl-stars svg:nth-child(5){animation-delay:.52s}
@keyframes clStar{
  0%,62%,100%{transform:scale(1) rotate(0deg);fill:#fbbc04;filter:none}
  74%{transform:scale(1.28) rotate(-9deg);fill:#ffd34d;
      filter:drop-shadow(0 0 5px rgba(251,188,4,.85))}
  86%{transform:scale(1) rotate(0deg);fill:#fbbc04;filter:none}}
@media(prefers-reduced-motion:reduce){.cl-stars svg{animation:none}}
.cl-quote{font-size:.73rem;line-height:1.35;color:var(--ink-soft);white-space:normal;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}

/* Who said it. Every quote is signed, so a visitor can see a person behind the
   line rather than an anonymous five stars. Quieter and smaller than the quote,
   no dash in front of it, and it never wraps to a second line. */
.cl-by{font-size:.68rem;line-height:1.3;color:var(--ink-mute);font-weight:600;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:1px}
@media(max-width:600px){
  .cl-card{padding:10px 16px 10px 10px;gap:11px}
  .cl-phone{width:52px;height:104px}
  .cl-meta b{font-size:.86rem}
  .cl-cat,.cl-when{font-size:.66rem}
  .cl-rev{max-width:176px;margin-top:4px}
  .cl-stars svg{width:10px;height:10px}
  .cl-quote{font-size:.68rem}
  .cl-by{font-size:.63rem}
}

/* ---------------------------------------------------------------------------
   "CURRENTLY POWERING" — the label, then the row (Tim, 31 Aug 2026).

   The label used to be four inline styles repeated on four pages, two of which
   had already drifted to a different size. It is a class now, so it is one
   thing in one place.

   THE LABEL ARRIVES FIRST, then the row fades up under it. Nothing here moves
   horizontally on entry: the row is already a horizontal movement, and a row
   that slides in sideways and then keeps sliding reads as one long stumble.
   The .in class is added by sc.js when the band scrolls into view. */
.cp-title{text-align:center;font-size:.72rem;font-weight:700;letter-spacing:.22em;
  text-transform:uppercase;color:var(--ink-mute);margin:0 0 24px}
html.js .cp-title,html.js .cl-tick{opacity:0;
  transition:opacity .7s cubic-bezier(.22,1,.36,1),transform .7s cubic-bezier(.22,1,.36,1)}
html.js .cp-title{transform:translateY(12px)}
html.js .cl-tick{transition-delay:.34s}
html.js .cp-title.in,html.js .cl-tick.in{opacity:1;transform:none}

/* The row starts WHEN YOU REACH IT, already part way along (Tim, 31 Aug 2026),
   so the first card you ever see is 22 Electrical Works, arriving through the
   fade at the left edge rather than sitting squarely against it waiting to set
   off. A standing start reads as a list; a row caught mid stride reads as a
   thing that has been going the whole time, which is the claim the section is
   making.

   TWO PARTS, AND IT NEEDS BOTH.

   Held paused until .in, because the row is below the fold on most screens and
   an animation that starts at page load has already walked several cards along
   by the time anybody scrolls down to it — the running order would be correct
   and nobody would ever see the front of it.

   Then a NEGATIVE animation-delay for the offset: the animation begins already
   this far into its own cycle, with no extra keyframes and nothing for JS to
   time. Paused plus a negative delay holds it AT that offset, so the first
   painted frame is the half arrived one.

   The three values are the same DISTANCE at each breakpoint, not the same
   number of seconds: the keyframe is a fixed 0 to -50% over whatever the
   duration is, so holding the delay at a constant ~5.5% of the duration moves
   the row about half a card in every case. Match them if the durations on
   durations below ever change.

   SLOWER THAN THE SHARED MARQUEE (Tim, 30 Sep 2026: "slow this down where it's
   shown"). The cards carry a quote and a name to read, so the row runs at
   twice the old desktop time here and about 1.6x on phones. Only .cl-tick,
   the other .marquee rows keep their own pace. */
html.js .cl-tick .track{animation-play-state:paused}
html.js .cl-tick.in .track{animation-play-state:running}
.marquee.cl-tick .track{animation-duration:72s;animation-delay:-4s}
@media(max-width:820px){.marquee.cl-tick .track{animation-duration:100s;animation-delay:-5.5s}}
@media(max-width:560px){.marquee.cl-tick .track{animation-duration:125s;animation-delay:-6.9s}}
@media(prefers-reduced-motion:reduce){
  html.js .cp-title,html.js .cl-tick{opacity:1;transform:none;transition:none}
}

/* client marquee */
.marquee{overflow:hidden;position:relative;padding:10px 0;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent);
  mask-image:linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent)}
.marquee .track{display:flex;gap:16px;width:max-content;animation:mq 36s linear infinite}
/* Tim, 24 Aug 2026: the row keeps moving under the cursor. Pausing on hover
   stalled it every time someone read a card, which read as broken. */
/* On a phone the cards are wider relative to the screen, so the same speed
   reads as a blur. Slower there, and slower again on the smallest screens. */
@media(max-width:820px){.marquee .track{animation-duration:60s}}
@media(max-width:560px){.marquee .track{animation-duration:78s}}
@keyframes mq{to{transform:translateX(-50%)}}
.cl-pill{display:flex;align-items:center;gap:12px;padding:14px 22px;border-radius:999px;white-space:nowrap;
  background:var(--panel);border:1px solid var(--line);-webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur)}
.cl-pill b{font-size:.95rem}
a.cl-pill{text-decoration:none;color:inherit;transition:border-color .25s,transform .25s,background .25s}
a.cl-pill:hover{border-color:var(--line-2);transform:translateY(-2px);background:rgba(var(--wa),.07)}
.cl-pill span{font-size:.74rem;color:var(--ink-mute);font-weight:600;letter-spacing:.06em;text-transform:uppercase}
.cl-pill .dot{width:8px;height:8px;border-radius:50%;background:var(--green);animation:pulse 1.8s infinite}
.cl-pill .ci{width:26px;height:26px;flex:none;color:var(--green)}
.cl-pill .ci svg{width:100%;height:100%;display:block;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round;fill:none}
.cl-pill.cl-more{flex-direction:column;gap:2px;align-items:flex-start;border-color:color-mix(in srgb,var(--green) 45%,var(--line));background:color-mix(in srgb,var(--green) 10%,var(--panel))}
.cl-pill.cl-more b{color:var(--green)}
/* A pill in the STATIC wrapped grid (/clients) must be able to wrap: at nowrap,
   "Eden Building & Landscaping · LANDSCAPING" is ~404px, wider than a phone, and
   that stray width made the whole page render zoomed out. Pills inside a
   .marquee .track keep nowrap — that track is meant to be one long line and the
   marquee clips it. */
@media(max-width:560px){
  .client-grid{gap:10px}
  .client-grid .cl-pill{white-space:normal;flex-wrap:wrap;max-width:100%;
    padding:12px 18px;border-radius:20px;row-gap:2px}
  .client-grid .cl-pill b{flex:1 1 auto;min-width:0}
  .client-grid .cl-pill>span:not(.ci){flex:1 1 100%}
}
.cl-pill.cl-more span{color:var(--green);opacity:.75}

/* phone (SMS demo) */
.phone{width:300px;max-width:100%;margin:0 auto;border-radius:38px;border:1px solid var(--line-2);
  background:linear-gradient(180deg,#101312,#0a0c0b);padding:16px 14px 22px;box-shadow:0 40px 90px rgba(0,0,0,.6)}
.phone .notch{width:96px;height:22px;border-radius:999px;background:#000;margin:0 auto 16px;border:1px solid var(--line)}
.msgs{display:grid;gap:9px;min-height:300px;align-content:end}
.msg{max-width:84%;padding:10px 14px;border-radius:18px;font-size:.82rem;line-height:1.45;opacity:0;transform:translateY(10px) scale(.96);transition:opacity .5s,transform .5s}
.msg.show{opacity:1;transform:none}
.msg.in{background:rgba(var(--wa),.09);border-bottom-left-radius:6px;justify-self:start}
.msg.out{background:var(--green);color:#04130a;font-weight:500;border-bottom-right-radius:6px;justify-self:end}
.msg time,.msg .t{display:block;font-size:.6rem;opacity:.6;margin-top:3px}

/* dashboard demo */
.dash{padding:24px}
.dash .dh{display:flex;align-items:center;gap:12px;margin-bottom:18px}
.dash .dh b{font-size:.92rem}
.dash .dh small{color:var(--ink-mute);font-size:.72rem}
.dkpis{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:18px}
.dkpi{padding:14px;border-radius:14px;background:rgba(var(--wa),.04);border:1px solid var(--line)}
.dkpi .v{font-size:1.3rem;font-weight:800;letter-spacing:-.02em}
.dkpi .k{font-size:.62rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-mute)}
.dkpi .up{color:var(--green);font-size:.68rem;font-weight:700}
.chart{position:relative}
.chart svg{width:100%;height:auto;display:block}
.chart .ln{fill:none;stroke:var(--green);stroke-width:2.5;stroke-linecap:round;
  stroke-dasharray:600;stroke-dashoffset:600}
.chart .ar{fill:url(#gfill);opacity:0}
.in .chart .ln, .chart.in .ln{animation:draw 1.8s .2s ease-out forwards}
.in .chart .ar, .chart.in .ar{animation:fadein 1s 1.2s forwards}
@keyframes draw{to{stroke-dashoffset:0}}
@keyframes fadein{to{opacity:1}}
.bars{display:flex;gap:6px;align-items:flex-end;height:64px}
.bars i{flex:1;border-radius:4px 4px 0 0;background:rgba(50,207,117,.35);transform:scaleY(0);transform-origin:bottom;transition:transform .8s cubic-bezier(.2,.8,.2,1)}
.in .bars i{transform:scaleY(1)}
.bars i:nth-child(even){background:rgba(50,207,117,.7)}

/* review demo */
.rev-card{padding:22px;display:grid;gap:10px}
.rev-card .g{display:flex;align-items:center;gap:10px}
.rev-card .stars{color:#fbbc04;letter-spacing:.1em}
/* stars land one at a time, the way a review arrives */
.rev-card .stars i{font-style:normal;display:inline-block;opacity:0;transform:scale(.4) rotate(-25deg);
  animation:starPop .45s cubic-bezier(.2,1.5,.4,1) forwards}
.rev-card .stars i:nth-child(1){animation-delay:.15s}
.rev-card .stars i:nth-child(2){animation-delay:.29s}
.rev-card .stars i:nth-child(3){animation-delay:.43s}
.rev-card .stars i:nth-child(4){animation-delay:.57s}
.rev-card .stars i:nth-child(5){animation-delay:.71s}
@keyframes starPop{to{opacity:1;transform:scale(1) rotate(0)}}

/* map pack: the three results, then your listing climbing to the top spot */
.mappack{padding:21px;display:grid;gap:12px;overflow:hidden}
.mp-search{display:flex;align-items:center;gap:9px;padding:9px 13px;border-radius:100px;
  background:rgba(var(--wa),.05);border:1px solid var(--line-2);font-size:.82rem;color:var(--ink-soft)}
.mp-search svg{width:14px;height:14px;flex:none;color:var(--ink-mute)}
.mp-search .typed{white-space:nowrap;overflow:hidden;border-right:2px solid var(--green);
  animation:mpType 2.4s steps(24) infinite}
@keyframes mpType{0%{max-width:0}30%,88%{max-width:14em}100%{max-width:14em}}
.mp-list{display:grid;gap:8px;position:relative}
.mp-row{display:flex;align-items:center;gap:11px;height:46px;padding:0 13px;border-radius:12px;
  background:rgba(var(--wa),.035);border:1px solid var(--line);font-size:.84rem;color:var(--ink-soft)}
.mp-row .pin{width:24px;height:24px;border-radius:8px;flex:none;display:grid;place-items:center;
  background:rgba(var(--wa),.06);color:var(--ink-mute);font-size:.72rem;font-weight:800}
/* third place at rest, then it climbs past the two above it into the top spot */
.mp-row.you{background:rgba(4,19,10,.98);border-color:rgba(50,207,117,.55);color:var(--ink);font-weight:700;
  position:relative;z-index:2;animation:mpClimb 6s cubic-bezier(.6,0,.2,1) infinite}
.mp-row.you .pin{background:var(--green);color:#04130a}
@keyframes mpClimb{0%,34%{transform:translateY(0);box-shadow:none}
  52%,92%{transform:translateY(-108px);box-shadow:0 14px 34px rgba(50,207,117,.24)}
  100%{transform:translateY(0);box-shadow:none}}
.mp-row.shift{animation:mpShift 6s cubic-bezier(.6,0,.2,1) infinite}
@keyframes mpShift{0%,34%{transform:translateY(0)}52%,92%{transform:translateY(54px)}100%{transform:translateY(0)}}
@media(prefers-reduced-motion:reduce){
  .rev-card .stars i{animation:none;opacity:1;transform:none}
  .mp-search .typed,.mp-row.you,.mp-row.shift{animation:none}
  .mp-row.you{order:0}
}
.rev-card p{font-size:.88rem;color:var(--ink-soft)}
.rev-flow{display:grid;gap:12px}
.rev-step{display:flex;gap:14px;align-items:center;padding:14px 18px;border-radius:var(--r-sm);
  background:var(--panel);border:1px solid var(--line)}
.rev-step .n{width:30px;height:30px;border-radius:50%;flex:none;display:grid;place-items:center;
  background:var(--green-soft);border:1px solid rgba(50,207,117,.35);color:var(--green);font-weight:800;font-size:.8rem}
.rev-step p{font-size:.88rem;color:var(--ink-soft);margin:0}
.rev-step b{color:var(--ink)}

/* staggered reveal */
.js-anim .stagger > *{opacity:0;transform:translateY(22px);transition:opacity .7s,transform .7s}
.js-anim .stagger.in > *{opacity:1;transform:none}
.js-anim .stagger.in > *:nth-child(2){transition-delay:.12s}
.js-anim .stagger.in > *:nth-child(3){transition-delay:.24s}
.js-anim .stagger.in > *:nth-child(4){transition-delay:.36s}
.js-anim .stagger.in > *:nth-child(5){transition-delay:.48s}
.js-anim .stagger.in > *:nth-child(6){transition-delay:.6s}
.js-anim .stagger.in > *:nth-child(7){transition-delay:.72s}
.js-anim .stagger.in > *:nth-child(8){transition-delay:.84s}
.js-anim .stagger.in > *:nth-child(9){transition-delay:.96s}
.js-anim .stagger.in > *:nth-child(10){transition-delay:1.08s}
.js-anim .stagger.in > *:nth-child(11){transition-delay:1.2s}
.js-anim .stagger.in > *:nth-child(12){transition-delay:1.32s}
.js-anim .stagger.in > *:nth-child(n+13){transition-delay:1.44s}

/* floating accents */
.float{animation:floaty 7s ease-in-out infinite}
.float.d2{animation-delay:-3.5s}
@keyframes floaty{0%,100%{transform:translateY(0)}50%{transform:translateY(-12px)}}
@media(prefers-reduced-motion:reduce){.float,.marquee .track{animation:none}}

/* animated AI flower mark */
.aimark-container{width:280px;height:280px;display:flex;align-items:center;justify-content:center}
.aimark{width:100%;height:100%;filter:drop-shadow(0 8px 28px rgba(50,207,117,.25))}
.aimark .petals{transform-origin:60px 60px;animation:knotspin 26s linear infinite}
.aimark .rays{transform-origin:60px 60px;animation:knotspin 40s linear infinite reverse}
@keyframes knotspin{from{transform:rotate(0)}to{transform:rotate(360deg)}}
.aimark .hub{transform-origin:60px 60px;animation:hubpulse 4s ease-in-out infinite}
@keyframes hubpulse{0%,100%{opacity:.85;transform:scale(1)}50%{opacity:1;transform:scale(1.15)}}
.aimark .petal{opacity:.25;stroke:#2f7d56;transition:opacity .7s ease,stroke .7s ease}
.aimark .petal.lit{opacity:1;stroke:#5fe39a}

/* small-screen polish */
@media(max-width:560px){
  .chip{white-space:normal;text-align:left;font-size:.7rem;line-height:1.4}
  .cta-row .btn{width:100%;font-size:.88rem}
  /* The phone bar is taller than the desktop one (the 40px menu button, not a
     30px logo), so the phone number is bigger to leave the SAME clear air.
     See SQUISHED HERO above. */
  .hero{padding-top:146px}
  .phone{width:260px}
  .aimark-container{width:200px;height:200px}
}

/* redacted (privacy-blurred) bits in the live feed */
.redact{filter:blur(5px);opacity:.85;user-select:none;pointer-events:none;display:inline-block}

/* feed: client attribution + stacked pills */
.ui-row .meta{margin-left:auto;flex:none;display:flex;flex-direction:column;gap:5px;align-items:flex-end}
.ui-row .meta .src{margin-left:0}
.ui-row .cl{flex:none;font-size:.64rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
  padding:4px 10px;border-radius:999px;background:var(--green-soft);border:1px solid rgba(50,207,117,.3);color:var(--green);white-space:nowrap}
@media(max-width:560px){.ui-row .cl{display:none}}

/* feed timestamps */
.ui-row .ago{font-size:.62rem;color:var(--ink-mute);white-space:nowrap;font-weight:600}

/* ===== Motion layer v2 (scalus.io feel) =====
   All hide/transform states are gated on html.js + .mv tagging added by sc.js,
   so content stays fully visible with JS off (and in headless snaps). */

/* nav CTA: keep dark text (beats .nav .links a color) */
.nav .links a.btn-green{color:#04130a}

/* drifting orbs */
.orbs{position:fixed;inset:0;pointer-events:none;z-index:0;overflow:hidden}
.orbs i{position:absolute;border-radius:50%;filter:blur(70px);opacity:.16;will-change:transform}
.orbs i.a{width:480px;height:480px;background:#32cf75;top:-140px;right:-120px;animation:orbA 26s ease-in-out infinite}
.orbs i.b{width:380px;height:380px;background:#0e8f4a;bottom:-160px;left:-120px;animation:orbB 32s ease-in-out infinite}
.orbs i.c{width:260px;height:260px;background:#FF4D00;top:48%;left:62%;opacity:.05;animation:orbC 40s ease-in-out infinite}
@keyframes orbA{0%,100%{transform:translate(0,0) scale(1)}50%{transform:translate(-90px,70px) scale(1.12)}}
@keyframes orbB{0%,100%{transform:translate(0,0) scale(1)}50%{transform:translate(110px,-80px) scale(.92)}}
@keyframes orbC{0%,100%{transform:translate(0,0)}33%{transform:translate(-70px,-50px)}66%{transform:translate(50px,40px)}}
/* animated grid mesh — the scalus.io ambient layer */
.mesh{position:fixed;inset:0;z-index:0;pointer-events:none;opacity:.35;
  background-image:linear-gradient(rgba(50,207,117,.06) 1px,transparent 1px),linear-gradient(90deg,rgba(50,207,117,.06) 1px,transparent 1px);
  background-size:54px 54px;
  mask-image:radial-gradient(ellipse 80% 60% at 50% 30%, #000 40%, transparent 100%);
  -webkit-mask-image:radial-gradient(ellipse 80% 60% at 50% 30%, #000 40%, transparent 100%);
  animation:meshpan 40s linear infinite}
@keyframes meshpan{from{background-position:0 0,0 0}to{background-position:54px 54px,54px 54px}}
@media(prefers-reduced-motion:reduce){.mesh{animation:none}}

/* ===== Flow visual: inputs → considered decision (ported from scalus.io) ===== */
.flow-wrap{position:relative;width:100%;max-width:520px;margin:0 auto;aspect-ratio:1/1.04}
.flow-wrap svg.wires{position:absolute;inset:0;width:100%;height:100%;z-index:1;overflow:visible}
.flow-wrap svg.wires path{fill:none;stroke:rgba(50,207,117,.22);stroke-width:1.4;stroke-dasharray:4 6}
.flow-wrap svg.wires path.warm{stroke:rgba(255,122,60,.28)}
.flow-cap{position:absolute;font-size:11px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-mute);z-index:4;white-space:nowrap}
.flow-cap.in{left:5%;top:0}
.inn{position:absolute;left:5%;width:50px;height:50px;border-radius:15px;background:var(--panel);border:1px solid var(--line);
  -webkit-backdrop-filter:var(--glass-sm);backdrop-filter:var(--glass-sm);display:flex;align-items:center;justify-content:center;z-index:3;
  box-shadow:0 8px 22px rgba(0,0,0,.4),var(--rim-sm);animation:bobin 6s ease-in-out infinite}
.inn svg{width:23px;height:23px;stroke:#5fe39a}
.inn.warm svg{stroke:#ff9466}
.i1{top:9%}.i2{top:22%}.i3{top:35%}.i4{top:48%}.i5{top:61%}.i6{top:74%}.i7{top:87%}
.i2{animation-delay:-.8s}.i3{animation-delay:-1.6s}.i4{animation-delay:-2.4s}.i5{animation-delay:-3.2s}.i6{animation-delay:-4s}.i7{animation-delay:-4.8s}
.flow-core{position:absolute;width:150px;height:150px;border-radius:38px;left:66%;top:50%;transform:translate(-50%,-50%);
  background:linear-gradient(150deg,rgba(15,38,26,.94),rgba(8,22,15,.92));border:1px solid rgba(50,207,117,.34);
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;z-index:5;-webkit-backdrop-filter:var(--glass-sm);backdrop-filter:var(--glass-sm);
  box-shadow:0 0 64px rgba(50,207,117,.4),inset 0 1px 0 rgba(var(--wa),.14);animation:coreglow 4s ease-in-out infinite}
@keyframes coreglow{0%,100%{box-shadow:0 0 64px rgba(50,207,117,.4),inset 0 1px 0 rgba(var(--wa),.14)}50%{box-shadow:0 0 96px rgba(50,207,117,.62),inset 0 1px 0 rgba(var(--wa),.18)}}
.aimark{width:74px;height:74px;display:block;filter:drop-shadow(0 4px 16px rgba(50,207,117,.5))}
.aimark .petals{transform-origin:60px 60px;animation:knotspin 26s linear infinite}
.aimark .rays{transform-origin:60px 60px;animation:knotspin 40s linear infinite reverse}
@keyframes knotspin{from{transform:rotate(0)}to{transform:rotate(360deg)}}
.aimark .hub{transform-origin:60px 60px;animation:hubpulse 4s ease-in-out infinite}
@keyframes hubpulse{0%,100%{opacity:.85;transform:scale(1)}50%{opacity:1;transform:scale(1.15)}}
.aimark .petal{opacity:.25;stroke:#2f7d56;transition:opacity .7s ease,stroke .7s ease}
.aimark .petal.lit{opacity:1;stroke:#5fe39a}
.aimark .petal.flash{animation:petalpop .8s ease-out}
@keyframes petalpop{0%{opacity:.25}30%{opacity:1;stroke-width:3.4}100%{opacity:1}}
.aimark.solved .hub{animation:hubsolve 1.6s ease-in-out;stroke:#ffcf6a}
@keyframes hubsolve{0%{transform:scale(1);stroke:#ff7a3c}40%{transform:scale(2.1);stroke:#ffe39a;stroke-width:4}100%{transform:scale(1);stroke:#ff7a3c}}
.flow-core.solved{animation:coresolve 1.6s ease-in-out}
@keyframes coresolve{0%,100%{box-shadow:0 0 64px rgba(50,207,117,.4),inset 0 1px 0 rgba(var(--wa),.14)}45%{box-shadow:0 0 140px rgba(120,255,180,.95),0 0 60px rgba(255,180,90,.7),inset 0 1px 0 rgba(var(--wa),.3);border-color:rgba(120,255,180,.7)}}
.aimark .petal.dissolving{animation:petaldissolve .9s ease-in forwards}
@keyframes petaldissolve{0%{opacity:1;stroke:#5fe39a;stroke-width:2.4}55%{opacity:.55;stroke:#bfe8d2}100%{opacity:.25;stroke:#2f7d56;stroke-width:2.4}}
.flow-core span{font-weight:700;font-size:12.5px;color:var(--ink);letter-spacing:-.005em;text-align:center;line-height:1.18;padding:0 8px}
@keyframes bobin{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}}
/* sequenced build: icon pops in → its wire grows → a petal lights up */
.flow-wrap.anim .inn{animation:none!important;opacity:0;transform:scale(.6);transition:opacity .4s ease,transform .5s cubic-bezier(.34,1.56,.64,1)}
.flow-wrap.anim .inn.on{opacity:1;transform:none}
.flow-wrap.anim svg.wires path{clip-path:inset(0 100% 0 0) fill-box;transition:clip-path .6s cubic-bezier(.3,.7,.3,1)}
.flow-wrap.anim svg.wires path.on{clip-path:inset(0 0 0 0) fill-box}
.spark{position:absolute;width:6px;height:6px;border-radius:50%;background:var(--green);box-shadow:0 0 12px var(--green);z-index:6;pointer-events:none}
.spark.warm{background:#ff9466;box-shadow:0 0 12px var(--orange)}
@media(max-width:520px){
  .inn{width:42px;height:42px;border-radius:13px}
  .inn svg{width:20px;height:20px}
  .flow-core{width:120px;height:120px;border-radius:30px;left:65%}
  .aimark{width:58px;height:58px}
  .flow-core span{font-size:10.5px}
  .flow-cap{font-size:9px;letter-spacing:.1em}
}
@media(prefers-reduced-motion:reduce){.flow-wrap *{animation:none!important}.flow-wrap .inn,.flow-wrap svg.wires path{opacity:1!important;transform:none!important;clip-path:none!important}.aimark .petal{opacity:1;stroke:#5fe39a}}

/* JS-tagged reveals */
html.js .mv{opacity:0;transform:translateY(26px);transition:opacity .8s cubic-bezier(.22,1,.36,1),transform .8s cubic-bezier(.22,1,.36,1)}
html.js .mv.in{opacity:1;transform:none}
html.js .mv-l{transform:translateX(-30px)}
html.js .mv-r{transform:translateX(30px)}
html.js .mv-s{transform:scale(.94) translateY(14px)}

/* ── card entrances with a bit more to them ──────────────────────────────
   The generic .mv fade was doing its job so quietly that a card grid looked
   static. Feature cards, the four day steps and the update strip come in with
   a lift and a slight rise out of perspective instead, and their icon lands a
   beat after the card so the eye goes to the icon last. Same .mv/.in wiring, so
   the observer, the stagger delays and the reduced-motion opt out all still
   apply unchanged. */
html.js .fcard.mv,html.js .step.mv,html.js .upcard.mv{
  transform:perspective(900px) translateY(34px) rotateX(6deg) scale(.965);
  transition:opacity .78s cubic-bezier(.22,1,.36,1),transform .78s cubic-bezier(.22,1,.36,1)}
html.js .fcard.mv.in,html.js .step.mv.in,html.js .upcard.mv.in{transform:none}
html.js .fcard.mv .ic,html.js .step.mv .ic{
  opacity:0;transform:scale(.6) rotate(-12deg);
  transition:opacity .5s cubic-bezier(.34,1.56,.64,1) .22s,transform .5s cubic-bezier(.34,1.56,.64,1) .22s}
html.js .fcard.mv.in .ic,html.js .step.mv.in .ic{opacity:1;transform:none}

/* The testimonial pull quotes come in one after another from the left, with the
   green rule drawing down beside each line. They are the proof on the page, so
   they arrive as separate statements rather than as one block of text. Driven by
   the .stagger observer on the list, not by the .mv auto tagger, so the order
   and the delays come straight from the markup. */
.js-anim .gv-quotes.stagger > li{transform:translateX(-20px);border-left-color:transparent;
  transition:opacity .6s cubic-bezier(.22,1,.36,1),transform .6s cubic-bezier(.22,1,.36,1),border-left-color .5s ease}
.js-anim .gv-quotes.stagger.in > li{transform:none;border-left-color:var(--green)}

/* The date a release shipped, arriving a beat after its card. */
.js-anim .upc-date.reveal{transform:translateY(10px);transition:opacity .55s ease .28s,transform .55s ease .28s}
.js-anim .upc-date.reveal.in{transform:none}

@media(prefers-reduced-motion:reduce){
  html.js .fcard.mv,html.js .step.mv,html.js .upcard.mv{opacity:1;transform:none;transition:none}
  html.js .fcard.mv .ic,html.js .step.mv .ic{opacity:1;transform:none;transition:none}
  .js-anim .gv-quotes.stagger > li,.js-anim .upc-date.reveal{opacity:1;transform:none;transition:none;border-left-color:var(--green)}
}

/* Rotating suburb in the hero headline. Green, not the white .grad, because the
   whole point is that the suburb is the one word doing the local work. It fades
   rather than slides so a long name swapping for a short one never jolts the
   line, and the inline-block keeps the baseline steady while it changes. */
.sub-rot{color:var(--green);display:inline-block;transition:opacity .22s ease,transform .22s ease}
.sub-rot.sub-out{opacity:0;transform:translateY(.08em)}
/* The suburb carries a green gradient that sweeps across it as each new name
   lands, so the word reads as something being looked up rather than a label
   sitting there. Painted on the text itself, with the flat green kept as the
   fallback for any engine without background-clip on text. */
@supports ((-webkit-background-clip:text) or (background-clip:text)){
  .sub-rot{background-image:linear-gradient(100deg,#1f9d57 0%,var(--green) 30%,#9dfbc6 50%,var(--green) 70%,#1f9d57 100%);
    background-size:260% 100%;background-position:100% 0;
    -webkit-background-clip:text;background-clip:text;color:transparent}
  .sub-rot.sub-sweep{animation:subSweep .9s cubic-bezier(.3,.7,.3,1) forwards}
}
@keyframes subSweep{from{background-position:100% 0}to{background-position:0 0}}

/* the magnifier that rides beside the suburb */
.sub-ico{display:inline-flex;align-items:center;justify-content:center;vertical-align:baseline;
  width:.62em;height:.62em;margin-left:.16em;color:var(--green)}
.sub-ico svg{width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:2.6;stroke-linecap:round}
.sub-ico.sub-pop{animation:subPop .62s cubic-bezier(.22,1.4,.4,1) both}
@keyframes subPop{0%{opacity:0;transform:scale(.5) rotate(-18deg)}
  60%{opacity:1;transform:scale(1.12) rotate(4deg)}
  100%{opacity:1;transform:none}}

@media(prefers-reduced-motion:reduce){.sub-rot{transition:none;animation:none}
  .sub-ico.sub-pop{animation:none}}

/* value bar: icon over the number, cards arriving one after another */
.value-bar .vb-ico{display:flex;align-items:center;justify-content:center;width:38px;height:38px;
  margin:0 auto 12px;border-radius:12px;background:var(--green-soft)}
.value-bar .vb-ico svg{width:19px;height:19px;stroke:var(--green);fill:none;stroke-width:2.2;
  stroke-linecap:round;stroke-linejoin:round}

/* hero headline word stagger. THE WORDS ARRIVE IN THEIR FINAL PLACE (Tim,
   16 Sep 2026). They used to rise .6em and untwist 2deg one at a time, and on a
   phone that read as the headline reshuffling itself: .6em is 27px at the 46px
   the h1 computes to on a 440px screen, against a 49px line, so a word still
   waiting its turn sat more than half a line below the one beside it. "Small"
   landed while "Businesses" was still down there, the three line headline spent
   its whole entrance looking like a four line one, and then it snapped.
   Nothing travels now. Every word occupies its final box from the first frame
   and simply comes into focus there, so the headline reads as three lines the
   whole way through. A blur is the only thing animating besides the fade, and
   a blur is the one entrance that cannot move anything: it has no geometry, so
   the drift measures 0.0px in both axes across the entire 4s. A scale was tried
   first and is not good enough here, .94 on "Businesses" walked its edge 10px.
   Whatever replaces this, measure the drift before shipping it. */
html.js .hw{display:inline-block;opacity:0;filter:blur(9px);
  animation:hw .9s cubic-bezier(.22,1,.36,1) forwards}
@keyframes hw{to{opacity:1;filter:blur(0)}}

/* button base (shine sweep removed — subtle scale hover instead) */
.btn{position:relative;overflow:hidden}

/* card hover lift+tilt base (JS drives the tilt vars) */
/* .no-tilt opts a card out entirely (e.g. the booking calendar — the tilt made
   the date grid swim around under the cursor). */
@media(hover:hover) and (pointer:fine){
  .panel:not(.no-tilt),.fcard:not(.no-tilt){transition:transform .35s cubic-bezier(.22,1,.36,1),border-color .3s,box-shadow .35s}
  .panel:not(.no-tilt):hover,.fcard:not(.no-tilt):hover{transform:perspective(900px) rotateX(var(--tx,0deg)) rotateY(var(--ty,0deg)) translateY(-4px);
    box-shadow:0 24px 60px rgba(0,0,0,.45),0 0 0 1px rgba(50,207,117,.12)}
}

/* scroll progress bar */
.sprog{position:fixed;top:0;left:0;height:2px;width:100%;z-index:200;pointer-events:none;
  transform-origin:0 50%;transform:scaleX(0);background:linear-gradient(90deg,#32cf75,#7df0ae);
  box-shadow:0 0 12px rgba(50,207,117,.7)}

/* THE ORBS COME OFF ON A PHONE (Tim, 14 Sep 2026: "the map is glitching out on
   mobile", the client dots losing their centres and flashing). Three fixed
   circles with a 70px blur and their own transform animations sit over the
   whole page, and on a phone they were eating most of the frame budget while
   the map ran its flight. Measured on an iPhone viewport against the live site,
   ten seconds of frames each time:
       as shipped            33.3ms median, 58.3ms p95, 41 frames over 50ms
       orbs animation off    24.9ms median
       glow off              25.0ms median
       ORBS OFF               8.4ms median, 17.1ms p95, 0 frames over 50ms
   Stopping the animation is not enough, the blurred layers cost the frames
   whether they move or not. They are background decoration that a phone screen
   barely shows anyway, and the map's blink runs off the frame loop, so at 30fps
   with stalls it strobes. The .glow gradient stays, it is static and cheap. */
@media(max-width:680px){ .orbs{display:none} }

@media(prefers-reduced-motion:reduce){
  .orbs i{animation:none}
  html.js .mv{opacity:1;transform:none;transition:none}
  html.js .hw{animation:none;opacity:1;transform:none;filter:none}
}

/* mobile overflow fix: let the hero feed shrink inside the grid */
.hero-grid > *{min-width:0}
.ui-row > div{min-width:0}
.ui-row small{max-width:100%}
@media(max-width:560px){
  /* keep the channel + time visible on mobile (times must show here too), just
     compact it: shrink the column, drop the redundant text label under the icon
     so the icon + relative time still read clearly without crowding the row. */
  .ui-row .meta{width:62px;align-items:flex-end}
  .ui-row .src i{display:none}
  .ui-row .src .sic{width:34px;height:34px}
  .ui-row .src .sic svg{width:17px;height:17px}
  .ui-row .ago{font-size:.58rem;text-align:right}
  .ui-row small{max-width:none}
}

/* ===== Talk to me now (voice AI callback) ===== */
.talk-now{position:fixed;left:18px;bottom:18px;z-index:70;display:inline-flex;align-items:center;gap:.55em;
  padding:12px 20px;border-radius:999px;cursor:pointer;font:inherit;font-size:.86rem;font-weight:700;
  background:rgba(10,12,11,.78);border:1px solid rgba(50,207,117,.45);color:var(--ink);
  -webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur);
  box-shadow:0 12px 36px rgba(0,0,0,.5),0 0 24px rgba(50,207,117,.18);transition:transform .25s,box-shadow .25s}
.talk-now:hover{transform:translateY(-2px);box-shadow:0 16px 44px rgba(0,0,0,.55),0 0 34px rgba(50,207,117,.3)}
.talk-now .tdot{width:8px;height:8px;border-radius:50%;background:var(--green);animation:pulse 1.6s infinite}
@media(max-width:560px){.talk-now{bottom:84px;font-size:.8rem;padding:11px 16px}}

.talk-modal{position:fixed;inset:0;z-index:120;display:none;align-items:flex-end;justify-content:flex-start;padding:18px}
.talk-modal.open{display:flex}
.talk-modal .back{position:absolute;inset:0;background:rgba(0,0,0,.55);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px)}
.talk-card{position:relative;width:min(360px,calc(100vw - 36px));padding:26px 24px;border-radius:var(--r);
  background:rgba(12,14,13,.92);border:1px solid var(--line-2);
  -webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur);
  box-shadow:0 30px 80px rgba(0,0,0,.6),0 0 40px rgba(50,207,117,.12)}
.talk-card h3{margin:0 0 6px;font-size:1.05rem}
.talk-card p{margin:0 0 16px;font-size:.84rem;color:var(--ink-soft)}
.talk-card input{width:100%;padding:14px 16px;border-radius:var(--r-sm);font:inherit;
  background:rgba(var(--wa),.05);border:1px solid var(--line-2);color:var(--ink);margin-bottom:12px}
.talk-card input:focus{outline:none;border-color:var(--green)}
.talk-card .btn{width:100%}
.talk-card .x{position:absolute;top:12px;right:14px;background:none;border:0;color:var(--ink-mute);
  font-size:1.2rem;cursor:pointer;line-height:1}
.talk-card .fine{margin:10px 0 0;font-size:.7rem;color:var(--ink-mute);text-align:center}
@media(max-width:560px){.talk-modal{justify-content:center;align-items:center}}

/* alternating sections: translucent gradient wash instead of a solid slab —
   no hard edges, and the node-network background stays visible through it */
section.alt{background:linear-gradient(180deg,
  rgba(var(--wa),0) 0%, rgba(var(--wa),.032) 22%,
  rgba(var(--wa),.032) 78%, rgba(var(--wa),0) 100%)}

/* ===========================================================================
   CHAT  (launcher + panel)
   Our own shell, built by buildChat() in sc.js and stamped onto every page, so
   the chat is never a bolted-on third-party bubble that ignores the brand. The
   messages go to /api/chat, which bridges into the Scalus GoHighLevel inbox, so
   an enquiry can be picked up by hand at any point in the conversation.
   =========================================================================== */
.sc-chat{position:fixed;right:18px;bottom:18px;z-index:70;display:flex;align-items:center;gap:10px;
  height:56px;padding:0 18px 0 16px;border-radius:999px;border:1px solid rgba(50,207,117,.5);cursor:pointer;
  background:linear-gradient(135deg,rgba(20,40,28,.92),rgba(10,14,12,.92));color:var(--ink);
  font:inherit;font-size:.86rem;font-weight:700;
  -webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur);
  box-shadow:0 14px 40px rgba(0,0,0,.55),0 0 28px rgba(50,207,117,.25);
  transition:transform .25s,box-shadow .25s,opacity .25s}
.sc-chat:hover{transform:translateY(-2px) scale(1.03);box-shadow:0 18px 48px rgba(0,0,0,.6),0 0 40px rgba(50,207,117,.4)}
.sc-chat.hide{opacity:0;pointer-events:none;transform:translateY(8px)}
.sc-chat .ic{width:34px;height:34px;border-radius:50%;display:grid;place-items:center;flex:none;
  background:var(--green);color:#04130a;box-shadow:0 0 16px rgba(50,207,117,.6)}
.sc-chat .ic svg{width:18px;height:18px}
.sc-chat .on{position:absolute;top:4px;right:6px;width:9px;height:9px;border-radius:50%;
  background:var(--green);border:2px solid #0a0c0b;animation:pulse 1.6s infinite}

/* THE DOCK'S GLASS (Tim, 6 Oct 2026: "make the chatbot bubble glassy, sides
   reflective like the dock, Netflix style"). Clear lifted glass round the
   green disc, the dock's rotating conic rim on ::before so the sides catch a
   travelling light, and a specular sheen across the top on ::after. */
.sc-chat{isolation:isolate;border-color:transparent;
  background:linear-gradient(180deg,rgba(var(--wa),.14),rgba(var(--wa),.04));
  -webkit-backdrop-filter:blur(16px) saturate(210%) brightness(1.22);
  backdrop-filter:blur(16px) saturate(210%) brightness(1.22);
  box-shadow:0 18px 40px -12px rgba(0,0,0,.7),0 3px 12px rgba(0,0,0,.35),0 0 26px rgba(50,207,117,.22),
    inset 0 1px 0 rgba(var(--wa),.22),inset 0 -1px 0 rgba(var(--wa),.06)}
.sc-chat::before{content:'';position:absolute;inset:0;border-radius:inherit;padding:1.5px;z-index:2;pointer-events:none;
  background:conic-gradient(from var(--sc-rim),
    rgba(var(--wa),.75),rgba(var(--wa),.06) 18%,rgba(140,255,190,.35) 38%,rgba(var(--wa),.05) 55%,
    rgba(var(--wa),.6) 76%,rgba(var(--wa),.08) 90%,rgba(var(--wa),.75));
  -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;
  animation:scDockRim 9s linear infinite}
.sc-chat::after{content:'';position:absolute;inset:0;border-radius:inherit;z-index:1;pointer-events:none;
  background:radial-gradient(60px 30px at 35% 0%,rgba(var(--wa),.35),rgba(var(--wa),0) 70%)}
.sc-chat .ic{position:relative;z-index:3}
.sc-chat .on{z-index:4}

/* "Ask me anything" (Tim, 6 Oct 2026): a glass speech bubble to the left of
   the button, its corner nearest the button squared off as the tail. It
   arrives a moment after the page and goes with the button whenever the
   button hides (the panel open, the focus states). */
.sc-chat .ask{position:absolute;right:calc(100% + 12px);top:50%;z-index:3;pointer-events:none;
  padding:9px 14px;border-radius:16px 16px 5px 16px;white-space:nowrap;
  font-size:.82rem;font-weight:700;letter-spacing:.005em;color:var(--ink);
  background:linear-gradient(180deg,rgba(var(--wa),.16),rgba(var(--wa),.06));
  -webkit-backdrop-filter:blur(16px) saturate(200%) brightness(1.2);
  backdrop-filter:blur(16px) saturate(200%) brightness(1.2);
  box-shadow:0 12px 30px -10px rgba(0,0,0,.6),inset 0 1px 0 rgba(var(--wa),.22),0 0 0 1px rgba(var(--wa),.12);
  transform:translateY(-50%);animation:scAskIn .5s cubic-bezier(.2,.9,.25,1) 1.4s backwards}
@keyframes scAskIn{from{opacity:0;transform:translate(10px,-50%) scale(.92)}}
@media(prefers-reduced-motion:reduce){.sc-chat::before,.sc-chat .ask{animation:none}}
@media(max-width:560px){.sc-chat{height:52px;padding:0 14px;font-size:.8rem;right:14px}}
/* ICON ONLY (Tim, 30 Sep 2026: "have the chat as a small icon on the right,
   no chat with me, just the icon"). The dock is now just Enquire and Call at
   every width, so the chat is a round button bottom right, centred on the
   dock's ~69px bar (18px off the bottom) so the two share a line. The label
   stays in the markup for screen readers via aria-label on the button. */
/* On the dock's line exactly (Tim, 6 Oct 2026: "make sure Ask me anything
   lines up with the bottom dock"): the same 46px height and the same 18px off
   the bottom as the 46px Enquire / Call bar, so the dock, the bubble and the
   button share a top edge and a bottom edge. */
@media(min-width:681px){
  .sc-chat{width:46px;height:46px;padding:0;gap:0;justify-content:center;
    right:22px;bottom:calc(18px + env(safe-area-inset-bottom))}
  .sc-chat .lb{display:none}
  .sc-chat .ic{width:36px;height:36px}
  .sc-chat .ic svg{width:18px;height:18px}
  html.ch-dock-off .sc-chat{bottom:calc(18px + env(safe-area-inset-bottom))}
  .sc-chat .ask{height:46px;display:flex;align-items:center;padding:0 16px;border-radius:23px 23px 6px 23px}
}
/* No floating "Chat with me" pill on phones at all (Tim, 2 Sep 2026). It sat on
   top of the hero form fields. This is unconditional, unlike the has dock rule
   below: even nodock ad funnels lose the pill on phones. The dock, where a page
   has one, still carries Chat; the chat panel itself is untouched. */
@media(max-width:680px){.sc-chat{display:none !important}}

/* ----- the panel -----
   The same piece of glass as the dock and the nav bar, turned up: the page runs
   right through it, and a slow chromatic bloom drifts around inside so the
   surface throws colour the way real thick glass does.

   ONE backdrop-filter, on the panel. Nothing inside it gets its own, because an
   element with backdrop-filter becomes the backdrop root for everything under
   it, so a nested one samples the panel rather than the page and just goes
   grey. The head, bubbles and composer are translucent fills and highlights on
   top of the single sheet instead.

   Layers, bottom to top: the panel's own background (a dark tint plus a fixed
   diagonal sheen) · ::after, the drifting colour, screen blended so it reads as
   light rather than paint · the content at z-index 2 · ::before, the rim. */
.sc-cw{position:fixed;right:18px;bottom:18px;z-index:121;width:388px;max-width:calc(100vw - 36px);
  height:min(600px,calc(100vh - 110px));display:flex;flex-direction:column;overflow:hidden;
  isolation:isolate;
  border-radius:22px;border:1px solid transparent;
  background:
    linear-gradient(135deg,rgba(var(--wa),.13),rgba(var(--wa),.03) 34%,rgba(var(--wa),0) 60%),
    linear-gradient(180deg,rgba(22,30,25,.4),rgba(5,8,6,.58));
  -webkit-backdrop-filter:blur(44px) saturate(210%) brightness(1.2);
  backdrop-filter:blur(44px) saturate(210%) brightness(1.2);
  box-shadow:0 30px 90px rgba(0,0,0,.72),0 0 60px rgba(50,207,117,.14),
    inset 0 1px 0 rgba(var(--wa),.22),inset 0 -1px 0 rgba(var(--wa),.06);
  /* Closing sucks the panel into the launcher's green circle, the way a window
     drops into the dock. sc.js sets transform-origin to the middle of that
     circle (the fallback below is the bottom right corner, for no JS), so the
     sheet is pulled toward the icon rather than merely shrinking in place. It
     collapses almost to nothing and stays opaque while it travels, so the move
     is watched rather than faded through. Opening runs it in reverse. */
  /* Tim, 16 Sep 2026: "expand more naturally, Apple style". The old open
     span the panel up from a dot with a twist, which read as a pop. Now it
     grows out of the launcher the way an iOS sheet does: from about half
     size, on Apple's own sheet curve (.32,.72,0,1), no rotation, the corner
     radius easing from the pill to the card as it goes, and the contents
     rising in a beat behind the glass. Closing is the same move reversed and
     a touch quicker, the way a sheet drops. */
  opacity:0;pointer-events:none;transform-origin:100% 100%;
  transform:scale(.55) translate3d(0,14px,0);border-radius:36px;
  transition:opacity .22s ease,
             transform .36s cubic-bezier(.32,.72,0,1),
             border-radius .36s cubic-bezier(.32,.72,0,1)}
.sc-cw.open{opacity:1;pointer-events:auto;transform:none;border-radius:22px;
  transition:opacity .28s ease,transform .6s cubic-bezier(.32,.72,0,1),border-radius .6s cubic-bezier(.32,.72,0,1)}
/* the contents let go first, so what gets pulled in is the glass, not squashed
   text riding the shrink down; on the way in they rise a beat behind it */
.sc-cw .cw-head,.sc-cw .cw-note,.sc-cw .cw-body,.sc-cw .cw-in{opacity:0;transform:translateY(8px);transition:opacity .12s ease,transform .12s ease}
.sc-cw.open .cw-head,.sc-cw.open .cw-note,.sc-cw.open .cw-body,.sc-cw.open .cw-in{
  opacity:1;transform:none;transition:opacity .4s ease .18s,transform .55s cubic-bezier(.32,.72,0,1) .18s}
.sc-cw.open .cw-note{transition-delay:.24s}
.sc-cw.open .cw-body{transition-delay:.3s}
.sc-cw.open .cw-in{transition-delay:.36s}
@media(prefers-reduced-motion:reduce){
  .sc-cw{transform:none;transition:opacity .2s ease}
  .sc-cw.open{transition:opacity .2s ease}}

/* the rim. Prismatic rather than plain white: green, then a warm flash off the
   logo orange, then a cold blue, so the edge changes colour as it travels. */
.sc-cw::before{content:'';position:absolute;inset:0;border-radius:inherit;padding:1px;z-index:4;
  pointer-events:none;
  background:conic-gradient(from var(--sc-rim),
    rgba(var(--wa),.65),rgba(130,255,205,.4) 12%,rgba(var(--wa),.04) 24%,
    rgba(50,207,117,.6) 36%,rgba(255,168,90,.4) 47%,rgba(var(--wa),.05) 57%,
    rgba(125,205,255,.5) 69%,rgba(var(--wa),.62) 81%,rgba(var(--wa),.06) 92%,
    rgba(var(--wa),.65));
  -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;
  animation:scDockRim 22s linear infinite}

/* the colour inside the glass. Oversized and slowly turning, so the blooms swim
   across the panel rather than sitting in the corners. Screen blend keeps it
   reading as light caught in the glass instead of a coloured wash over it. */
.sc-cw::after{content:'';position:absolute;inset:-40%;z-index:0;pointer-events:none;
  mix-blend-mode:screen;
  background:
    radial-gradient(38% 30% at 24% 20%,rgba(50,207,117,.62),rgba(50,207,117,0) 70%),
    radial-gradient(34% 26% at 78% 30%,rgba(90,196,255,.46),rgba(90,196,255,0) 72%),
    radial-gradient(40% 32% at 62% 84%,rgba(255,142,60,.42),rgba(255,142,60,0) 72%),
    radial-gradient(30% 24% at 14% 78%,rgba(168,120,255,.38),rgba(168,120,255,0) 72%);
  animation:cwBloom 34s linear infinite}
@keyframes cwBloom{
  from{transform:rotate(0deg) scale(1)}
  50%{transform:rotate(180deg) scale(1.12)}
  to{transform:rotate(360deg) scale(1)}}

/* content rides above the bloom */
.sc-cw .cw-head,.sc-cw .cw-note,.sc-cw .cw-body,.sc-cw .cw-in{position:relative;z-index:2}

.sc-cw .cw-head{display:flex;align-items:center;gap:12px;padding:14px 14px 13px 16px;flex:none;
  border-bottom:1px solid rgba(var(--wa),.1);
  background:linear-gradient(135deg,rgba(30,66,46,.4),rgba(12,16,14,.28))}
/* light catching the top edge of the sheet, sweeping across it */
.sc-cw .cw-head::after{content:'';position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(190px 60px at var(--cw-spec,10%) -30%,
    rgba(var(--wa),.3),rgba(var(--wa),.06) 46%,rgba(var(--wa),0) 74%);
  animation:cwSpec 13s ease-in-out infinite}
@property --cw-spec{syntax:'<percentage>';inherits:false;initial-value:10%}
@keyframes cwSpec{0%{--cw-spec:6%}50%{--cw-spec:94%}100%{--cw-spec:6%}}
.sc-cw .cw-head>*{position:relative;z-index:1}
.sc-cw .cw-head img{width:38px;height:38px;border-radius:50%;object-fit:cover;flex:none}
.sc-cw .cw-head .t{min-width:0}
.sc-cw .cw-head b{display:block;font-size:.92rem;line-height:1.25}
.sc-cw .cw-head small{display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--ink-mute)}
.sc-cw .cw-head small i{width:7px;height:7px;border-radius:50%;background:var(--green);animation:pulse 1.6s infinite;flex:none}
/* Out of hours the panel wears the Scalus mark instead of Tim's face, and the
   live dot stops pulsing. Contained rather than cropped: a logo is not a
   headshot, and cover would eat its edges. */
.sc-cw .cw-head img.mark{object-fit:contain;padding:6px;background:rgba(var(--wa),.08);
  border:1px solid rgba(var(--wa),.14)}
.sc-cw .cw-head small.off i{background:var(--ink-mute);animation:none}
.sc-cw .cw-x{margin-left:auto;flex:none;width:32px;height:32px;border-radius:50%;cursor:pointer;
  display:grid;place-items:center;font-size:1rem;line-height:1;transition:.2s;
  background:linear-gradient(165deg,rgba(var(--wa),.18),rgba(var(--wa),.06));
  border:1px solid rgba(var(--wa),.16);color:var(--ink-soft);
  box-shadow:inset 0 1px 0 rgba(var(--wa),.28)}
.sc-cw .cw-x:hover{background:linear-gradient(165deg,rgba(var(--wa),.26),rgba(var(--wa),.1));color:var(--ink)}

/* the standing note under the header: where this goes, and that nobody waits */
.sc-cw .cw-note{flex:none;padding:9px 16px 10px;font-size:.7rem;line-height:1.45;
  color:var(--ink-soft);background:rgba(var(--wa),.035);
  border-bottom:1px solid rgba(var(--wa),.08)}

.sc-cw .cw-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:18px 16px;display:flex;
  flex-direction:column;gap:10px;scrollbar-width:thin}
.sc-cw .cw-msg{max-width:84%;padding:11px 14px;border-radius:16px;font-size:.86rem;line-height:1.5;
  white-space:pre-wrap;overflow-wrap:anywhere;animation:cwIn .3s cubic-bezier(.2,.8,.2,1) both}
@keyframes cwIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.sc-cw .cw-msg.bot{align-self:flex-start;
  background:linear-gradient(165deg,rgba(var(--wa),.13),rgba(var(--wa),.05));
  border:1px solid rgba(var(--wa),.14);
  box-shadow:inset 0 1px 0 rgba(var(--wa),.22),0 6px 18px rgba(0,0,0,.28);
  border-bottom-left-radius:6px;color:var(--ink)}
.sc-cw .cw-msg.me{align-self:flex-end;background:linear-gradient(135deg,rgba(74,225,140,.92),rgba(34,160,90,.92));
  box-shadow:inset 0 1px 0 rgba(var(--wa),.4),0 6px 18px rgba(15,80,45,.35);
  border-bottom-right-radius:6px;color:#04130a;font-weight:600}
/* Tim answering by hand. Not the automatic reply, so it is signed and sits in
   blue glass instead of the assistant's white. */
.sc-cw .cw-msg.tim{background:linear-gradient(165deg,rgba(120,180,255,.26),rgba(56,120,220,.12));
  border-color:rgba(140,190,255,.34);
  box-shadow:inset 0 1px 0 rgba(var(--wa),.3),0 6px 18px rgba(10,36,80,.32)}
.sc-cw .cw-msg .cw-au{display:block;font-size:.66rem;font-weight:800;letter-spacing:.08em;
  text-transform:uppercase;color:#9ecdff;margin-bottom:4px}
.sc-cw .cw-msg.typing{display:flex;gap:5px;align-items:center;padding:14px}
.sc-cw .cw-msg.typing i{width:6px;height:6px;border-radius:50%;background:var(--ink-mute);animation:cwDot 1.2s infinite}
.sc-cw .cw-msg.typing i:nth-child(2){animation-delay:.18s}
.sc-cw .cw-msg.typing i:nth-child(3){animation-delay:.36s}
@keyframes cwDot{0%,60%,100%{opacity:.25;transform:translateY(0)}30%{opacity:1;transform:translateY(-3px)}}

/* The quick replies (Tim, 24 Aug 2026). They used to be four different coloured
   words sitting in the flow: pretty, but green, blue, orange and purple text on
   a dark glass panel is four different contrast problems and none of them read
   as a button. Now they are one green on a soft green fill inside a rounded
   border, so the row is one set of tappable pills and every word is legible. */
.sc-cw .cw-chips{display:flex;flex-wrap:wrap;gap:8px;margin:6px 0 4px 2px;font-size:.82rem}
.sc-cw .cw-chips button{font:inherit;font-weight:700;cursor:pointer;text-align:left;
  padding:7px 13px;border-radius:999px;
  color:#bdf7d5;background:rgba(50,207,117,.12);border:1px solid rgba(50,207,117,.42);
  transition:background .2s ease,border-color .2s ease,color .2s ease,transform .15s ease}
.sc-cw .cw-chips button:hover{background:rgba(50,207,117,.2);border-color:rgba(50,207,117,.7);color:#e4fff0}
.sc-cw .cw-chips button:active{transform:scale(.97)}
.sc-cw .cw-chips button:focus-visible{outline:2px solid var(--green);outline-offset:2px}

/* the details card: the one place a chat enquiry becomes a real lead */
.sc-cw .cw-card{align-self:stretch;position:relative;z-index:2;padding:15px;border-radius:16px;
  background:linear-gradient(165deg,rgba(var(--wa),.13),rgba(var(--wa),.045));
  border:1px solid rgba(50,207,117,.34);display:grid;gap:8px;
  box-shadow:inset 0 1px 0 rgba(var(--wa),.24),0 8px 24px rgba(0,0,0,.3);
  animation:cwIn .3s cubic-bezier(.2,.8,.2,1) both}
.sc-cw .cw-card b{font-size:.86rem}
.sc-cw .cw-card p{font-size:.76rem;color:var(--ink-soft);margin:-3px 0 3px;line-height:1.45}
.sc-cw .cw-card input{width:100%;padding:11px 13px;border-radius:12px;font:inherit;font-size:.84rem;
  background:rgba(var(--wa),.05);border:1px solid var(--line-2);color:var(--ink)}
.sc-cw .cw-card input:focus{outline:none;border-color:var(--green)}
.sc-cw .cw-card button{font:inherit;font-size:.84rem;font-weight:700;cursor:pointer;padding:11px 14px;
  border-radius:12px;border:0;background:var(--green);color:#04130a;transition:.2s}
.sc-cw .cw-card button:hover{filter:brightness(1.08)}
.sc-cw .cw-card button[disabled]{opacity:.6;cursor:default}
.sc-cw .cw-card .err{font-size:.74rem;color:#ff8a6a}
.sc-cw .cw-card .fine{font-size:.68rem;color:var(--ink-mute);text-align:center;margin:0}

.sc-cw .cw-in{flex:none;display:flex;gap:9px;padding:12px 13px;border-top:1px solid rgba(var(--wa),.1);
  background:linear-gradient(180deg,rgba(var(--wa),.02),rgba(var(--wa),.07))}
.sc-cw .cw-in input{flex:1 1 auto;min-width:0;padding:12px 15px;border-radius:999px;font:inherit;font-size:.86rem;
  background:linear-gradient(165deg,rgba(var(--wa),.12),rgba(var(--wa),.045));
  border:1px solid rgba(var(--wa),.16);color:var(--ink);
  box-shadow:inset 0 1px 0 rgba(var(--wa),.2)}
/* the browser default placeholder disappears against the glass, so set it white */
.sc-cw .cw-in input::placeholder{color:#fff;opacity:.82}
.sc-cw .cw-in input:focus{outline:none;border-color:rgba(50,207,117,.75);
  box-shadow:inset 0 1px 0 rgba(var(--wa),.2),0 0 0 3px rgba(50,207,117,.16)}
.sc-cw .cw-in button{flex:none;width:42px;height:42px;border-radius:50%;border:0;cursor:pointer;
  display:grid;place-items:center;color:#04130a;transition:.2s;
  background:linear-gradient(160deg,#5cf09b,#22a75c);
  box-shadow:inset 0 1px 0 rgba(var(--wa),.5),0 4px 16px rgba(50,207,117,.4)}
.sc-cw .cw-in button:hover{filter:brightness(1.08);transform:scale(1.05)}
.sc-cw .cw-in button svg{width:18px;height:18px}

/* On a phone the panel is a sheet: full width, and short enough that the
   keyboard opening never pushes the composer off screen. */
@media(max-width:560px){
  .sc-cw{right:10px;left:10px;bottom:10px;width:auto;max-width:none;height:min(78vh,calc(100vh - 90px));border-radius:20px;
    /* the sheet covers most of the screen here, so it carries a little more
       tint than the desktop panel. Any less and the headline behind it competes
       with the conversation for the same pixels. */
    background:
      linear-gradient(135deg,rgba(var(--wa),.12),rgba(var(--wa),.03) 34%,rgba(var(--wa),0) 60%),
      linear-gradient(180deg,rgba(20,27,23,.58),rgba(4,7,5,.74))}
  .sc-cw .cw-msg{max-width:88%}
}
@media(prefers-reduced-motion:reduce){
  .sc-cw,.sc-cw .cw-msg,.sc-cw .cw-card{transition:none;animation:none}
  .sc-cw .cw-msg.typing i{animation:none}
  .sc-cw::before,.sc-cw::after,.sc-cw .cw-head::after{animation:none}
}

/* ===== reels: iPhone-framed example videos ===== */
.reels{display:grid;grid-template-columns:repeat(3,1fr);gap:26px;max-width:880px;margin:0 auto}
.reel{position:relative;border-radius:40px;padding:10px;background:linear-gradient(180deg,#1a1d1c,#0b0d0c);
  border:1px solid var(--line-2);box-shadow:0 30px 70px rgba(0,0,0,.6), inset 0 1px 0 rgba(var(--wa),.08)}
.reel::before{content:"";position:absolute;inset:3px;border-radius:37px;border:1px solid rgba(var(--wa),.05);pointer-events:none}
.reel .scr{position:relative;border-radius:30px;overflow:hidden;aspect-ratio:9/16;background:#000}
.reel video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
.reel .island{position:absolute;top:12px;left:50%;transform:translateX(-50%);width:72px;height:18px;
  border-radius:999px;background:#000;z-index:3;box-shadow:0 0 0 1px rgba(var(--wa),.07)}
.reel .snd{position:absolute;right:12px;bottom:12px;z-index:4;width:38px;height:38px;border-radius:50%;
  display:grid;place-items:center;cursor:pointer;border:1px solid rgba(var(--wa),.25);color:#fff;
  background:rgba(0,0,0,.55);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);transition:.25s}
.reel .snd:hover{background:rgba(0,0,0,.75)}
.reel .snd svg{width:17px;height:17px}
.reel .tag{position:absolute;left:12px;bottom:14px;z-index:4;font-size:.6rem;font-weight:800;letter-spacing:.14em;
  text-transform:uppercase;color:#fff;padding:5px 11px;border-radius:999px;
  background:rgba(0,0,0,.55);border:1px solid rgba(var(--wa),.2);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}
@media(max-width:760px){.reels{grid-template-columns:repeat(3,1fr);gap:14px}.reel{border-radius:30px;padding:7px}
  .reel .scr{border-radius:23px}.reel::before{border-radius:27px}.reel .island{width:54px;height:14px;top:9px}
  .reel .snd{width:32px;height:32px;right:9px;bottom:9px}.reel .tag{left:9px;bottom:11px;font-size:.52rem;padding:4px 9px}}

/* ===== posting queue / scheduler mock ===== */
.queue{padding:22px;display:grid;gap:10px}
.queue .qh{display:flex;align-items:center;gap:10px;margin-bottom:6px}
.queue .qh b{font-size:.9rem}
.queue .qh small{color:var(--ink-mute);font-size:.7rem}
.qrow{display:flex;align-items:center;gap:12px;padding:12px 14px;border-radius:var(--r-sm);
  background:rgba(var(--wa),.04);border:1px solid var(--line);font-size:.82rem}
.qrow .th{width:34px;height:46px;border-radius:8px;flex:none;background:linear-gradient(135deg,#143425,#0c1410);
  border:1px solid rgba(50,207,117,.25);display:grid;place-items:center;color:var(--green)}
.qrow .th svg{width:14px;height:14px}
.qrow b{display:block;font-size:.8rem;line-height:1.3}
.qrow small{color:var(--ink-mute);font-size:.68rem;display:block}
.qrow .when{margin-left:auto;flex:none;text-align:right;font-size:.66rem;color:var(--ink-mute);font-weight:600;line-height:1.5}
.qrow .pl{display:flex;gap:4px;justify-content:flex-end}
.qrow .pl i{font-style:normal;font-size:.56rem;font-weight:800;letter-spacing:.06em;padding:2px 7px;border-radius:999px;
  background:var(--green-soft);border:1px solid rgba(50,207,117,.3);color:var(--green)}

/* ===== platform (omnipresent) grid ===== */
.plats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:14px}
.plat{padding:22px 18px;text-align:center}
.plat .pic{width:44px;height:44px;border-radius:14px;margin:0 auto 12px;display:grid;place-items:center;
  background:var(--green-soft);border:1px solid rgba(50,207,117,.3);color:var(--green)}
.plat .pic svg{width:21px;height:21px}
.plat b{display:block;font-size:.88rem}
.plat small{color:var(--ink-mute);font-size:.72rem}

/* 3-up feature grid (meta-ads management) */
@media(min-width:761px){.fgrid-3{grid-template-columns:repeat(3,1fr)}}

/* feed channel pills → green icon + tiny label */
.ui-row .src{background:none;border:0;padding:0;margin-left:0;display:flex;flex-direction:column;align-items:center;gap:3px}
.ui-row .src .sic{width:30px;height:30px;border-radius:50%;display:grid;place-items:center;
  background:var(--green-soft);border:1px solid rgba(50,207,117,.3);color:var(--green)}
.ui-row .src .sic svg{width:15px;height:15px}
.ui-row .src i{font-style:normal;font-size:.54rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-mute)}

/* "Updating live" feed caption */
.feed-live{display:flex;align-items:center;justify-content:flex-end;gap:7px;font-size:.7rem;color:var(--ink-mute);margin-top:10px}
.feed-live .dot{width:7px;height:7px;border-radius:50%;background:var(--green);animation:pulse 1.6s infinite}

/* fixed-width centered channel column so icons/labels/times align across rows */
.ui-row .meta{width:84px;flex:none;align-items:center}
.ui-row .meta .ago{text-align:center}

/* ensure feed is visible and timestamps display on all screens */
@media(max-width:560px){
  .ui{margin-bottom:16px}
  .ui-row .meta{width:60px}
  .ui-row .ago{font-size:.56rem;font-weight:600}
}

/* prominent feed header title */
.ui-top .feed-title{display:flex;align-items:center;gap:9px;
  font-size:.95rem;font-weight:800;letter-spacing:-.01em;color:var(--ink)}
.ui-top .feed-title .dot{width:10px;height:10px;border-radius:50%;background:var(--green);
  box-shadow:0 0 10px rgba(50,207,117,.9),0 0 22px rgba(50,207,117,.5);animation:pulse 1.6s infinite}

/* ============================================================
   PORTFOLIO / OUR WORK  (case-study showcase + work pages)
   flat hover only — NO 3D perspective tilt (standing rule)
   ============================================================ */
.work-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(330px,1fr));gap:22px;margin-top:8px}
.work-card{display:flex;flex-direction:column;background:var(--panel);border:1px solid var(--line);
  border-radius:var(--r);overflow:hidden;text-decoration:none;color:inherit;
  transition:transform .3s cubic-bezier(.22,1,.36,1),border-color .3s,box-shadow .3s}
.work-card:hover{transform:translateY(-5px);border-color:var(--line-2);
  box-shadow:0 22px 50px rgba(0,0,0,.45)}
.work-card .shot{position:relative;aspect-ratio:16/10;overflow:hidden;background:var(--bg-2);
  border-bottom:1px solid var(--line)}
.work-card .shot img{width:100%;height:100%;object-fit:cover;object-position:top center;display:block;
  transition:transform .5s cubic-bezier(.22,1,.36,1)}
.work-card:hover .shot > img{transform:scale(1.04)}
/* The chip sits on a screenshot, so its pill is dark in BOTH themes and its
   text and leaf are pinned to literal colours rather than the theme tokens.
   Tim, 13 Sep 2026: on var(--ink-soft) the label went dark grey on a dark pill
   in light mode and could not be read, and var(--green) swaps to the darker
   #12a155 there, so the leaf stopped being Scalus green. Both are fixed values
   here for that reason, and this is the exception to the tokens rule. */
/* EACH PILL WEARS ITS OWN CLIENT'S COLOUR (Tim, 17 Sep 2026: "put different
   colour glassy borders on all of these ... actually reflect the primary
   colours of each website"). Nine identical grey pills on nine different
   websites said nothing about any of them; now the rim, the tint and the icon
   all come from that client's own brand, so a row of cards reads as a row of
   businesses rather than a row of screenshots.

   One token per card does it. --cb is the brand colour, --cb-lift is the same
   hue pulled toward white so a navy or a maroon still shows a rim on a dark
   glass pill, and everything else is mixed off those two, which is why adding
   a client below is one line rather than a new rule. */
.work-card .shot .tag{position:absolute;top:12px;left:12px;z-index:3;font-size:.66rem;font-weight:700;
  letter-spacing:.1em;text-transform:uppercase;padding:6px 11px;border-radius:999px;
  --cb:#8f98a3;
  --cb-lift:color-mix(in oklab,var(--cb) 74%,#fff);
  /* The glass itself: a diagonal wash of the brand over a near black base, so
     the pill still separates from a pale screenshot and the colour reads as a
     tint in the glass rather than a filled chip. */
  background:
    linear-gradient(140deg,
      color-mix(in srgb,var(--cb) 34%,transparent),
      color-mix(in srgb,var(--cb) 9%,transparent) 48%,
      color-mix(in srgb,var(--cb) 26%,transparent)),
    rgba(7,8,8,.6);
  -webkit-backdrop-filter:var(--glass-sm);backdrop-filter:var(--glass-sm);
  border:1px solid color-mix(in srgb,var(--cb-lift) 62%,transparent);
  /* A short throw of the brand under the pill, the white specular along the
     top edge that makes it read as glass, and the house rim underneath. */
  box-shadow:
    0 8px 20px -12px color-mix(in srgb,var(--cb) 75%,transparent),
    inset 0 1px 0 rgba(255,255,255,.34),
    inset 0 -1px 0 color-mix(in srgb,var(--cb) 26%,transparent),
    var(--rim-sm);
  color:#fff;
  display:inline-flex;align-items:center;gap:.5em}
.work-card .shot .tag svg{width:13px;height:13px;flex:none;color:var(--cb-lift)}
/* The brand map. The first nine are read straight out of each client's own
   stylesheet, the token named beside them; the rest are measured off their
   site because the build is not one of ours to read. Keyed on the href, which
   is the one thing about a card that is the same on every page it appears on. */
.work-card[href*="22-electrical-works"] .shot .tag{--cb:#00a2e2}  /* 22EW --blue */
.work-card[href*="federation-flags"]    .shot .tag{--cb:#c8102e}  /* Flags --red */
.work-card[href*="/eden/"]              .shot .tag{--cb:#a8c92f}  /* Eden --leaf-bright */
.work-card[href*="/qcs/"]               .shot .tag{--cb:#2a7cab}  /* QCS --steel-2, the navy's own lighter sibling */
.work-card[href*="jt-handyman"]         .shot .tag{--cb:#e7161f}  /* JT brand red */
.work-card[href*="fullmoon"]            .shot .tag{--cb:#d2ae81}  /* Full Moon --gold */
.work-card[href*="soak"]                .shot .tag{--cb:#e8b667}  /* Soak Haus --gold */
.work-card[href*="conquerchronichealth"] .shot .tag{--cb:#34bbc0} /* CCH --teal */
.work-card[href*="pasha-legal"]         .shot .tag{--cb:#8a8f97}  /* Pasha --silver */
.work-card[href*="prolifica"]           .shot .tag{--cb:#f4f2ee}  /* Prolifica --ink, bone on black */
.work-card[href*="/biba/"]              .shot .tag{--cb:#c3a46c}  /* Biba gold */
.work-card[href*="/sprint/"]            .shot .tag{--cb:#c76b1e}  /* Sprint orange */
.work-card[href*="steiner"]             .shot .tag{--cb:#f38a39}  /* Steiner orange */

/* ── work card auto scroll ───────────────────────────────────────────────
   The 16:10 crop only ever showed the top of a site, which is the one part
   every website has. The card now pans down through the whole page instead,
   in slow creeps with a random pause between each, so a passing glance lands
   on a different part of the build each time.

   The tall shot is a second layer that JS loads on top of the crop, desktop
   only and only once the card is on screen: the crop stays the real markup,
   so a crawler, a phone and a browser with no JS all get the light image and
   nothing waits on a 6000px screenshot. */
.work-card .shot .shot-pan{position:absolute;inset:0;overflow:hidden;opacity:0;z-index:2;
  transition:opacity .55s ease}
.work-card .shot.pan-on .shot-pan{opacity:1}
/* the turn at the bottom: fade, jump back to the top behind the fade, fade in,
   so the card never shows itself rewinding */
.work-card .shot.pan-on.pan-turn .shot-pan{opacity:0;transition:opacity .4s ease}
.work-card .shot .shot-pan img{position:absolute;top:0;left:0;width:100%;height:auto;display:block;
  will-change:transform;transform:translate3d(0,0,0);
  transition:transform var(--pan-ms,4000ms) cubic-bezier(.36,0,.64,1)}
/* the base crop's hover zoom would show through under a translucent edge */
.work-card:hover .shot.pan-on > img{transform:none}
@media(prefers-reduced-motion:reduce){
  .work-card .shot .shot-pan{display:none}
}
.work-card .body{padding:20px 22px 22px;display:flex;flex-direction:column;gap:8px;flex:1}
.work-card .body h2,.work-card .body h3{margin:0;font-size:1.18rem;letter-spacing:-.01em}
.work-card .body p{margin:0;font-size:.9rem;color:var(--ink-soft);line-height:1.55}
.work-card .stat-row{display:flex;flex-wrap:wrap;gap:6px;margin-top:auto;padding-top:12px}
.work-card .stat-row span{font-size:.68rem;font-weight:600;letter-spacing:.04em;color:var(--ink-mute);
  border:1px solid var(--line);border-radius:999px;padding:4px 10px}
.work-card .more{margin-top:12px;font-size:.82rem;font-weight:700;color:var(--green);
  display:inline-flex;align-items:center;gap:6px}
.work-card:hover .more{gap:9px}

/* case-study page */
.cs-hero{padding:150px 0 30px}
@media(max-width:560px){.cs-hero{padding-top:146px}}
.cs-eyebrow{display:inline-flex;gap:.6em;font-size:.74rem;font-weight:700;letter-spacing:.18em;
  text-transform:uppercase;color:var(--green)}
.cs-shot{margin:30px 0 8px;border:1px solid var(--line);border-radius:var(--r);overflow:hidden;
  box-shadow:0 30px 70px rgba(0,0,0,.5);background:var(--bg-2)}
.cs-shot img{width:100%;height:auto;display:block}

/* ── CLIENT SPOTLIGHT (Tim, 25 Aug 2026, rebuilt 27 Aug 2026) ─────────────
   One client at a time, picked fresh on every load: the story on the left, and
   on the right their actual site running on a laptop with the phone beside it.

   It used to be the client's site blown up to fill the whole band as a living
   background. The problem was the veil: to keep white copy readable over a site
   that might be light, busy or both, the veil had to be so heavy that the site
   underneath stopped being legible as a website at all — a dark green smear
   with some type over it. The site now lives in the devices where it is shown
   at a size it was designed for, and the capture stays on as a soft wash behind
   everything, which is all it was ever really doing. */
/* No min-height any more: the band used to be sized to fill the screen because
   the screen WAS the content. Now the content is a column of copy and a laptop,
   and a forced 940px left a third of the section as empty black above them. */
.sp-full{position:relative;isolation:isolate;overflow:hidden;display:flex;align-items:center;
  padding:96px 0}
.sp-bg{position:absolute;inset:0;z-index:0;overflow:hidden}
/* THE GHOST FIX (Tim, 28 Aug 2026). A 3px blur at half opacity is not a wash,
   it is a second copy of the website. On the Pasha spotlight you could read the
   words SUPREME COURT floating across the middle of the copy column and again
   beside the phone, so the band looked like two screenshots printed on top of
   each other. The capture was only ever here to keep the section from being a
   black rectangle, and colour is all it has to give: blurred this far past
   legibility it is a tint, and the site itself is now shown properly in the
   devices on the right where it is meant to be read. */
.sp-bg img{position:absolute;top:0;left:0;width:100%;height:auto;min-height:100%;
  object-fit:cover;object-position:top center;opacity:.26;
  filter:blur(28px) saturate(.8);transform:scale(1.08)}
/* The veil no longer thins to half way down the band. It used to drop to .5
   through the middle, which is exactly the height the copy sits at, so the
   thinnest part of the veil was over the longest run of text. */
/* The veil resolves to the page background, not to its own darker black (Tim,
   10 Sep 2026), so the spotlight joins the sections above and below it with no
   visible edge. It also opens and closes fully opaque on the page colour, which
   is what carries the blurred capture in and out instead of a ruled line. */
.sp-veil{position:absolute;inset:0;z-index:1;pointer-events:none;background:
  linear-gradient(90deg,rgba(7,8,8,.97) 0%,rgba(7,8,8,.95) 46%,rgba(7,8,8,.9) 72%,rgba(7,8,8,.86) 100%),
  linear-gradient(180deg,rgba(7,8,8,1) 0%,rgba(7,8,8,.8) 26%,rgba(7,8,8,.82) 62%,rgba(7,8,8,1) 100%)}
.sp-full .wrap{position:relative;z-index:2}
.sp-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,.95fr);gap:52px;align-items:center}
.sp-copy{max-width:600px}
/* The band up the page keeps the phone tucked against the laptop's corner; here
   the column is narrower, so it gets its own room rather than being cropped. */
.sp-dev .mbp{max-width:560px}
/* A pool of light under the machine. Against a section this dark the laptop
   otherwise floats with nothing holding it, and a dark client site inside it
   reads as a hole rather than a screen. */
.sp-dev{position:relative}
.sp-dev::before{content:'';position:absolute;left:50%;top:50%;translate:-50% -50%;
  width:86%;aspect-ratio:1.5;border-radius:50%;z-index:-1;pointer-events:none;
  background:radial-gradient(closest-side,rgba(50,207,117,.16),transparent 74%)}
/* ---- THE WAKE SWEEP (Tim, 28 Aug 2026) -----------------------------------
   The laptop is lit from the moment the section arrives. The phone beside it
   is not: its screen sits asleep, grey and dark, until a band of light travels
   across the pair from left to right, and the screen wakes as the light
   reaches it. Then the whole thing rests, and does it again ten seconds later.

   It is the same idea as the charge wave on the client ticker, aimed at one
   pair of devices instead of a row of cards: the light is what causes the
   thing to come on, so the eye follows it right and lands on the phone, which
   is the half of the build most people never think to look at.

   Transform and opacity only on the band, one filter on the phone screen, so
   the whole loop is two composited layers and costs the main thread nothing.

   The band lives in its own overflow:hidden wrapper rather than on a pseudo of
   .sp-dev, because .sp-dev cannot clip: its ::before light pool and the
   laptop's drop shadows both sit outside its box and would be sliced off. */
.sp-sweep{position:absolute;inset:0;z-index:4;overflow:hidden;pointer-events:none}
/* Gated on .in, the class the scroll reveal adds when the section arrives, for
   two reasons. The loop then starts the moment the pair is on screen, so the
   first pass is the one the visitor sees rather than one that already ran while
   they were four sections up. And with JS off there is no .in, so there is no
   sweep and no sleeping phone: both screens are simply on, which is the right
   thing to fall back to. The half second delay lets the reveal itself land
   first, and it is on both animations so they stay in step. */
.sp-dev.in .sp-sweep::after{content:'';position:absolute;top:-20%;bottom:-20%;left:0;width:34%;
  transform:translate3d(-140%,0,0);opacity:0;will-change:transform,opacity;
  background:linear-gradient(104deg,
    rgba(var(--wa),0) 0%,
    rgba(170,255,212,.015) 38%,
    rgba(200,255,228,.06) 47%,
    rgba(var(--wa),.2) 50%,
    rgba(200,255,228,.06) 53%,
    rgba(170,255,212,.015) 62%,
    rgba(var(--wa),0) 100%);
  /* The wrapper clips at the box edge, and a bright band cut off square against
     a near black section is a visible grey rectangle. The mask fades the band
     out before it ever reaches the top and bottom of the clip. */
  -webkit-mask-image:linear-gradient(180deg,transparent 0,#000 17%,#000 83%,transparent 100%);
  mask-image:linear-gradient(180deg,transparent 0,#000 17%,#000 83%,transparent 100%);
  animation:spSweep 10s cubic-bezier(.42,0,.3,1) .5s infinite}
/* The band is 34% of the box, and translateX is a percentage of ITS OWN width,
   so -140% parks it entirely off the left edge and 330% carries it entirely
   past the right. It is only travelling for the first 1.6s of the ten. */
@keyframes spSweep{
  0%   {transform:translate3d(-140%,0,0);opacity:0}
  4%   {opacity:1}
  16%  {transform:translate3d(330%,0,0);opacity:1}
  20%  {transform:translate3d(330%,0,0);opacity:0}
  100% {transform:translate3d(330%,0,0);opacity:0}
}
/* The phone screen only. The aluminium body, the notch and the sheen stay
   real, so it reads as a handset with its display off rather than a grey
   cut out of a phone. Scoped to .sp-dev: the band up the page keeps both of
   its screens on the whole time. */
.sp-dev.in .mbp-phone-screen{animation:spPhoneWake 10s ease-in-out .5s infinite}
@keyframes spPhoneWake{
  0%,11% {filter:grayscale(1) brightness(.3) contrast(.9)}
  17%    {filter:grayscale(0) brightness(1) contrast(1)}
  93%    {filter:grayscale(0) brightness(1) contrast(1)}
  100%   {filter:grayscale(1) brightness(.3) contrast(.9)}
}
/* No sweep, no sleeping phone: both screens simply on. */
@media(prefers-reduced-motion:reduce){
  .sp-sweep{display:none}
  .sp-dev.in .mbp-phone-screen{animation:none}
}
.sp-nodev .sp-grid{grid-template-columns:1fr}
.sp-domain{display:inline-flex;align-items:center;gap:7px;margin-top:26px;font-size:.76rem;
  color:var(--ink-mute);text-decoration:none;padding-bottom:3px;
  border-bottom:1px solid rgba(var(--wa),.14);transition:color .2s ease,border-color .2s ease}
.sp-domain:hover{color:var(--ink);border-color:rgba(50,207,117,.6)}
@media(max-width:1000px){
  .sp-full{min-height:0;padding:64px 0}
  .sp-grid{grid-template-columns:1fr;gap:40px}
  .sp-copy{max-width:none}
  /* THE PHONE FIX (Tim, 27 Aug 2026). One column makes this section a tall
     narrow strip, and a landscape screenshot told to cover it was scaled up
     until all you could see was a handful of blown out pixels. The photo now
     sits as a band across the top at its own proportions and fades into the
     section, which is what the veil was always doing on desktop. */
  /* The band is a fixed height inside a much taller section, so its bottom
     edge is a straight cut across the page. Now that the capture behind it is
     an even wash rather than a picture, that cut reads as a seam. Faded out
     before it gets there. */
  .sp-bg{inset:0 0 auto;height:min(44vh,400px);
    -webkit-mask-image:linear-gradient(180deg,#000 0,#000 52%,transparent 100%);
    mask-image:linear-gradient(180deg,#000 0,#000 52%,transparent 100%)}
  .sp-bg img{position:absolute;inset:0;width:100%;height:100%;
    object-fit:cover;object-position:top center;opacity:.3;
    filter:blur(20px) saturate(.85);transform:scale(1.1)}
  .sp-veil{background:linear-gradient(180deg,rgba(3,7,5,.66) 0%,rgba(3,7,5,.88) 24%,
    rgba(3,7,5,.97) 44%,rgba(3,7,5,.99) 100%)}
}
.cs-meta{display:flex;flex-wrap:wrap;gap:10px;margin-top:20px}
.cs-meta .chip{font-size:.74rem;font-weight:600;color:var(--ink-soft);border:1px solid var(--line);
  border-radius:999px;padding:7px 14px;background:var(--panel)}
.cs-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:14px;margin:8px 0}
.cs-stat{background:var(--panel);border:1px solid var(--line);border-radius:var(--r-sm);padding:22px 20px}
.cs-stat b{display:block;font-size:1.7rem;letter-spacing:-.02em;color:var(--ink)}
.cs-stat span{font-size:.82rem;color:var(--ink-mute)}
.cs-prose{max-width:720px;margin:0 auto}
.cs-prose h2{margin:34px 0 12px}
.cs-prose h3{margin:26px 0 8px;font-size:1.12rem}
.cs-prose p{color:var(--ink-soft);line-height:1.72;margin:0 0 16px}
.cs-prose ul{color:var(--ink-soft);line-height:1.72;padding-left:20px;margin:0 0 16px}
.cs-prose li{margin:6px 0}
.cs-prose blockquote{margin:20px 0;padding:18px 22px;border-left:3px solid var(--green);
  background:var(--green-soft);border-radius:0 var(--r-sm) var(--r-sm) 0;color:var(--ink)}
.cs-prose blockquote footer{margin-top:8px;font-size:.82rem;color:var(--ink-mute);font-style:normal}

/* ============================================================
   BLOG  (index cards + article)
   ============================================================ */
.blog-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:22px}
.post-card{display:flex;flex-direction:column;background:var(--panel);border:1px solid var(--line);
  border-radius:var(--r);overflow:hidden;text-decoration:none;color:inherit;
  transition:transform .3s cubic-bezier(.22,1,.36,1),border-color .3s,box-shadow .3s}
.post-card:hover{transform:translateY(-5px);border-color:var(--line-2);box-shadow:0 22px 50px rgba(0,0,0,.45)}
.post-card .shot{aspect-ratio:16/9;overflow:hidden;background:var(--bg-2);border-bottom:1px solid var(--line)}
.post-card .shot img{width:100%;height:100%;object-fit:cover;object-position:top center;display:block;transition:transform .5s}
.post-card:hover .shot img{transform:scale(1.04)}
.post-card .body{padding:18px 20px 20px;display:flex;flex-direction:column;gap:8px;flex:1}
.post-card .body .kicker{font-size:.68rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--green)}
.post-card .body h2,.post-card .body h3{margin:0;font-size:1.1rem;line-height:1.3;letter-spacing:-.01em}
.post-card .body p{margin:0;font-size:.86rem;color:var(--ink-soft);line-height:1.55}
.post-card .body time{margin-top:auto;padding-top:8px;font-size:.74rem;color:var(--ink-mute)}

.article{max-width:760px;margin:0 auto;padding:118px 22px 20px}
@media(max-width:560px){.article{padding-top:120px}}
.article .a-hero{border-radius:var(--r);overflow:hidden;border:1px solid var(--line);margin:24px 0 8px;
  box-shadow:0 30px 70px rgba(0,0,0,.5)}
.article .a-hero img{width:100%;display:block}
.article .a-kicker{font-size:.74rem;font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:var(--green)}
.article h1{margin:14px 0 12px;font-size:clamp(1.9rem,4vw,2.7rem)}
.article .a-meta{font-size:.84rem;color:var(--ink-mute);display:flex;gap:10px;flex-wrap:wrap;align-items:center}
.article .a-body{margin-top:18px}
.article .a-body h2{margin:32px 0 12px;font-size:1.5rem}
.article .a-body h3{margin:24px 0 8px;font-size:1.16rem}
.article .a-body p{color:var(--ink-soft);line-height:1.78;margin:0 0 18px;font-size:1.02rem}
.article .a-body ul,.article .a-body ol{color:var(--ink-soft);line-height:1.78;padding-left:22px;margin:0 0 18px}
.article .a-body li{margin:7px 0}
.article .a-body blockquote{margin:22px 0;padding:18px 24px;border-left:3px solid var(--green);
  background:var(--green-soft);border-radius:0 var(--r-sm) var(--r-sm) 0;color:var(--ink);font-size:1.05rem}
.article .a-body a{color:var(--green)}
/* buttons inside the article body must keep their own text colour
   (the green link rule above otherwise made .btn-green green-on-green) */
.article .a-body a.btn-green{color:#04130a}
.article .a-body a.btn-ghost{color:var(--ink)}
.article .a-cta{margin:34px 0 0;padding:30px 28px;text-align:center;background:var(--panel);
  border:1px solid var(--line);border-radius:var(--r)}
.article .a-cta h3{margin:0 0 10px}
.article .a-cta p{margin:0 0 18px;color:var(--ink-soft)}
@media(max-width:560px){.cs-stat b{font-size:1.4rem}}

/* ── the lead modal ───────────────────────────────────────────────────────
   Every "Get in contact" on the site opens the same form in this sheet, so a
   visitor never has to scroll to find it. Promoted from the /225-1/ page,
   which had already proven the pattern (bottom sheet on phones, 16px inputs
   so iOS does not zoom the page the moment you tap a field).
   The modal's markup and its form are stamped by scripts/sync-form.mjs. */
.sc-modal{position:fixed;inset:0;z-index:130;display:none;align-items:center;justify-content:center;
  background:rgba(2,8,5,.82);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);padding:20px}
.sc-modal.open{display:flex}
.sc-sheet{width:min(480px,96vw);max-height:92vh;overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;scrollbar-width:none;
  background:#0b0e0d;border:1px solid var(--line-2);border-radius:20px;padding:32px 28px;
  box-shadow:0 30px 90px rgba(0,0,0,.6);position:relative;animation:scIn .3s cubic-bezier(.2,.8,.3,1)}
@keyframes scIn{from{opacity:0;transform:translateY(18px) scale(.98)}to{opacity:1;transform:none}}
.sc-x{position:absolute;top:12px;right:12px;cursor:pointer;width:38px;height:38px;border-radius:11px;
  display:grid;place-items:center;border:1px solid var(--line-2);color:#fff;background:rgba(var(--wa),.04);
  font-size:1rem;line-height:1;transition:border-color .2s}
.sc-x:hover{border-color:var(--green)}
.sc-modal .form input{font-size:16px}
.sc-modal h2{font-size:1.5rem;letter-spacing:-.01em;margin:0 0 8px}
.sc-modal .sc-sub{font-size:.9rem;color:var(--ink-mute);line-height:1.6;margin:0 0 22px}
@media(max-width:600px){
  .sc-modal{padding:0;align-items:flex-end}
  .sc-sheet{width:100%;max-width:none;max-height:92dvh;border-radius:22px 22px 0 0;border-bottom:0;
    padding:28px 20px calc(22px + env(safe-area-inset-bottom));animation:scUp .32s cubic-bezier(.2,.8,.3,1)}
  .sc-modal .form input{padding:14px 16px}
}
@keyframes scUp{from{transform:translateY(100%)}to{transform:none}}
.sc-sheet::-webkit-scrollbar{display:none}
@media(prefers-reduced-motion:reduce){.sc-sheet{animation:none}}

/* ===========================================================================
   LIQUID GLASS DOCK  (.sc-dock)
   The house floating nav, stamped onto every page by sc.js so it never has to
   be pasted into 87 files. Modelled on the iOS 26 tab bar: a pill that hovers
   over the content, the page visibly refracting through it rather than a flat
   translucent strip.

   Four layers make it read as real glass instead of a grey box:
     1) backdrop-filter blur + saturate + a brightness lift — the lift is what
        stops dark content behind it going muddy, and is the single biggest
        tell between "glass" and "semi transparent black";
     2) a masked gradient RIM (::before) that slowly rotates, so the edge
        catches light the way a moulded glass edge does;
     3) a specular hotspot (::after) that tracks the pointer on desktop and
        drifts with scroll on touch, so the surface is never static;
     4) a sliding pill behind the active item, with its own inner highlight so
        it sits INSIDE the glass rather than on top of it.

   TRAP, and the reason this is centred with flex rather than the usual
   left:50% + translateX(-50%): a TRANSFORMED ANCESTOR breaks backdrop-filter
   on its child in Chrome and Safari. The filter still computes, so devtools
   and getComputedStyle both look correct, but nothing behind the element is
   ever sampled and the "glass" silently renders as a flat translucent box.
   The wrapper therefore carries NO transform — it is a full width flex row
   that centres the pill — and every transform (entrance, hide, press) lives
   on .sc-dock-in itself or below it.

   Add data-nodock to <body> on any page that already owns the bottom edge.
   =========================================================================== */
@property --sc-rim{syntax:'<angle>';inherits:false;initial-value:200deg}

/* 18px, the same bottom offset the chat pill uses, so the two things floating
   over the bottom edge sit on one line instead of one riding lower. */
.sc-dock{position:fixed;z-index:75;left:0;right:0;
  bottom:calc(18px + env(safe-area-inset-bottom));
  display:flex;justify-content:center;padding:0 10px;
  opacity:1;transition:opacity .3s ease;pointer-events:none}
.sc-dock.hide{opacity:0;pointer-events:none}
.sc-dock.hide .sc-dock-in{transform:translateY(18px) scale(.96)}

/* --dock-r and --dock-pad are the concentric pair for the whole dock: every
   rounded thing INSIDE it derives from these two, so the phone override further
   down only has to restate the radius and the items follow. */
.sc-dock-in{--dock-r:27px; --dock-pad:7px;
  pointer-events:auto;position:relative;isolation:isolate;max-width:100%;
  display:flex;align-items:stretch;gap:1px;padding:var(--dock-pad);border-radius:var(--dock-r);
  /* Clearer glass (Tim, 16 Sep 2026): less tint and a lighter blur, so the
     page reads through the dock instead of a frosted grey slab. */
  background:linear-gradient(180deg,rgba(var(--wa),.075),rgba(var(--wa),.02));
  -webkit-backdrop-filter:blur(14px) saturate(210%) brightness(1.22);
  backdrop-filter:blur(14px) saturate(210%) brightness(1.22);
  box-shadow:0 26px 54px -14px rgba(0,0,0,.78),0 3px 12px rgba(0,0,0,.4),
    inset 0 1px 0 rgba(var(--wa),.16),inset 0 -1px 0 rgba(var(--wa),.05);
  transform:translateY(0) scale(1);
  transition:transform .38s cubic-bezier(.2,.8,.2,1)}

/* the rim. A conic gradient painted only on a 1px ring (mask-composite cuts the
   middle out), rotating slowly so the highlight travels around the edge. */
.sc-dock-in::before{content:'';position:absolute;inset:0;border-radius:inherit;padding:1px;z-index:2;
  pointer-events:none;
  background:conic-gradient(from var(--sc-rim),
    rgba(var(--wa),.6),rgba(var(--wa),.05) 18%,
    rgba(50,207,117,.4) 38%,rgba(var(--wa),.04) 55%,
    rgba(var(--wa),.5) 76%,rgba(var(--wa),.08) 90%,rgba(var(--wa),.6));
  -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;
  animation:scDockRim 16s linear infinite}
@keyframes scDockRim{to{--sc-rim:560deg}}

/* ---------- the refractive border ----------
   The thing that makes the iOS 26 tab bar read as a lozenge of glass rather
   than a pill with a stroke on it: the EDGE behaves differently from the middle.
   It is thicker there, so it bends and intensifies whatever is behind it, and
   the colour of the rim changes as content scrolls past underneath. Different
   parts of the same edge can be different colours at the same moment.

   Both layers are RINGS made with mask-composite: two identical masks, one
   clipped to the padding box, xored, leaves only the band of padding. That is
   what lets a backdrop-filter apply to an edge and nothing else. A radial
   gradient fade would be simpler but it is an ellipse, and on a box this wide
   and short it reaches the vertical centre almost immediately and fogs the
   whole dock.

   TWO TRAPS, and between them they are why a first attempt barely reacts:

   1. THE NEGATIVE INSET IS LOAD BEARING. backdrop-filter samples whatever has
      already been painted behind the element, and the panel is painted behind
      its own children. Sitting flush with the panel, these rings sample the
      panel's blurred, tinted output instead of the page, so the colour arrives
      already washed out. Pulled out past the panel, the band sits outside its
      footprint where the raw page is, which is also where a real lens edge
      catches its light.
   2. BLUR IS THE ENEMY HERE. A 6px blur averages a 12px circle of page into
      every pixel of a 3px ring, which is most of the local colour gone. The
      reference line is sharp, so this samples nearly one for one.

   Brightness is LIFTED here (unlike the Steiner build of the same effect, which
   deepens instead) because scalus.app is near black behind the dock, so a
   lifted rim glows rather than clipping to white. */
.sc-dock-lens{position:absolute;inset:-9px;border-radius:999px;padding:9px;z-index:1;
  pointer-events:none;opacity:.55;
  -webkit-backdrop-filter:blur(6px) saturate(2.8) brightness(1.4);
  backdrop-filter:blur(6px) saturate(2.8) brightness(1.4);
  -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}

.sc-dock-edge{position:absolute;inset:-2px;border-radius:999px;padding:3px;z-index:2;
  pointer-events:none;
  -webkit-backdrop-filter:blur(.5px) saturate(3.6) brightness(1.55) contrast(1.35);
  backdrop-filter:blur(.5px) saturate(3.6) brightness(1.55) contrast(1.35);
  -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 specular hotspot — position comes from --sc-x/--sc-y, set in sc.js */
.sc-dock-in::after{content:'';position:absolute;inset:0;border-radius:inherit;z-index:1;
  pointer-events:none;
  background:radial-gradient(130px 66px at var(--sc-x,28%) var(--sc-y,-10%),
    rgba(var(--wa),.2),rgba(var(--wa),.05) 45%,rgba(var(--wa),0) 72%);
  transition:background-position .3s ease}

/* the pill that slides behind whichever item is current */
.sc-dock-pill{position:absolute;top:var(--dock-pad);bottom:var(--dock-pad);left:0;z-index:0;width:var(--w,0px);
  border-radius:calc(var(--dock-r) - var(--dock-pad));pointer-events:none;opacity:0;
  transform:translate3d(var(--x,0px),0,0);
  background:linear-gradient(180deg,rgba(var(--wa),.16),rgba(var(--wa),.055));
  box-shadow:inset 0 1px 0 rgba(var(--wa),.3),inset 0 -1px 0 rgba(0,0,0,.22),
    0 4px 14px rgba(0,0,0,.3);
  transition:transform .5s cubic-bezier(.34,1.35,.5,1),width .5s cubic-bezier(.34,1.35,.5,1),opacity .35s}
.sc-dock.ready .sc-dock-pill{opacity:1}

.sc-dock-i{position:relative;z-index:3;display:flex;flex-direction:column;align-items:center;
  justify-content:center;gap:4px;min-width:64px;padding:8px 8px 7px;
  border-radius:calc(var(--dock-r) - var(--dock-pad));
  text-decoration:none;color:var(--ink-soft);-webkit-tap-highlight-color:transparent;
  transition:color .25s ease,transform .2s ease}
.sc-dock-i:hover{color:var(--ink)}
.sc-dock-i:active{transform:scale(.93)}
.sc-dock-i.on{color:#fff}
.sc-dock-i .ic{width:26px;height:26px;display:grid;place-items:center;flex:none}
.sc-dock-i .ic svg{width:22px;height:22px;display:block}
.sc-dock-i .lb{font-size:.64rem;font-weight:650;line-height:1;letter-spacing:.005em;white-space:nowrap}
.sc-dock-i:focus-visible{outline:2px solid var(--green);outline-offset:2px}

/* the one item that is an action rather than a page, so it carries the brand
   fill the way the blue avatar tile does on the reference dock */
.sc-dock-i.cta{color:var(--ink)}
.sc-dock-i.cta .ic{border-radius:9px;color:#04130a;
  background:linear-gradient(160deg,#4ce68e,#22a75c);
  box-shadow:0 0 16px rgba(50,207,117,.45),inset 0 1px 0 rgba(var(--wa),.45)}
.sc-dock-i.cta .ic svg{width:15px;height:15px}

/* SEO is a PAGE, not an action, so it reads exactly like Work and Pricing: the
   plain outline mark at the same size, no tile behind it. Tim, 27 Aug 2026 —
   it used to carry a green tile, which made the row look like it had three
   different calls to action in it. The magnifier with the tick stays, that is
   what tells you what the page checks. */
.sc-dock-i.seo{color:var(--ink-soft)}
.sc-dock-i.seo:hover{color:var(--ink)}

/* Message opens the three field sheet. Tim, 14 Sep 2026: the orange tile is
   gone. A filled orange square sitting beside the filled green Call tile gave
   the row two competing accents, so Message now reads as the plain outline mark
   Home, Work, Pricing and SEO use, and Call is the one focal point left. */
.sc-dock-i.dmsg{color:var(--ink-soft)}
.sc-dock-i.dmsg:hover{color:var(--ink)}
.sc-msg .sc-sheet{text-align:left}
.sc-msg-brand{display:block;width:auto;height:24px;margin:0 0 14px;opacity:.92}
.sc-msg .sc-msg-form{display:grid;gap:12px}
.sc-msg .thanks h2{font-size:1.3rem;margin:0}

/* Chat is a live thing rather than a page, so it carries the same green "we are
   here" dot the floating launcher does. It is not given the filled tile — one
   accent tile per dock, or the row stops having a focal point. */
.sc-dock-i.chat .ic{position:relative}
.sc-dock-i.chat .ic::after{content:'';position:absolute;top:0;right:0;width:7px;height:7px;
  border-radius:50%;background:#32cf75;box-shadow:0 0 0 2px rgba(7,9,8,.85),0 0 10px rgba(50,207,117,.8)}
@media(prefers-reduced-motion:no-preference){
  .sc-dock-i.chat .ic::after{animation:scDockLive 2.6s ease-in-out infinite}
}
@keyframes scDockLive{0%,100%{opacity:1}50%{opacity:.45}}
/* One way into the chat at every width. Above 1024px the floating "Chat with me"
   pill has room of its own and does the inviting, so the dock hands the job over
   rather than offering the same thing twice. */
@media(min-width:1025px){.sc-dock-i.chat{display:none}}

/* entrance. On .sc-dock-in, never on the wrapper — see the TRAP note above.
   Fill mode is BACKWARDS rather than both on purpose: a forwards fill keeps the
   keyframe's own transform winning over every later declaration for the life of
   the page, which silently beat the hide states below (an animated value always
   outranks a plain one). With backwards the dock owns its transform again the
   moment the entrance finishes, and the end frame matches the resting rule so
   nothing jumps. */
.sc-dock-in{animation:scDockUp .55s cubic-bezier(.2,.9,.25,1) backwards}
@keyframes scDockUp{from{opacity:0;transform:translateY(26px)}
  to{opacity:1;transform:translateY(0)}}

/* room at the foot of the page so the dock never covers the last line */
body.has-dock{padding-bottom:calc(94px + env(safe-area-inset-bottom))}

/* Below the point where the nav collapses to a hamburger, the dock squares up
   with the bar at the top of the screen: same 16px gutter, same width, so the
   two pieces of glass frame the page instead of floating at two random widths.
   The items share the row evenly, and the sliding pill needs no help because it
   measures the live element rather than a hard coded width. */
@media(max-width:1000px){
  .sc-dock{padding:0 16px}
  .sc-dock-in{width:100%;max-width:1140px}
  .sc-dock-i{flex:1 1 0;min-width:0}
}

/* The chat launcher sits centre-right, so on anything narrower than a laptop it
   would land on top of the dock. Below 1024px the dock carries Chat itself, so
   the pill stands down rather than offering the same thing twice.

   Scoped to .has-dock-chat, set by dock.js only once the item is actually in the
   dock, because a page can have a chat and NO dock (the ad funnels run
   data-nodock so they own their own bottom edge). Hiding the pill there would
   leave that page with no way into the chat at all. */
@media(max-width:1024px){
  body.has-dock-chat .sc-chat{display:none}
  .talk-now{bottom:calc(88px + env(safe-area-inset-bottom))}
}
@media(max-width:560px){
  .sc-dock{bottom:calc(4px + env(safe-area-inset-bottom))}
  .sc-dock-in{--dock-r:25px; --dock-pad:6px}
  .sc-dock-i{padding:7px 3px 6px;gap:3px}
  .sc-dock-i .lb{font-size:.6rem}
  .sc-dock-i .ic{width:24px;height:24px}
  .sc-dock-i .ic svg{width:21px;height:21px}
  .sc-chat{bottom:calc(80px + env(safe-area-inset-bottom))}
  .talk-now{bottom:calc(80px + env(safe-area-inset-bottom))}
}
@media(max-width:360px){
  .sc-dock-i .lb{font-size:.55rem}
  .sc-dock-i .ic svg{width:19px;height:19px}
}

/* ---------- the phone CTA bar (.sc-dock-cta) ----------
   Tim, 21 Sep 2026: below 620px the dock stops being a nav (that stayed a
   small icon row) and becomes two big taps — Enquire and Call — in the same
   liquid glass panel the rest of the dock uses, just wider buttons instead
   of more of them. The chat bubble is no longer folded in here, so it keeps
   floating above this bar at every width (see the .sc-chat rule below).

   Enquire carries the bold green fill — it is the one Tim can actually
   follow up, so it is the button meant to get tapped first. Call stays the
   lighter, clearer glass: still obviously tappable, but visually second so
   the two don't compete for the same amount of attention. */
/* Slimmer (Tim, 6 Oct 2026: "the dock on scalus is too big", then "reduce
   this size, make it designer better"): about 46px tall and 340px wide at
   most, a thin 4px glass rim round the two taps. */
.sc-dock-cta .sc-dock-in.sc-cta-in{--dock-r:999px; --dock-pad:4px;gap:4px;width:100%;max-width:340px}
.sc-dock-cta .sc-dock-i{flex:1 1 0;min-width:0;flex-direction:row;gap:7px;
  padding:10px 12px;border-radius:999px;
  font-weight:700}
.sc-dock-cta .sc-dock-i .ic{width:18px;height:18px}
.sc-dock-cta .sc-dock-i .ic svg{width:17px;height:17px}
.sc-dock-cta .sc-dock-i .lb{font-size:.86rem;letter-spacing:.01em}

/* PILLS, LIKE THE PAGE'S OWN BUTTONS (Tim, 29 Sep 2026: "make the dock on mobile
   the same colour and style of button as [Get in contact] so it moulds better
   to the side of the iphone"). The bar and both taps are full pills, so their
   ends run concentric with the phone's rounded corners instead of sitting as
   squared off tiles inside them.

   Enquire is .btn-green exactly: the flat brand green, the inset ring and the
   slow glow, heavy dark label. */
.sc-dock-cta .cta-enquire{color:#04130a;font-weight:800;
  background:var(--green);
  box-shadow:0 6px 20px rgba(50,207,117,.34),0 0 0 1px rgba(140,255,190,.45) inset;
  animation:btnGlow 3.6s ease-in-out infinite}
.sc-dock-cta .cta-enquire .lb{font-weight:800}
.sc-dock-cta .cta-enquire:hover,.sc-dock-cta .cta-enquire:focus-visible{color:#04130a}
.sc-dock-cta .cta-enquire:active{transform:scale(.96)}
@media(prefers-reduced-motion:reduce){.sc-dock-cta .cta-enquire{animation:none}}
[data-theme="light"] .sc-dock-cta .cta-enquire{background:#32cf75}

/* Call is clear glass, no fill of its own (Tim, 6 Oct 2026: "designer
   better"): the bar's glass shows straight through it, so the dock reads as
   one piece with a single green tap in it rather than two slabs side by side. */
.sc-dock-cta .cta-call{color:var(--ink);background:rgba(var(--wa),.07);box-shadow:none;transition:background .2s}
[data-theme="light"] .sc-dock-cta .cta-call{background:rgba(255,255,255,.6)}
.sc-dock-cta .cta-call:hover,.sc-dock-cta .cta-call:focus-visible{color:var(--ink);background:rgba(var(--wa),.08)}
[data-theme="light"] .sc-dock-cta .cta-call:hover,[data-theme="light"] .sc-dock-cta .cta-call:focus-visible{background:rgba(255,255,255,.85)}
.sc-dock-cta .cta-call:active{transform:scale(.96)}

/* data-dock-start: one Get started button at every width, so on a wide screen
   the bar stays button sized instead of stretching to the phone's 480px */
.sc-dock-start .sc-dock-in.sc-cta-in{max-width:320px}
.sc-dock-start .sc-dock-i .lb{font-size:.95rem}

/* room at the foot of the page for the taller bar */
body.has-dock-cta{padding-bottom:calc(76px + env(safe-area-inset-bottom))}

/* ...but on a page that ends in the site footer, that room goes INSIDE the
   footer. As body padding it showed as a strip of bare page colour under the
   black footer (Tim, 7 Oct 2026: "fix bottom there is a bar of nothing"). */
body.has-dock{--dock-room:calc(94px + env(safe-area-inset-bottom))}
body.has-dock-cta{--dock-room:calc(76px + env(safe-area-inset-bottom))}
body.has-dock:has(footer.site),body.has-dock-cta:has(footer.site){padding-bottom:0}
body.has-dock footer.site,body.has-dock-cta footer.site{padding-bottom:calc(48px + var(--dock-room))}
@media(max-width:600px){
  body.has-dock footer.site,body.has-dock-cta footer.site{padding-bottom:calc(40px + var(--dock-room))}
}

/* the floating chat bubble now stays up at every width (it no longer hands
   off to the dock on a phone), so it needs to clear the taller CTA bar
   rather than the old slim dock's 88px/80px offsets. */
@media(max-width:620px){
  .sc-chat{bottom:calc(78px + env(safe-area-inset-bottom))}
  .talk-now{bottom:calc(78px + env(safe-area-inset-bottom))}
}
@media(max-width:400px){
  .sc-dock-cta .sc-dock-i{padding:10px 6px}
  .sc-dock-cta .sc-dock-i .lb{font-size:.84rem}
}

@media(prefers-reduced-motion:reduce){
  .sc-dock-in{animation:none}
  .sc-dock-in::before{animation:none}
  .sc-dock-pill{transition:opacity .2s}
}

/* ===========================================================================
   CHROME ON DEMAND  (the top bar and the dock take turns)
   Tim, 25 Aug 2026: two bars on screen at once reads as clutter, so only one of
   them is ever there.

     · the top bar is ON from the first frame and stays stuck to the top
       (Tim, 7 Oct 2026, "keep the menu sticky"; it used to hide over a
       .hero-brand on arrival). A phone keeps its dock at the top too;
     · any time the visitor scrolls UP: the top bar is present
       and the dock has retracted;
     · the moment they scroll DOWN: the top bar fades and shrinks up out of the
       way, and the dock rises in its place;
     · while they are filling in a form or picking a time on the calendar: both
       stand down, along with the chat launcher and the score prompt, so the
       screen belongs to the thing they are doing.

   State lives in three classes on <html>, set by the controller at the bottom
   of dock.js: ch-nav-off, ch-dock-off, ch-focus. Nothing here fires until that
   script has run, so a page with JS off keeps both bars exactly as they were.

   TRANSFORM TRAP, again: the transform that moves the bar sits on .nav .bar
   ITSELF, which is allowed, because it is the element that carries the
   backdrop-filter rather than an ancestor of it. Putting it on .nav instead
   would make .nav a transformed ancestor and the glass would flatten. The
   resting value is `none` and not translateY(0) for the same reason: the mobile
   menu inside the bar has a backdrop-filter of its own, and a bar left sitting
   on an identity matrix would still count as transformed and flatten it.
   =========================================================================== */

/* no transitions on the first frame, or the dock would visibly fade out on
   load on its way to the hidden state it should have started in */
html.ch-boot .nav .bar,html.ch-boot .sc-dock,html.ch-boot .sc-dock-in{
  transition:none!important;animation:none!important}

/* ── the top bar ──────────────────────────────────────────────────────────
   Shrinks toward its own top edge as it leaves, so it reads as the bar
   retracting into the screen rather than a box sliding off it. Leaving is
   slower than arriving: a visitor who scrolls up wants the menu NOW, and a
   visitor who scrolls down should barely notice it go. */
.nav .bar{transform-origin:50% 0;
  transition:background .3s,transform .34s cubic-bezier(.22,1,.36,1),opacity .26s ease}
html.ch-nav-off .nav{pointer-events:none}
html.ch-nav-off .nav .bar{transform:translateY(-128%) scale(.9);opacity:0;
  transition:background .3s,transform .55s cubic-bezier(.5,0,.75,.15),opacity .45s ease}

/* the mobile menu is hanging off the bar, so the bar stays no matter what the
   scroll is doing */
.nav.open .bar{transform:none!important;opacity:1!important}
.nav.open{pointer-events:auto!important}

/* keyboard users can still tab into a bar that scrolled away: focus brings it
   straight back rather than moving focus to something invisible */
html.ch-nav-off .nav.kb-in{pointer-events:auto}
html.ch-nav-off .nav.kb-in .bar{transform:none;opacity:1}

/* ── the dock ─────────────────────────────────────────────────────────────
   Dissolves a beat before it finishes retracting, so it shrinks away rather
   than dropping off the bottom edge. */
.sc-dock{transition:opacity .3s ease}
html.ch-dock-off .sc-dock{opacity:0;pointer-events:none;transition:opacity .42s ease}
html.ch-dock-off .sc-dock-in{transform:translateY(150%) scale(.86);
  transition:transform .52s cubic-bezier(.5,0,.75,.15)}
html.ch-dock-off .sc-dock.kb-in{opacity:1;pointer-events:none}
html.ch-dock-off .sc-dock.kb-in .sc-dock-in{transform:none;pointer-events:auto}

/* ── the chat is never left without a way in ──────────────────────────────
   Below 1024px the floating pill normally stands down because the dock is
   carrying Chat (see the has-dock-chat note above). Now that the dock retracts
   on the way back up the page, the pill has to take the job back while it is
   away, or a visitor scrolling up on a phone would have no chat at all. The two
   swap: pill while the dock is gone, dock item while it is there. Tim's rule is
   that the chat is present through every scroll, in both directions.
   Coming back is an animation rather than a transition because the pill is
   display:none in the other state, and a transition has nothing to run from. */
@media(max-width:1024px){
  html.ch-dock-off body.has-dock-chat .sc-chat{display:flex;
    animation:scChatBack .34s cubic-bezier(.2,.9,.25,1) backwards}
  html.ch-dock-off .sc-chat,html.ch-dock-off .talk-now{
    bottom:calc(18px + env(safe-area-inset-bottom))}
}
@keyframes scChatBack{from{opacity:0;transform:translateY(14px) scale(.94)}
  to{opacity:1;transform:none}}

/* ── heads down on a form, a calendar or the chat ─────────────────────────
   Everything floating over the page steps out at once. */
html.ch-focus .sc-chat,html.ch-focus .talk-now{
  opacity:0;pointer-events:none;transform:translateY(10px) scale(.96)}
.talk-now{transition:transform .25s,box-shadow .25s,opacity .25s}

@media(prefers-reduced-motion:reduce){
  .nav .bar,.sc-dock,.sc-dock-in{transition:opacity .2s ease!important}
  html.ch-nav-off .nav .bar{transform:none}
  html.ch-dock-off .sc-dock-in{transform:none}
  html.ch-focus .sc-chat,html.ch-focus .talk-now{transform:none}
}

/* ─────────────────────────────────────────────────────────────────────────
   THE LAPTOP BAND (.mbp)
   A 14 inch MacBook Pro in space black, sitting beside a line of copy, with the
   real client sites rolling past inside the screen: each one scrolls the way
   someone reads it, then fades out and the next one takes over. Built in CSS
   rather than shipped as a photo so it stays sharp on every screen and the
   screenshot inside it is the only image that loads.

   The screen is 1.54:1, which is the real 3024x1964 panel, so the shape is the
   machine and not an approximation of it.
   ───────────────────────────────────────────────────────────────────────── */
/* The band lost its gradient wash on 25 Aug 2026 (Tim). Two client screens are
   already the brightest thing in this section; a lit background behind them was
   competing with the very thing it was framing. Flat black, and the devices
   carry it. The copy column also tightened and moved hard left, which buys the
   width the phone needs on the right. */
/* THE LAPTOP BAND AND THE SPOTLIGHT ARE ONE DARK PANE (Tim, 3 Sep 2026).
   The band used to carry the .alt white wash, which lifted its top third into a
   soft grey green while the spotlight directly under it sat at flat near black,
   so the two read as two different backgrounds with a line between them. Both
   now sit on the same near black the spotlight veil resolves to, and the .alt
   class has come off the section in the page. */
/* No background of its own any more (Tim, 10 Sep 2026). The band painting its
   own #030705 over a #070808 page put a faint edge top and bottom, which is
   what the old glass strip was there to hide. It now sits on the page colour
   like every other section, so there is nothing to hide. */
.mbp-band{background:none}
.sec-light + .mbp-band{background:none}
/* A GROUND OF ITS OWN (Tim, 14 Sep 2026). The band moved down the home page,
   past the spotlight and past the two photographs, which left it sitting
   between two sections that both take the page colour, so it read as more of
   the same scroll rather than as its own moment. .mbp-band-tint gives it a
   green tinted ground with a hairline top and bottom. The faint edge the
   10 Sep note above was trying to lose is the point this time: it is what
   marks the band off. Low saturation on purpose, so the two client screens are
   still the brightest thing in the frame.

   It is a MODIFIER, not a change to .mbp-band, because thirteen other pages
   carry this band inside their own designs and none of them asked for a new
   colour. */
.mbp-band-tint{position:relative;
  background:
    radial-gradient(120% 150% at 50% 0%, rgba(50,207,117,.10) 0%, rgba(50,207,117,0) 60%),
    linear-gradient(180deg, #0a1310 0%, #0b1613 55%, #091110 100%);
  border-top:1px solid rgba(var(--wa),.07);
  border-bottom:1px solid rgba(var(--wa),.07)}
.sec-light + .mbp-band-tint{background:
    radial-gradient(120% 150% at 50% 0%, rgba(50,207,117,.10) 0%, rgba(50,207,117,0) 60%),
    linear-gradient(180deg, #0a1310 0%, #0b1613 55%, #091110 100%)}
.mbp-band .wrap{display:grid;grid-template-columns:.72fr 1.28fr;gap:40px;align-items:center}
.mbp-band .wrap > :first-child{text-align:left;justify-self:start;max-width:420px}
@media(max-width:900px){
  .mbp-band .wrap{grid-template-columns:1fr;gap:30px}
  .mbp-band .wrap > :first-child{max-width:none}
}

/* The laptop and its phone sit on one row: the phone overlaps the laptop's
   bottom right corner the way it would if you set it down beside the machine,
   so the pair reads as one desk rather than two separate mockups. */
.mbp{position:relative;width:100%;max-width:620px;margin:0 auto;perspective:1400px;
  padding-right:118px}
@media(max-width:900px){.mbp{padding-right:96px}}
@media(max-width:520px){.mbp{padding-right:0}}
.mbp-lid{--lid-r:16px; --lid-gap:11px;
  position:relative;border-radius:var(--lid-r);padding:var(--lid-gap) var(--lid-gap) 13px;
  background:linear-gradient(155deg,#3a3d40 0%,#17181a 26%,#0e0f10 62%,#232527 100%);
  box-shadow:0 34px 70px -18px rgba(0,0,0,.85),0 2px 0 rgba(var(--wa),.07) inset,
    0 0 0 1px rgba(var(--wa),.06)}
.mbp-screen{position:relative;aspect-ratio:1.54;overflow:hidden;background:#070a08;
  border-radius:calc(var(--lid-r) - var(--lid-gap));
  box-shadow:0 0 0 1px rgba(0,0,0,.9) inset}
.mbp-screen img{position:absolute;top:0;left:0;width:100%;height:auto;display:block;
  will-change:transform;transform:translate3d(0,0,0);opacity:0;
  transition:transform var(--pan-ms,4000ms) cubic-bezier(.36,0,.64,1),opacity .5s ease}
.mbp-screen img.on{opacity:1}
/* the glass sheen across the panel */
.mbp-screen::after{content:'';position:absolute;inset:0;z-index:2;pointer-events:none;
  background:linear-gradient(118deg,rgba(var(--wa),.11) 0%,rgba(var(--wa),.02) 26%,
    rgba(var(--wa),0) 46%)}
/* the base: a shallow wedge with the notch you grip it by */
.mbp-base{position:relative;height:13px;margin:0 auto;width:104%;left:-2%;
  background:linear-gradient(180deg,#2c2f31,#141517 55%,#0d0e0f);
  border-radius:0 0 12px 12px;clip-path:polygon(1.4% 0,98.6% 0,100% 100%,0 100%);
  box-shadow:0 20px 34px -14px rgba(0,0,0,.8)}
.mbp-base::after{content:'';position:absolute;top:0;left:50%;transform:translateX(-50%);
  width:13%;height:5px;border-radius:0 0 7px 7px;background:rgba(0,0,0,.55)}
.mbp-domain{margin:16px 0 0;text-align:center;font-size:.74rem;color:var(--ink-mute);
  transition:opacity .35s ease}
.mbp-domain.fade{opacity:0}

/* The page turn. While a screenful is travelling the panel dims and softens;
   the instant it lands on the next section it comes back up to full. That dip
   is what makes it read as a page changing rather than a scrollbar moving, and
   it hides the fact that the capture is one long strip. */
.mbp-screen img,.mbp-phone-screen img{
  transition:transform var(--pan-ms,1500ms) cubic-bezier(.5,0,.2,1),
             opacity .5s ease,filter .5s ease}
/* Lighter than it was (Tim, 27 Aug 2026). At four pages a site the dip was a
   small part of a long turn; now that a site only holds for five seconds, a
   dip to a third of full brightness meant a visitor glancing at the band had a
   good chance of catching it half faded and reading it as broken. */
.mbp-screen.turning img,.mbp-phone.turning .mbp-phone-screen img{
  opacity:.66;filter:blur(.7px)}

/* ---- the phone beside the laptop -----------------------------------------
   390x844 is the real iPhone logical viewport, so the aspect ratio here is the
   true one rather than a guess. It carries its OWN mobile capture, stepped in
   time with the laptop, because the whole point of it is showing that the
   mobile build is a different design and not a squashed desktop. */
.mbp-phone{position:absolute;right:0;bottom:2px;width:132px;z-index:3;
  border-radius:20px;padding:5px;
  background:linear-gradient(155deg,#3a3d40 0%,#17181a 30%,#0e0f10 66%,#242628 100%);
  box-shadow:0 26px 46px -14px rgba(0,0,0,.9),0 0 0 1px rgba(var(--wa),.07),
             inset 0 1px 0 rgba(var(--wa),.09)}
.mbp-phone-screen{position:relative;aspect-ratio:390/844;border-radius:16px;overflow:hidden;
  background:#070a08;box-shadow:0 0 0 1px rgba(0,0,0,.9) inset}
.mbp-phone-screen img{position:absolute;top:0;left:0;width:100%;height:auto;display:block;
  will-change:transform;transform:translate3d(0,0,0);opacity:0}
.mbp-phone-screen img.on{opacity:1}
/* the notch, and the same sheen the laptop panel carries */
.mbp-phone-screen::before{content:'';position:absolute;top:6px;left:50%;transform:translateX(-50%);
  width:38%;height:9px;border-radius:0 0 8px 8px;background:#070a08;z-index:3}
.mbp-phone-screen::after{content:'';position:absolute;inset:0;z-index:2;pointer-events:none;
  background:linear-gradient(118deg,rgba(var(--wa),.1) 0%,rgba(var(--wa),.02) 28%,
    rgba(var(--wa),0) 48%)}
/* No mobile capture for this site yet: show a dark screen, never a desktop
   layout crushed to phone width. */
.mbp-phone.mbp-phone-empty .mbp-phone-screen img{opacity:0}
@media(max-width:900px){.mbp-phone{width:112px}}
@media(max-width:520px){
  /* On a phone the little phone is the joke that does not land. The laptop
     alone, full width. */
  .mbp-phone{display:none}
}
@media(prefers-reduced-motion:reduce){
  .mbp-screen img,.mbp-phone-screen img{transition:opacity .3s ease}
  .mbp-screen.turning img,.mbp-phone.turning .mbp-phone-screen img{opacity:1;filter:none}
}

/* ===========================================================================
   THE CHARGE WAVE  (see the block of the same name in sc.js)
   A pulse travels along a row and each item lights as it arrives. Used on the
   client ticker and on the release cards, so both read as one thing reaching
   every site. Transform and box-shadow only, so each item is a composited
   layer and a row of fourteen costs the main thread nothing.
   =========================================================================== */
.cl-card,.upcard{transition:transform .45s cubic-bezier(.2,.8,.3,1),
                            box-shadow .45s ease,border-color .45s ease}
.cl-card.charged,.upcard.charged{
  transform:translateY(-6px);
  border-color:rgba(50,207,117,.55);
  box-shadow:0 14px 40px -12px rgba(50,207,117,.5),
             0 0 0 1px rgba(50,207,117,.4),
             inset 0 1px 0 rgba(170,255,210,.28)}
/* the screen itself brightens, so what lights up is the SITE, not a border */
.cl-card.charged .cl-phone img{filter:brightness(1.22) saturate(1.08)}
.cl-phone img{transition:filter .45s ease,opacity .6s ease}

/* The source of the wave on the updates section: the version badge pulses
   once, and that is the update being made before it goes anywhere. */
.sc-charge-src{animation:scChargeSrc .8s ease-out}
@keyframes scChargeSrc{
  0%  {text-shadow:none;transform:scale(1)}
  30% {text-shadow:0 0 22px rgba(50,207,117,.9);transform:scale(1.06)}
  100%{text-shadow:none;transform:scale(1)}
}
@media(prefers-reduced-motion:reduce){
  .cl-card,.upcard,.cl-phone img{transition:none}
  .cl-card.charged,.upcard.charged{transform:none;box-shadow:none}
  .sc-charge-src{animation:none}
}

/* ===========================================================================
   #system FEATURE CARDS — idle motion  (Tim, 25 Aug 2026: "needs some more
   animations here")

   The four cards were completely static, which made the section read as a
   printed list in the middle of a page where everything else is alive. Each
   icon now does the thing its card claims, on a slow loop, staggered so they
   never all move at once:

     screen  a scan line travels down the monitor
     star    it catches the light and twinkles
     chat    a live dot pulses in the corner, the same one the launcher uses
     inbox   a message lands in the tray

   All of it is transform/opacity on a 46px tile, so four looping animations
   cost one composited layer each and nothing on the main thread. Everything
   stops flat for prefers-reduced-motion.
   =========================================================================== */
#system .fcard{transition:transform .5s cubic-bezier(.2,.8,.3,1),
                          border-color .5s ease,box-shadow .5s ease}
#system .fcard:hover{transform:translateY(-6px);border-color:var(--line-2);
  box-shadow:0 22px 50px -20px rgba(0,0,0,.7)}
#system .fcard .ic{position:relative;overflow:hidden}
#system .fcard .ic svg{position:relative;z-index:2}

/* screen: a scan line down the monitor */
#system .fcard .ic-green::after{content:'';position:absolute;left:0;right:0;height:34%;z-index:1;
  background:linear-gradient(180deg,transparent,rgba(50,207,117,.42),transparent);
  animation:icScan 4.6s ease-in-out infinite}
@keyframes icScan{
  0%,72%,100%{transform:translateY(-120%);opacity:0}
  76%{opacity:1}
  96%{transform:translateY(300%);opacity:0}
}

/* star: catches the light */
#system .fcard .ic-gold svg{animation:icTwinkle 5.4s ease-in-out infinite;animation-delay:.7s}
@keyframes icTwinkle{
  0%,64%,100%{transform:scale(1) rotate(0);filter:none}
  72%{transform:scale(1.18) rotate(-8deg);filter:drop-shadow(0 0 9px rgba(255,198,61,.95))}
  84%{transform:scale(1) rotate(0);filter:none}
}

/* chat: the live dot, same green as the launcher's */
#system .fcard .ic-blue::after{content:'';position:absolute;top:8px;right:8px;z-index:3;
  width:7px;height:7px;border-radius:50%;background:var(--green);
  box-shadow:0 0 0 0 rgba(50,207,117,.6);animation:icLive 2.6s ease-out infinite;animation-delay:1.4s}
@keyframes icLive{
  0%,70%,100%{box-shadow:0 0 0 0 rgba(50,207,117,.55);opacity:.9}
  85%{box-shadow:0 0 0 7px rgba(50,207,117,0);opacity:1}
}

/* inbox: a message lands in the tray */
#system .fcard .ic-violet::after{content:'';position:absolute;left:50%;top:5px;z-index:1;
  width:13px;height:9px;margin-left:-6.5px;border-radius:2px;
  background:currentColor;opacity:0;animation:icDrop 5s ease-in infinite;animation-delay:2.1s}
@keyframes icDrop{
  0%,58%,100%{transform:translateY(-10px);opacity:0}
  66%{transform:translateY(0);opacity:.75}
  80%{transform:translateY(9px);opacity:0}
}

@media(prefers-reduced-motion:reduce){
  #system .fcard,#system .fcard .ic-gold svg{transition:none;animation:none}
  #system .fcard .ic-green::after,
  #system .fcard .ic-blue::after,
  #system .fcard .ic-violet::after{animation:none;opacity:0}
  #system .fcard:hover{transform:none}
}

/* ===========================================================================
   /diy/  —  THE DO IT YOURSELF GUIDE          (Tim, 14 Sep 2026)

   The page is a long read, so every claim on it now carries a mark: one 46px
   tile per card, one hue per idea, so a visitor scanning the page can find the
   step they wanted without reading the whole thing again.

   Three shapes, all built out of parts that already exist:
     .diy-set    the five settings, tile beside the setting rather than above it
     .diy-costs  the four costs as cards, then one total panel that counts up
     .diy-order  the running order as the house .steps strip

   Motion follows the #system rule above: the card reveal and the icon pop come
   free from the .mv auto tagger, and only THREE tiles loop after that, on a
   long stagger, so the page never looks busy. Everything stops for
   prefers-reduced-motion.
   =========================================================================== */
.diy .diy-chips{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;margin-top:22px}

/* the tile, wherever it is not already an .fcard or a .step */
.diy .diy-set .ic,.diy .diy-total .ic{width:46px;height:46px;border-radius:14px;
  display:grid;place-items:center;flex:none;margin:0}
.diy .diy-set .ic svg,.diy .diy-total .ic svg{width:22px;height:22px}

/* the five settings: the mark sits BESIDE the setting, so the numbered headings
   still read as one list down the page rather than as five separate cards */
.diy .diy-set{display:flex;gap:18px;align-items:flex-start;margin-bottom:14px;padding:26px}
.diy .diy-set p{margin:0;color:var(--ink-soft)}
@media(max-width:560px){.diy .diy-set{gap:14px;padding:22px}}

/* what it costs */
.diy .diy-costs{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:18px}
.diy .diy-costs .fcard{padding:26px 24px}
.diy .diy-total{display:flex;gap:18px;align-items:flex-start;padding:28px}
.diy .diy-big{margin:0;font-weight:800;font-size:1.3rem;letter-spacing:-.02em}
.diy .diy-big .grad{font-variant-numeric:tabular-nums}

/* the running order. Pinned to five columns on a desktop so the five steps
   read as one line rather than four and a stray fifth on its own row. */
.diy .diy-order{margin-top:28px}
@media(min-width:1040px){.diy .diy-order{grid-template-columns:repeat(5,1fr)}}
.diy .diy-order .step{padding:26px 22px}
.diy .diy-order .step::before{top:22px;right:20px;font-size:1.25rem}
/* the counter sits top right, so the title stops short of it rather than
   running underneath it when it wraps to a second line */
.diy .diy-order .step h3{padding-right:34px;font-size:1.02rem}

/* the icon lifts under the pointer, on every card shape the page uses */
@media(hover:hover){
  .diy .fcard .ic,.diy .step .ic,.diy .diy-set .ic{transition:transform .35s cubic-bezier(.22,1,.36,1)}
  .diy .fcard:hover .ic,.diy .step:hover .ic,.diy .diy-set:hover .ic{transform:translateY(-3px) scale(1.07)}
}

/* three loops, and only three. The keyframes are the ones the #system cards
   already use, so this adds behaviour and not weight. */
.diy .ic-scan,.diy .ic-live,.diy .ic-drop{position:relative;overflow:hidden}
.diy .ic-scan svg,.diy .ic-live svg,.diy .ic-drop svg{position:relative;z-index:2}
/* let the search engines in: a scan line travels down the tile */
.diy .ic-scan::after{content:'';position:absolute;left:0;right:0;height:34%;z-index:1;
  background:linear-gradient(180deg,transparent,rgba(50,207,117,.42),transparent);
  animation:icScan 5.2s ease-in-out infinite}
/* book or enquire: the live dot, the same one the chat launcher carries */
.diy .ic-live{overflow:visible}
.diy .ic-live::after{content:'';position:absolute;top:6px;right:6px;z-index:3;
  width:7px;height:7px;border-radius:50%;background:var(--green);
  animation:icLive 2.8s ease-out infinite;animation-delay:1.2s}
/* contact: a message lands in the tray */
.diy .ic-drop::after{content:'';position:absolute;left:50%;top:5px;z-index:1;
  width:13px;height:9px;margin-left:-6.5px;border-radius:2px;
  background:currentColor;opacity:0;animation:icDrop 5.4s ease-in infinite;animation-delay:2.4s}
/* build it with AI: the spark catches the light */
.diy .ic-twinkle svg{animation:icTwinkle 5.6s ease-in-out infinite;animation-delay:1.8s}

@media(prefers-reduced-motion:reduce){
  .diy .ic-scan::after,.diy .ic-live::after,.diy .ic-drop::after{animation:none;opacity:0}
  .diy .ic-twinkle svg{animation:none}
  .diy .fcard:hover .ic,.diy .step:hover .ic,.diy .diy-set:hover .ic{transform:none}
}

/* ===========================================================================
   #system STORY BAND — the two photographs under the feature cards
   (Tim, 25 Aug 2026)

   The four cards say what the system does. These two frames say what it feels
   like from both ends: a customer books from their kitchen table, and the owner
   knows about it a minute later. Deliberately one continuous row on desktop
   with a charge running between them, because the point is that the second
   thing happens BECAUSE of the first.

   The arc between them uses the same green pulse as the charge wave, so the
   whole page keeps telling one story with one visual language.
   =========================================================================== */
/* No top margin any more: the pair used to hang off the bottom of the features
   grid and needed the gap. It now owns its own section under the spotlight, so
   the section padding is the spacing. */
.story{display:grid;grid-template-columns:1fr auto 1fr;gap:22px;align-items:center}
.story-shot{margin:0;border-radius:var(--r);overflow:hidden;border:1px solid var(--line);
  background:var(--panel);position:relative;
  transition:transform .5s cubic-bezier(.2,.8,.3,1),border-color .5s ease,box-shadow .5s ease}
.story-shot:hover{transform:translateY(-5px);border-color:var(--line-2);
  box-shadow:0 24px 54px -22px rgba(0,0,0,.75)}
.story-shot img{display:block;width:100%;height:auto;aspect-ratio:4/3;object-fit:cover}
.story-shot figcaption{padding:20px 22px 24px;font-size:.92rem;line-height:1.6;
  color:var(--ink-soft)}
.story-shot figcaption b{display:block;margin-bottom:6px;font-size:1.05rem;font-weight:800;
  letter-spacing:-.01em;color:var(--ink)}

/* the charge crossing from the booking to the phone */
.story-arc{position:relative;width:74px;height:2px;border-radius:2px;
  background:linear-gradient(90deg,rgba(50,207,117,.12),rgba(50,207,117,.34),rgba(50,207,117,.12))}
.story-arc i{position:absolute;top:50%;left:0;width:9px;height:9px;margin-top:-4.5px;
  border-radius:50%;background:var(--green);
  box-shadow:0 0 14px rgba(50,207,117,.85);
  animation:storyArc 3.4s cubic-bezier(.5,0,.5,1) infinite}
@keyframes storyArc{
  0%,12%{left:0;opacity:0}
  20%{opacity:1}
  76%{left:calc(100% - 9px);opacity:1}
  88%,100%{left:calc(100% - 9px);opacity:0}
}
@media(max-width:820px){
  .story{grid-template-columns:1fr;gap:18px}
  /* vertical on a phone: the charge drops from one frame to the next */
  .story-arc{width:2px;height:54px;justify-self:center;
    background:linear-gradient(180deg,rgba(50,207,117,.12),rgba(50,207,117,.34),rgba(50,207,117,.12))}
  .story-arc i{left:50%;margin-left:-4.5px;top:0;margin-top:0;animation:storyArcDown 3.4s cubic-bezier(.5,0,.5,1) infinite}
}
@keyframes storyArcDown{
  0%,12%{top:0;opacity:0}
  20%{opacity:1}
  76%{top:calc(100% - 9px);opacity:1}
  88%,100%{top:calc(100% - 9px);opacity:0}
}
@media(prefers-reduced-motion:reduce){
  .story-shot{transition:none}
  .story-shot:hover{transform:none}
  .story-arc i{animation:none;left:50%;opacity:.9}
}

/* ===========================================================================
   FOUNDER SLOT + CLIENT VIDEO TESTIMONIALS + GOOGLE REVIEW MARQUEE
   Lifted out of the home page head 28 Aug 2026 when /info/ started using the
   same three blocks. One definition, same as the nav and the lead form: a
   second copy in a second head is how two pages quietly drift apart.
   =========================================================================== */
/* ---- founder slot (Tim intro) ---- */
.vsl-wrap{max-width:820px;margin:0 auto}
.vsl-frame{position:relative;aspect-ratio:16/9;border-radius:22px;overflow:hidden;border:1px solid var(--line-2);background:#04130a;box-shadow:0 30px 90px rgba(0,0,0,.5)}
.vsl-frame video,.vsl-frame iframe{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;border:0}
.vsl-frame > img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center 30%;display:block}
/* soft gradient so the overlay text is always legible */
.vsl-frame .vsl-scrim{position:absolute;inset:0;background:linear-gradient(90deg,rgba(2,10,6,.72) 0%,rgba(2,10,6,.30) 42%,transparent 70%),linear-gradient(0deg,rgba(2,10,6,.55),transparent 45%);pointer-events:none}
/* credentials overlay (bottom-left) */
.vsl-cred{position:absolute;left:26px;bottom:24px;right:26px;z-index:2;color:#fff}
.vsl-cred .name{font-size:1.5rem;font-weight:800;letter-spacing:-.01em;line-height:1.1;margin:0 0 10px;text-shadow:0 2px 18px rgba(0,0,0,.5)}
.vsl-cred ul{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:8px 10px}
.vsl-cred li{display:inline-flex;align-items:center;gap:7px;font-size:.8rem;font-weight:600;color:#eafff2;background:rgba(6,26,15,.5);border:1px solid rgba(50,207,117,.4);
  -webkit-backdrop-filter:var(--glass-sm);backdrop-filter:var(--glass-sm);box-shadow:var(--rim-sm);
  padding:6px 12px;border-radius:100px}
.vsl-cred li svg{width:14px;height:14px;color:var(--green);flex:0 0 14px}
@media(max-width:640px){
  /* on a short mobile frame, give the overlay its own strip under the photo
     so it never covers Tim's face the frame grows to fit */
  .vsl-frame{aspect-ratio:auto}
  .vsl-frame > img{position:relative;height:auto;aspect-ratio:16/10;object-position:center 22%}
  .vsl-scrim{background:linear-gradient(0deg,rgba(2,10,6,.92) 0%,rgba(2,10,6,.55) 34%,transparent 55%)}
  .vsl-cred{position:relative;left:0;right:0;bottom:0;padding:14px 18px 18px;background:#061a0f}
  .vsl-cred .name{font-size:1.15rem;margin-bottom:12px}
  .vsl-cred ul{flex-direction:column;gap:9px}
  .vsl-cred li{font-size:.78rem;padding:7px 12px;background:rgba(var(--wa),.04)}
}

/* ---- client video testimonials + moving google reviews (homepage) ---- */
.gv-pair{display:grid;grid-template-columns:repeat(2,1fr);gap:34px;max-width:820px;margin:0 auto}
@media(max-width:760px){.gv-pair{grid-template-columns:1fr;gap:40px;max-width:340px}}
.gv-wrap{position:relative;max-width:340px;margin:0 auto;width:100%}
.gv-frame{position:relative;aspect-ratio:9/16;border-radius:28px;overflow:hidden;border:1px solid var(--line-2);background:#04130a;box-shadow:0 30px 90px rgba(0,0,0,.55)}
.gv-frame video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;background:#000}
.gv-sound{position:absolute;left:50%;bottom:18px;transform:translateX(-50%);z-index:3;display:inline-flex;align-items:center;gap:8px;background:rgba(4,19,10,.72);border:1px solid rgba(50,207,117,.45);
  -webkit-backdrop-filter:var(--glass-sm);backdrop-filter:var(--glass-sm);box-shadow:var(--rim-sm);color:#eafff2;font-size:.78rem;font-weight:700;padding:9px 16px;border-radius:100px;cursor:pointer;white-space:nowrap}
.gv-sound svg{width:15px;height:15px;color:var(--green)}
.gv-sound[hidden]{display:none}
.gv-tag{display:flex;flex-direction:column;align-items:center;gap:2px;margin-top:16px;font-size:.82rem;color:var(--ink-mute);text-align:center}
.gv-tag b{color:var(--ink)}
.gv-quotes{margin:16px 0 0;list-style:none;padding:0;display:grid;gap:10px}
.gv-quotes li{font-size:.95rem;line-height:1.5;color:var(--ink-soft);border-left:2px solid var(--green);padding-left:13px}
.gv-quotes em{font-style:normal;font-weight:700;color:var(--ink)}
/* The edge fade is a SLIVER, not a seventh of the row (Tim, 15 Sep 2026: "the
   shadow fades out the reviews"). At 7% of a 1440 wide band the fade was 100px,
   which is a quarter of a 440px card, and in light mode a white card dissolving
   into a near white page reads as a review being wiped out rather than as one
   leaving the screen. 26px softens the cut at the edge and touches nothing
   that anybody is reading. */
/* THE ROW NEEDS ROOM UNDER THE CARDS (Tim, 17 Sep 2026: "fix shadows"). The
   track is exactly as tall as a card, so `overflow:hidden` was cutting every
   card's shadow off flush with its own top and bottom edge. What survived was
   the sideways half, pooling in the 18px gaps and ending in a hard horizontal
   line across the whole row: a grey slab on a near white page. The padding
   gives the shadow the room it needs to fall and fade, and the margins pull
   the row back to where it sat, so nothing else on the page moves. */
.gq-mq{overflow:hidden;position:relative;padding:26px 0 30px;margin:8px 0 -30px;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 26px,#000 calc(100% - 26px),transparent);
          mask-image:linear-gradient(90deg,transparent,#000 26px,#000 calc(100% - 26px),transparent)}
.gq-mq .track{display:flex;gap:18px;width:max-content;animation:gqmq var(--gq-dur,30s) linear infinite}

@keyframes gqmq{from{transform:translateX(0)}to{transform:translateX(-50%)}}
@media(max-width:760px){.gq-mq .track{animation-duration:calc(var(--gq-dur,30s) * 1.34)}}
@media(prefers-reduced-motion:reduce){.gq-mq .track{animation:none}}
.gq{width:440px;flex:0 0 440px;padding:22px 24px;display:flex;flex-direction:column}
@media(max-width:560px){.gq{width:300px;flex:0 0 300px;padding:20px 18px}}
.gq .stars{color:#ffc63d;letter-spacing:.12em;font-size:.9rem;margin-bottom:12px}
/* Every panel in the track is as tall as the longest review in it, so one very
   long review would stretch all of them. Tim, 13 Sep 2026: the cards are wider
   and shorter now, and the longer reviews carry a trimmed verbatim excerpt from
   scripts/build-home-reviews.mjs. The reviews page carries every word. */
.gq p{margin:0 0 14px;font-size:.95rem;line-height:1.55;color:var(--ink);display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:7;overflow:hidden}
/* Small body, one big verbatim line per review (Tim, 1 Oct 2026). */
.gq p{font-size:.84rem;line-height:1.55;color:var(--ink-soft);-webkit-line-clamp:9}
.gq p .gq-hl{font-size:1.18rem;line-height:1.32;font-weight:700;letter-spacing:-.01em;color:var(--ink)}
@media(max-width:560px){.gq p{-webkit-line-clamp:10}}
.gq footer{margin-top:auto;font-size:.82rem;color:var(--ink);font-weight:700}
.gq footer span{display:block;font-weight:500;color:var(--ink-mute)}
/* The 5.0 scorecard above the review marquee. Real rating, real count, both
   generated from data/google-reviews.json by scripts/build-home-reviews.mjs. */
.gq-score{display:flex;align-items:center;justify-content:center;gap:11px;flex-wrap:wrap;width:max-content;max-width:calc(100% - 32px);margin:52px auto 0;padding:11px 22px;border:1px solid var(--line-2);border-radius:100px;background:rgba(4,19,10,.55);text-decoration:none}
.gq-score:hover{border-color:rgba(50,207,117,.45)}
.gq-score .g-logo{width:19px;height:19px;flex:0 0 19px}
.gq-score b{font-size:1.3rem;font-weight:800;color:var(--ink);line-height:1}
.gq-score .stars{color:#ffc63d;letter-spacing:.1em;font-size:1rem;line-height:1}
.gq-score .gq-count{font-size:.9rem;color:var(--ink-mute)}
@media(max-width:560px){.gq-score{gap:9px;padding:10px 18px;margin-top:42px}.gq-score b{font-size:1.15rem}.gq-score .stars{font-size:.92rem}.gq-score .gq-count{font-size:13px}}
/* In the hero (Tim, 11 Sep 2026: the scorecard moved up under the CTAs) it is
   part of the left hand copy column, so it drops the band's centring and its
   52px of air and lines up hard left with the buttons above it. */
.hero .gq-score{margin:20px 0 0}
@media(max-width:560px){.hero .gq-score{margin-top:18px}}

/* ============================================================
   SCALUS PHONE STANDARD  (30 Aug 2026)
   From the fleet phone audit at 375 x 667 and 390 x 844.
   See the concept-site skill, lib/phone-standard.css
   Phone scoped or legibility only, desktop untouched.
   ============================================================ */

/* iOS zooms the whole page when a focused field is under 16px and an older
   iPhone never zooms back out. That includes the site chat input. */
@media(max-width:680px){
  input[type=text],input[type=email],input[type=tel],input[type=search],
  input[type=number],input[type=date],input:not([type]),textarea,select,
  .sc-cw .cw-in input{font-size:16px}
}

/* Nothing a visitor reads sits under 12px on a phone. */
@media(max-width:680px){
  .fr-legend,.hq-count,.sp-domain,.kicker,.eyebrow,.micro,.meta,.tag,
  .legal,.legal a,small,figcaption{font-size:12px}
  .fr-legend,.kicker,.eyebrow{letter-spacing:.1em}
}

/* Finger sized hit areas, look unchanged. */
@media(max-width:680px){
  footer a,.foot a,.crumbs a,.sp-domain{display:inline-block; padding:7px 0}
}
.powering-lead{font-size:.72rem}
@media(max-width:680px){
  .cl-cat,.cl-when,.cl-quote,.cl-by,.powering-lead{font-size:12px}
  footer.site p:has(a + a) a{font-size:12px; padding:10px 13px}
  footer.site a,footer.site li a{display:inline-block; padding:7px 0}
}
@media(max-width:680px){
  .tag,.stat-row span,.pw-tag,.chip{font-size:12px}
}
@media(max-width:680px){
  .pcard .tag,.work-card .shot .tag,.work-card .stat-row span,.reel .tag{font-size:12px; letter-spacing:.12em}
}
@media(max-width:680px){
  .mbp-domain,.upc-tag,.upc-date,.upc-meta,.up-tag,.up-date{font-size:12px}
}
/* Inline sizes under 12px were stripped from the markup and land here, so the
   phone floor can reach them. Desktop keeps the finer setting. */
.tiny-inline{font-size:.72rem}
@media(max-width:680px){
  /* Marked important because this page nests deeply, for example
     footer.site p:has(a + a) ~ p beats any short class rule. It is phone
     scoped, so desktop is untouched.
     NOTE: .mv is a shared reveal utility that sits on form fields too, so a
     font-size must never be attached to it. Name the text classes instead. */
  .tiny-inline,.sh-note,.dmo-chip,.dmo-name,.stars span,
  .disclaimer,.lb,small,.tl .t,.timeline .t,span.t{font-size:12px !important}
}
/* the logo link in the top bar was a 30px target */
@media(max-width:680px){ nav.nav .bar > a{display:inline-flex; align-items:center; min-height:36px} }

/* ── LIGHT SECTIONS (Tim, 2 Sep 2026) ─────────────────────────────────────
   .sec-light flips a section to the white light look the homepage hero wears.
   The background lives here as the no JS fallback; when the node canvas boots
   it adds html.net-paint and paints these bands itself, so the constellation
   shows through in a faint grey green instead of being covered. Ink overrides
   below re colour the shared dark palette components that appear inside. */
/* THE LIGHT RUN (Tim, 3 Sep 2026). Three light sections now sit back to back:
   the hero, Currently powering and Our work. Painting the same white to mint
   gradient inside each of them restarted it twice, which drew two visible seams
   across what is meant to read as one pane of light. So the gradient is split
   across the run instead. The FIRST section opens white, any middle section
   holds the mid tone, and the LAST one closes out to mint and then fades away
   to nothing, so the join into the dark band underneath is a gradient rather
   than a ruled line. Fading to transparent rather than to the dark colour keeps
   the constellation and the ambient glow visible through the join. */
.sec-light{background:#f8fbf9}
.sec-light:not(.sec-light + .sec-light){background:linear-gradient(180deg,#ffffff,#f8fbf9)}
.sec-light:not(:has(+ .sec-light)){background:linear-gradient(180deg,#f8fbf9,#eef5f0)}
.sec-light:not(.sec-light + .sec-light):not(:has(+ .sec-light)){background:linear-gradient(180deg,#ffffff,#eef5f0)}
html.net-paint .sec-light{background:none}

/* THE JOIN OUT OF THE LIGHT. The run closes on mint at its very last pixel, so
   the dark section directly underneath opens on that same mint and walks down
   to its own colour over the next 190px. Carrying the gradient on the DARK side
   rather than fading the light one out is what removes the last hairline: the
   two colours are interpolated directly instead of a translucent mint being
   composited over whatever happens to sit behind.
   The laptop band carries its own version of this join (see .mbp-band above),
   because it also has to land on the near black it shares with the spotlight,
   so it is excluded here rather than left to a specificity race. */
.sec-light + section:not(.sec-light):not(.mbp-band){
  background:linear-gradient(180deg,#eef5f0 0,rgba(238,245,240,.09) 80px,rgba(238,245,240,0) 200px)}
.sec-light h2,.sec-light h3{color:#0d1613}
.sec-light .line{color:#0d1613}
.sec-light .sect-head p{color:#42504a}
.sec-light p{color:#42504a}
.sec-light .work-card{background:#ffffff;border-color:rgba(12,26,19,.1);
  box-shadow:0 22px 54px -26px rgba(15,35,25,.25)}
.sec-light .work-card .body h2,.sec-light .work-card .body h3{color:#0d1613}
.sec-light .work-card .body p{color:#42504a}
.sec-light .work-card .stat-row span{color:#5a6a62;border-color:rgba(12,26,19,.16);
  background:rgba(12,26,19,.04)}
.sec-light .story-shot{border-color:rgba(12,26,19,.1);background:#ffffff;
  box-shadow:0 22px 54px -26px rgba(15,35,25,.25)}
.sec-light .story-shot figcaption{color:#42504a}
.sec-light .story-shot figcaption b{color:#0d1613}
.sec-light .grad-flow{background-image:
  linear-gradient(100deg,transparent 40%,rgba(var(--wa),.85) 50%,transparent 60%),
  linear-gradient(100deg,#0f9d63,#0b7f8f,#6b4fd8,#b8478a,#a86a15,#0f9d63)}
.sec-light .grad{color:#12a155}
/* a ghost button is white ink on a 6% white fill, which is invisible on a light
   band. See all my work at the foot of Our work was disappearing entirely. */
.sec-light .btn-ghost{background:rgba(12,26,19,.05);border-color:rgba(12,26,19,.16);color:#0d1613}
.sec-light .btn-ghost:hover{background:rgba(12,26,19,.09);border-color:rgba(12,26,19,.26)}

/* THE CLIENTS TICKER IN A LIGHT BAND (Tim, 3 Sep 2026). Currently powering used
   to be a dark slab between two light ones. Now it joins the light run, so the
   cards need the same treatment the work cards already get: a white card with a
   real border and a soft drop shadow instead of a 4% white film that vanishes
   on white, and dark ink on every line inside it. The phone bezels stay black,
   which is what makes them read as devices rather than thumbnails. */
.sec-light .cl-card{background:#ffffff;border-color:rgba(12,26,19,.1);color:#0d1613;
  box-shadow:0 18px 44px -24px rgba(15,35,25,.28)}
.sec-light .cl-cat,.sec-light .cl-by{color:#5a6a62}
.sec-light .cl-quote{color:#42504a}
.sec-light .cl-when{color:#12a155}
.sec-light .cl-when::before{background:#12a155;box-shadow:0 0 0 3px rgba(18,161,85,.18)}
.sec-light .cp-title{color:#5a6a62}
/* the no JS fallback pills */
.sec-light .cl-pill{background:#ffffff;border-color:rgba(12,26,19,.1);color:#0d1613;
  box-shadow:0 18px 44px -24px rgba(15,35,25,.28)}
.sec-light a.cl-pill:hover{background:#ffffff;border-color:rgba(12,26,19,.22)}
.sec-light .cl-pill span{color:#5a6a62}
.sec-light .cl-pill .ci{color:#12a155}
.sec-light .cl-pill.cl-more{border-color:rgba(18,161,85,.45);background:rgba(18,161,85,.06)}
.sec-light .cl-pill.cl-more b{color:#12a155}
.gq footer .g-logo{width:13px;height:13px;vertical-align:-2px;margin-right:5px;display:inline-block}

/* Jabez quote card floating with the phone in the Real sites band. */
.mbp-quote{position:absolute;right:-18px;bottom:-26px;z-index:4;margin:0;max-width:225px;
  background:#fff;border-radius:14px;padding:12px 16px;text-align:left;
  box-shadow:0 18px 44px rgba(0,0,0,.5)}
.mbp-quote .stars{color:#f5b301;font-size:.78rem;letter-spacing:2px}
.mbp-quote blockquote{margin:5px 0 6px;font-size:.85rem;line-height:1.45;color:#1c2620;font-weight:600}
.mbp-quote figcaption{font-size:.7rem;color:#5a6a62;font-weight:600}
@media(max-width:900px){.mbp-quote{right:2px;bottom:-16px;max-width:190px;padding:10px 13px}}
/* The card turns over with the site behind it, so it has to be out of sight
   while the words are being swapped. Same timing as the domain line fading,
   which is what it is changing alongside. */
.mbp-quote{transition:opacity .3s ease, transform .3s ease}
.mbp-quote-off{opacity:0;transform:translateY(6px)}
/* On a phone the floating card landed over the domain line, which was tolerable
   while the quote never changed and nothing depended on reading the two
   together. Now that the words belong to the site named on that line, covering
   the line hides the whole point, so on a narrow screen the card drops out of
   the float and sits under the laptop where both can be read at once. */
@media(max-width:520px){
  .mbp-quote{position:static;right:auto;bottom:auto;max-width:none;
    margin:14px auto 0;padding:13px 16px;
    box-shadow:0 12px 30px -14px rgba(0,0,0,.45)}
  .mbp-quote-off{transform:none}
}

/* The update push animation: the laptop builds an update, nodes carry it down
   to the little sites underneath, and each site flips grey to green.
   It plays the release at the front of the stack below it (9 Oct 2026): the
   screen shows that version number, a chip beside the laptop says what is
   happening, every site gets a tick as it lands, and the line underneath
   says what that release was applied to. sc.js writes the words. */
.upd-anim{position:relative;max-width:720px;margin:0 auto 34px}
.ua-laptop{position:relative;width:150px;margin:0 auto}
.ua-screen{position:relative;height:84px;border-radius:8px 8px 0 0;background:#0e1613;border:1px solid var(--line-2);
  border-bottom:0;padding:40px 12px 0}
.ua-ver{position:absolute;left:0;right:0;top:13px;text-align:center;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:.86rem;font-weight:700;letter-spacing:-.02em;color:#32cf75;text-shadow:0 0 14px rgba(50,207,117,.6)}
.ua-bar{height:8px;border-radius:999px;background:rgba(255,255,255,.12);overflow:hidden}
.ua-bar i{display:block;height:100%;width:0;background:var(--green);border-radius:999px;
  transition:width 1.15s ease}
.ua-laptop.building .ua-bar i{width:100%}
.ua-laptop.reset .ua-bar i{transition:none;width:0}
.ua-base{height:7px;background:#1c2621;border-radius:0 0 10px 10px;margin:0 -14px;
  border:1px solid var(--line-2);border-top:0}
.ua-chip{display:inline-flex;align-items:center;gap:7px;padding:6px 12px;border-radius:999px;
  font-size:.74rem;font-weight:700;color:var(--ink);background:rgba(var(--wa),.07);border:1px solid rgba(var(--wa),.18);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 10px 24px -12px rgba(0,0,0,.6);
  -webkit-backdrop-filter:blur(12px) saturate(1.6);backdrop-filter:blur(12px) saturate(1.6);
  opacity:0;transform:translateY(6px) scale(.96);transition:opacity .35s ease,transform .45s cubic-bezier(.2,.9,.25,1)}
.ua-chip.on{opacity:1;transform:none}
.ua-chip-build{position:absolute;z-index:3;left:calc(100% - 4px);top:-15px;white-space:nowrap}
.ua-chip-build::before{content:"";flex:none;width:7px;height:7px;border-radius:50%;background:var(--green);
  box-shadow:0 0 10px rgba(50,207,117,.9)}
.ua-sites{display:flex;gap:14px;justify-content:center;margin-top:56px}
.ua-site{position:relative;width:92px;border-radius:10px;border:1px solid rgba(var(--wa),.16);background:rgba(var(--wa),.045);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.1);
  padding:9px;filter:grayscale(1);opacity:.5;
  transition:filter .5s,opacity .5s,border-color .5s,box-shadow .5s}
.ua-site .h{height:5px;border-radius:999px;background:rgba(var(--wa),.28);width:60%}
.ua-site .l{height:4px;border-radius:999px;background:rgba(var(--wa),.14);margin-top:6px}
.ua-site .b{height:10px;width:44px;border-radius:999px;background:rgba(50,207,117,.55);margin-top:9px}
.ua-site.updated{filter:none;opacity:1;border-color:rgba(50,207,117,.55);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.14),0 0 24px rgba(50,207,117,.28)}
.ua-tick{position:absolute;right:-7px;top:-7px;width:18px;height:18px;border-radius:50%;background:var(--green);
  box-shadow:0 4px 10px rgba(0,0,0,.4);transform:scale(0);transition:transform .4s cubic-bezier(.3,1.7,.4,1)}
.ua-tick::after{content:"";position:absolute;left:6px;top:3px;width:4px;height:8px;border:solid #04130a;border-width:0 2px 2px 0;transform:rotate(45deg)}
.ua-site.updated .ua-tick{transform:scale(1)}
.ua-applied{display:flex;justify-content:center;margin:18px auto 0;height:62px;padding:0 4px}
.ua-applied .ua-chip{display:block;max-width:580px;padding:9px 16px;border-radius:16px;text-align:center;
  font-size:.82rem;font-weight:500;line-height:1.4;color:var(--ink-soft);align-self:center}
.ua-applied .ua-chip span{display:inline}
.ua-applied .ua-chip{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.ua-applied b{color:var(--ink);font-weight:700;margin-right:.35em}
.ua-dot{position:absolute;left:0;top:0;width:5px;height:5px;border-radius:50%;background:#7df0ae;
  box-shadow:0 0 10px rgba(125,240,174,.95);z-index:2;pointer-events:none}
@media(max-width:640px){.ua-sites{gap:8px;margin-top:44px}.ua-site{width:58px;padding:6px}
  .ua-site .b{width:30px;height:8px}
  .ua-chip-build{left:calc(100% - 22px);top:-22px;font-size:.7rem;padding:5px 10px}
  .ua-tick{right:-6px;top:-6px;width:16px;height:16px}.ua-tick::after{left:5px;top:2px}
  .ua-applied{height:82px}.ua-applied .ua-chip{-webkit-line-clamp:3;font-size:.8rem}}

/* GLASS SEAMS: REMOVED (Tim, 10 Sep 2026).
   Every background change used to carry a lit glass strip straddling the join,
   a 22px pane with a bright specular hairline along its top. On a page that is
   now near black end to end it stopped reading as glass and read as a ruled
   line across the screen, brightest right where the laptop band meets the
   client spotlight. The joins are gone instead of dressed: the laptop band no
   longer paints its own darker black, and the spotlight veil resolves to the
   page background, so the run reads as one continuous surface with nothing
   drawn across it. Do not reinstate a divider here. */

/* ── ACTIVE CLIENTS MAP + THE PHONE (Tim, 8 Sep 2026, rebuilt 15 Sep 2026) ──
   Two halves of one stage. The map of greater Melbourne on the left, and on
   the right the active client's own website running in an iPhone Duo. See
   clientmap.js. Nothing here is transparent: the constellation behind the
   homepage was showing through the old panel.

   The side column is flush to the stage edges rather than inset, so its two
   corners ARE the stage corners and there is no second radius to keep in step
   with the first. */
/* Tim, 16 Sep 2026: less height. The stage was two thirds of a laptop screen
   for one map and one phone, so it came down from 660 to 540 and the phone
   beside it from 266 to 224 wide, which is what actually sets the floor. */
/* Tim, 17 Sep 2026: "make the map as wide as the normal elements on the
   website, the whole section". It used to run to 1320px while everything above
   and below it stops at the .wrap, so the stage hung 112px past the heading on
   each side and read as a different page. The numbers here ARE the .wrap: it
   is max-width 1140 with 22px of padding, so its content measures 1096px on a
   wide screen and 100% less the two 22px gutters on anything narrower. Change
   .wrap and these two values have to move with it. */
.cm-stage{--stage-r:22px;
  position:relative;margin:0 auto;width:calc(100% - 44px);max-width:1096px;
  /* No fixed height (Tim, 16 Sep 2026: "just high enough for the phone"). The
     row is as tall as the side panel's device plus its padding, and the map
     fills whatever that comes to. min-height is only a floor for a very short
     caption. */
  height:auto;min-height:340px;
  display:grid;grid-template-columns:minmax(0,1fr) clamp(330px,31%,430px);
  border:1px solid var(--line);border-radius:var(--stage-r);overflow:hidden;
  /* No ground of its own any more (Tim, 16 Sep 2026): the map half paints its
     own, and everything else lets the page's constellation and ambient glow
     through, which is what makes the side read as glass rather than a box. */
  background:transparent;isolation:isolate}
.cm-stage.cm-empty{display:none}
/* The map half. */
.cm-map{position:relative;min-width:0;overflow:hidden}
.cm-plot{position:absolute;inset:0;cursor:grab;touch-action:pan-y;user-select:none;
  background:
    radial-gradient(90% 70% at 50% 0%,rgba(50,207,117,.06),transparent 60%),
    #0a0e12}
.cm-plot.dragging{cursor:grabbing}
.cm-svg{display:block;width:100%;height:100%}
.cm-landfill{fill:#101613;stroke:rgba(150,200,235,.28);stroke-linejoin:round}
/* Terrain. Real SRTM shaded relief for greater Melbourne, baked to a PNG by
   scripts/build-map-relief.mjs and placed by clientmap.js as one <image> in lon
   and mercator space. A raster and not a filter on purpose: this viewBox goes
   below a degree wide and an SVG filter length is taken in user units there, so
   anything sized in px becomes kilometres. The alpha is baked into the image,
   so the water stays clear and the ground carries the light. */
.cm-terrain{pointer-events:none}
.cm-relief{pointer-events:none}
.cm-back circle{fill:rgba(var(--wa),.12);transition:opacity .6s ease}
.cm-back{transition:opacity .6s ease}
.cm-lab text{fill:rgba(var(--wa),.42);font-family:inherit;font-weight:600;letter-spacing:.02em;
  paint-order:stroke;stroke:#0a0e12;stroke-width:.35em;stroke-linejoin:round;pointer-events:none}
.cm-lab0 text{fill:rgba(var(--wa),.62);text-transform:uppercase;letter-spacing:.14em}
.cm-city{fill:rgba(var(--wa),.55)}
.cm-lab2 text{fill:rgba(var(--wa),.34);font-weight:500}
/* THE ONE THE MAP IS ON IS THE ONLY GREEN ONE (Tim, 19 Sep 2026: "make the non
   selected dots in grey, for light, and reverse white for dark mode"). Twenty
   odd green dots all calling at once left nothing for the client the map has
   actually flown to, so at rest a dot is neutral and it goes green when it is
   the one on screen, hovered or tabbed to. One rule covers both themes: --wa is
   white in dark and the ink colour in light, so the rest state is white on dark
   and grey on light without a second declaration. The stroke still needs its
   own light value further down, because it is a solid colour and not an alpha
   over --wa. */
.cm-dot{fill:rgba(var(--wa),.62);stroke:rgba(7,8,8,.9);transition:fill .35s ease}
.cm-pin:focus-visible .cm-dot,.cm-pin:hover .cm-dot,.cm-pin.on .cm-dot{fill:var(--green)}
/* The resting glow goes neutral with the dot, and nearly away. A green aura at
   .2 was there to make a green dot bloom; the same disc in a neutral colour is
   just a grey smudge, twenty odd of them, each more than twice the width of the
   dot it sits under. It stays only as a faint seat that separates a dot from
   the land, and the real halo belongs to the one that is lit. */
.cm-glow{fill:rgba(var(--wa),.045);transition:fill .3s ease}
.cm-hit{fill:transparent}
.cm-pin{cursor:pointer}
.cm-pin:focus{outline:none}
.cm-pin:focus-visible .cm-glow,.cm-pin:hover .cm-glow,.cm-pin.on .cm-glow{fill:rgba(50,207,117,.42)}
.cm-halo{fill:rgba(50,207,117,.34);opacity:0}
/* The little name that hangs off the lit dot. The phone beside it carries
   everything else, so this stays to the name and the suburb. */
.cm-tag{position:absolute;z-index:5;display:none;flex-direction:column;gap:1px;
  padding:7px 11px;border-radius:10px;transform:translateY(-50%);
  border:1px solid rgba(50,207,117,.4);background:#0b100d;box-shadow:0 10px 30px rgba(0,0,0,.5);
  pointer-events:none;white-space:nowrap}
.cm-tag.on{display:flex}
.cm-tag.flip{transform:translate(-100%,-50%)}
.cm-tag b{font-size:.84rem;color:var(--ink);line-height:1.25}
.cm-tag span{font-size:.7rem;color:var(--green);font-weight:600}
/* Zoom buttons, top right of the MAP half, not the stage. */
.cm-ctl{--ctl-gap:15px;position:absolute;top:14px;right:14px;z-index:4;display:flex;flex-direction:column;gap:6px}
.cm-ctl button{position:relative;isolation:isolate;width:38px;height:38px;border:1px solid transparent;
  border-radius:calc(var(--stage-r) - var(--ctl-gap));
  background:linear-gradient(180deg,rgba(var(--wa),.11),rgba(var(--wa),.03));
  -webkit-backdrop-filter:blur(16px) saturate(210%) brightness(1.22);
  backdrop-filter:blur(16px) saturate(210%) brightness(1.22);
  box-shadow:0 10px 26px -10px rgba(0,0,0,.65),inset 0 1px 0 rgba(var(--wa),.2);
  color:var(--ink);cursor:pointer;display:grid;place-items:center;padding:0;
  transition:background .2s,box-shadow .2s}
.cm-ctl button:hover{background:linear-gradient(180deg,rgba(50,207,117,.2),rgba(50,207,117,.06));
  box-shadow:0 10px 26px -10px rgba(0,0,0,.65),inset 0 1px 0 rgba(var(--wa),.28),
    0 0 0 1px rgba(50,207,117,.4)}
.cm-ctl svg{position:relative;z-index:2}
.cm-ctl svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}

/* THE LAPTOP (Tim, 16 Sep 2026), the same machine the .mbp band up the page
   uses, at the size this panel has room for. It carries the DESKTOP capture, so
   a narrow screen is not shown a phone page stretched across a lid. Hidden
   above 900, where the phone has the room to stand up instead. */
.cm-lap{display:none;width:100%;max-width:340px;margin:0 auto}
.cm-lap-lid{--lid-r:12px; --lid-gap:8px;
  position:relative;border-radius:var(--lid-r);padding:var(--lid-gap) var(--lid-gap) 10px;
  background:linear-gradient(155deg,#4a4e51 0%,#1b1d1f 26%,#0e0f10 62%,#2a2c2e 100%);
  box-shadow:0 24px 48px -16px rgba(0,0,0,.85),inset 0 1px 0 rgba(var(--wa),.12),
    0 0 0 .5px rgba(0,0,0,.8)}
.cm-lap-screen{position:relative;aspect-ratio:1.54;overflow:hidden;background:#070a08;
  border-radius:calc(var(--lid-r) - var(--lid-gap));
  box-shadow:0 0 0 1px rgba(0,0,0,.9) inset;isolation:isolate;transform:translateZ(0)}
.cm-lap-reel{position:absolute;inset:0;overflow:hidden}
.cm-lap-reel.is-blank{background:
  radial-gradient(120% 90% at 50% 0%,rgba(50,207,117,.14),transparent 62%),#0b100d}
.cm-lap-page{position:absolute;top:0;left:0;width:100%;height:auto;display:block;
  transform:translate3d(0,0,0);will-change:transform;animation:cmPageIn .6s ease both}
.cm-lap-sheen{position:absolute;inset:0;z-index:2;pointer-events:none;
  background:linear-gradient(118deg,rgba(var(--wa),.1) 0%,rgba(var(--wa),.03) 26%,rgba(var(--wa),0) 46%)}
/* the base: a shallow wedge with the notch you grip it by */
.cm-lap-base{display:block;position:relative;height:9px;width:104%;left:-2%;
  background:linear-gradient(180deg,#313436,#151618 55%,#0d0e0f);
  border-radius:0 0 9px 9px;clip-path:polygon(1.4% 0,98.6% 0,100% 100%,0 100%);
  box-shadow:0 16px 28px -12px rgba(0,0,0,.8)}

/* ---- the side: the client's site on a device ------------------------------
   466 x 678 is the Duo's cover screen in CSS pixels, so the proportion of the
   glass is the real one. The casing radius is 34 and the bezel is 8, so the
   screen radius is 26: outer minus the gap, the house rule, with no second
   number to keep in step. */
/* Tim, 16 Sep 2026: less height again, and this half more transparent. Both
   come from the same change. The phone moved BESIDE the caption rather than
   above it, so the panel is only as tall as the device, and the ground became
   glass over the page instead of a solid slab. */
.cm-side{position:relative;z-index:6;display:flex;flex-direction:row;
  align-items:center;justify-content:center;gap:16px;padding:18px;
  border-left:1px solid var(--line);min-width:0;
  background:linear-gradient(145deg,rgba(var(--wa),.075),rgba(var(--wa),.02) 55%,rgba(var(--wa),.05));
  -webkit-backdrop-filter:blur(20px) saturate(190%) brightness(1.1);
  backdrop-filter:blur(20px) saturate(190%) brightness(1.1)}
.cm-cap{flex:1 1 auto}
/* AN iPHONE 18 PRO MAX, BUILT FROM APPLE'S PUBLISHED NUMBERS (Tim, 16 Sep 2026:
   the pill was not the real shape, get the real template). Everything below is
   a ratio off the ONE number --dw, the body width, so the device is correct at
   any size it is drawn.

     body            78.0 x 163.4 mm            apple.com/iphone-18-pro/specs
     display         2868 x 1320 px at 460 ppi  =  158.36 x 72.89 mm
     band            (78.0 - 72.89) / 2         =  2.56 mm  = 3.28% of --dw
     screen radius   55 pt of a 440 pt width    =  9.11 mm  = 11.68% of --dw
     body radius     screen + band              =  11.67 mm = 14.96% of --dw

   The band works out the same top and bottom as it does at the sides, which is
   the uniform bezel, and the two radii are concentric by arithmetic rather than
   by a second number anyone has to keep in step.

   THE PILL. The 18 Pro shrank the Dynamic Island from 20.76 mm to 13.49 mm by
   moving the flood illuminator under the display, so it is 18.5% of the screen
   width now and not the 28.5% every earlier iPhone wore. The height did not
   change with it: still 6.07 mm, which is 3.83% of the screen height, so the
   pill is much rounder than the old one. That is the shape that was wrong. */
.cm-duo{--dw:158px;
  position:relative;width:100%;max-width:var(--dw);flex:none;
  padding:calc(var(--dw) * .0328);border-radius:calc(var(--dw) * .1496);
  background:linear-gradient(152deg,#6a6f73 0%,#2f3235 20%,#111214 50%,#292c2f 76%,#666b6f 100%);
  box-shadow:0 26px 52px -18px rgba(0,0,0,.9),0 0 0 .5px rgba(0,0,0,.8),
             inset 0 1px 0 rgba(var(--wa),.22),inset 0 -1px 0 rgba(0,0,0,.6)}
/* The rails. One element a side, the buttons painted as separate bands of one
   background so the count is not capped at what two pseudo elements allow:
   action, volume up, volume down on the left; power and camera control right. */
.cm-duo-fold,.cm-duo-keys{position:absolute;top:0;bottom:0;
  width:calc(var(--dw) * .017);border-radius:calc(var(--dw) * .01)}
.cm-duo-fold{left:calc(var(--dw) * -.011);
  background:
    linear-gradient(90deg,#7e8387,#2a2d30) 0 17% / 100% 4.4% no-repeat,
    linear-gradient(90deg,#7e8387,#2a2d30) 0 27% / 100% 7.6% no-repeat,
    linear-gradient(90deg,#7e8387,#2a2d30) 0 37% / 100% 7.6% no-repeat}
.cm-duo-keys{right:calc(var(--dw) * -.011);
  background:
    linear-gradient(270deg,#7e8387,#2a2d30) 0 26% / 100% 10.5% no-repeat,
    linear-gradient(270deg,#7e8387,#2a2d30) 0 42% / 100% 5.2% no-repeat}
.cm-duo-screen{position:relative;aspect-ratio:1320/2868;border-radius:calc(var(--dw) * .1168);overflow:hidden;
  background:#070a08;box-shadow:0 0 0 1px rgba(0,0,0,.9) inset;
  /* WebKit lets a transformed child paint outside a rounded overflow clip
     unless the clipper has its own layer: the page's square corners poked out
     of the phone while it panned. isolation gives it that layer. */
  isolation:isolate;transform:translateZ(0)}
.cm-duo-reel{position:absolute;inset:0;overflow:hidden}
.cm-duo-reel.is-blank{background:
  radial-gradient(120% 90% at 50% 0%,rgba(50,207,117,.14),transparent 62%),#0b100d}
.cm-duo-page{position:absolute;top:0;left:0;width:100%;height:auto;display:block;
  transform:translate3d(0,0,0);will-change:transform;
  animation:cmPageIn .6s ease both}
@keyframes cmPageIn{from{opacity:0}to{opacity:1}}
.cm-duo-island{position:absolute;top:1.15%;left:50%;transform:translateX(-50%);z-index:3;
  width:18.5%;height:3.83%;border-radius:999px;background:#05070a;
  box-shadow:inset 0 0 0 .5px rgba(var(--wa),.07)}
.cm-duo-sheen{position:absolute;inset:0;z-index:2;pointer-events:none;
  background:linear-gradient(118deg,rgba(var(--wa),.11) 0%,rgba(var(--wa),.03) 26%,
    rgba(var(--wa),0) 46%)}
/* The card under the phone: who it is and what they said. */
.cm-cap{width:100%;display:flex;flex-direction:column;gap:2px;text-align:left;
  opacity:0;transform:translateY(7px);transition:opacity .45s ease,transform .45s ease}
.cm-cap.in{opacity:1;transform:none}
.cm-cap-n{font-size:1rem;color:var(--ink);line-height:1.25;font-weight:700}
.cm-cap-c{font-size:.76rem;color:var(--green);font-weight:600}
.cm-cap-w{font-size:.74rem;color:var(--ink-soft)}
.cm-cap-w i{font-style:normal;color:var(--ink-mute)}
.cm-cap-w i::before{content:"\00b7";margin:0 5px}
.cm-cap-q{display:flex;flex-direction:column;gap:3px;margin-top:8px}
.cm-cap-q .cl-quote{font-size:.78rem;line-height:1.35;color:var(--ink-soft);white-space:normal;max-width:none}
.cm-cap-q .cl-by{font-size:.68rem;color:var(--ink-mute);font-weight:600}
/* No JS: the pills are the whole section, and the stage stops being a stage. */
.cm-fallback{display:none}
.cm-stage:not(.cm-ready){display:block;height:auto;min-height:0}
.cm-stage:not(.cm-ready) .cm-map,.cm-stage:not(.cm-ready) .cm-side{display:none}
.cm-stage:not(.cm-ready) .cm-fallback{display:flex;flex-wrap:wrap;justify-content:center;
  gap:12px;padding:20px 16px}
/* The note. House glass: a real backdrop blur with a saturation lift, a thin
   light edge and an inner highlight, so it reads as a pane laid on the map
   rather than a grey box sitting over it.
   The radius is CONCENTRIC. The stage is --stage-r and the note is inset 12px,
   so the inner radius is the outer minus that gap, and the two curves stay
   parallel at every breakpoint without a second number to keep in step. */
.cm-note{position:absolute;left:12px;top:12px;z-index:4;margin:0;max-width:252px;
  display:flex;flex-direction:column;gap:1px;
  padding:8px 13px;border-radius:calc(var(--stage-r) - 12px);
  /* Liquid glass, the house build (Tim, 16 Sep 2026), same three layers the
     dock wears: a barely there tint over a bright blur, a 1px rim cut out of a
     gradient with mask-composite, and a specular sheen across the top. */
  background:linear-gradient(180deg,rgba(var(--wa),.1),rgba(var(--wa),.025));
  border:1px solid transparent;
  box-shadow:0 14px 34px -12px rgba(0,0,0,.6),inset 0 1px 0 rgba(var(--wa),.2),
    inset 0 -1px 0 rgba(var(--wa),.05);
  -webkit-backdrop-filter:blur(16px) saturate(210%) brightness(1.22);
  backdrop-filter:blur(16px) saturate(210%) brightness(1.22)}
.cm-note::before,.cm-ctl button::before{content:"";position:absolute;inset:0;z-index:1;
  border-radius:inherit;padding:1px;pointer-events:none;
  background:linear-gradient(150deg,rgba(var(--wa),.55),rgba(var(--wa),.06) 42%,
    rgba(var(--wa),0) 60%,rgba(var(--wa),.28));
  -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}
.cm-note::after,.cm-ctl button::after{content:"";position:absolute;inset:0;z-index:0;
  border-radius:inherit;pointer-events:none;
  background:radial-gradient(120% 70% at 28% -20%,rgba(var(--wa),.22),rgba(var(--wa),0) 70%)}
.cm-note > *{position:relative;z-index:2}
.cm-note-t{font-size:.82rem;font-weight:700;letter-spacing:-.01em;line-height:1.3;color:var(--ink)}
.cm-note span{font-size:.66rem;line-height:1.35;color:var(--ink-mute)}
/* "Active" in moving green. The gradient is three times the width of the word
   so there is something to travel; clipped to the glyphs, so the animation is
   the paint and never the layout. */
.cm-note-t em{font-style:normal;font-weight:800;
  background:linear-gradient(100deg,#1f9d57,#32cf75,#8ef0b6,#32cf75,#1f9d57);
  background-size:300% 100%;
  -webkit-background-clip:text;background-clip:text;color:transparent;
  animation:cm-live 5.5s linear infinite}
@keyframes cm-live{from{background-position:0% 50%}to{background-position:300% 50%}}
@media(prefers-reduced-motion:reduce){
  .cm-note-t em{animation:none;background-position:50% 50%}
}
/* Under 900 the two halves stack, because side by side means two things at
   half size and the point of this rebuild was one thing at a time, big enough
   to read (Tim, 15 Sep 2026). The map keeps a real height of its own and the
   phone sits under it with the words beside it. */
@media(max-width:899px){
  .cm-stage{grid-template-columns:1fr;grid-template-rows:auto auto;
    height:auto;min-height:0}
  .cm-map{height:clamp(230px,30vh,300px)}
  /* The laptop takes over here and the words go under it, which is the shape a
     narrow panel actually has room for. */
  .cm-side{flex-direction:column;align-items:stretch;gap:12px;padding:14px;
    border-left:0;border-top:1px solid var(--line)}
  .cm-duo{display:none}
  .cm-lap{display:block;max-width:min(320px,86%)}
}
@media(max-width:719px){
  /* margin:0 8px used to widen the stage past the gutters on a phone. It now
     keeps the .wrap's 22px on both sides at every width (Tim, 17 Sep 2026). */
  .cm-stage{--stage-r:18px}
  /* THE NOTE IS NOT ON THE MAP ON A PHONE. It was a 176px card on a 346px map
     with six client dots underneath it. clientmap.js makes it a child of the
     stage so it can leave the map's overflow; here it drops out of the flow
     entirely and reads as the line of fine print it always was. No card, no
     glass, no rim: it is sitting on the page now, not on the map. */
  .cm-note{position:static;max-width:none;padding:12px 16px 16px;
    background:none;border:0;box-shadow:none;
    /* and the card's radius goes with the card. It is fine print on a flat
       ground now, with nothing to round, and a leftover 6px in the stage's
       bottom corner is a corner that is not concentric with anything. */
    border-radius:0;
    -webkit-backdrop-filter:none;backdrop-filter:none}
  .cm-note::before,.cm-note::after{display:none}
  .cm-note-t{font-size:.76rem}
  .cm-note span{display:block;font-size:.66rem}
  .cm-ctl{--ctl-gap:9px;top:8px;right:8px;flex-direction:row;gap:5px}
  .cm-ctl button{width:34px;height:34px}
  /* A long business name cannot be a single nowrap line on a 364px map. */
  .cm-tag{max-width:calc(100% - 24px);white-space:normal}
  .cm-tag b{font-size:.78rem}
  .cm-cap-n{font-size:.92rem}
  .cm-cap-q .cl-quote{font-size:.72rem}
}
/* Under 420 there is no room for a phone beside a paragraph, so the phone goes
   big and centred with the words under it. Tim, 15 Sep 2026: on a phone this
   has to be easy to see, and a 150px device with a website in it is not. */
@media(max-width:419px){
  .cm-lap{max-width:100%}
  .cm-cap-n{font-size:.94rem}
  .cm-cap-q .cl-quote{font-size:.7rem}
}
@media(prefers-reduced-motion:reduce){
  .cm-halo{opacity:.3}
  .cm-pin.on .cm-halo{opacity:.5}
  .cm-duo-page{animation:none}
}

/* ============================================================
   /reviews/  (9 Sep 2026)
   The review record page. Built by scripts/build-reviews.mjs from
   data/google-reviews.json, so nothing here is hand placed on the page.

   The wall is a MASONRY COLUMN layout, not a grid. Reviews run from four
   words to a full paragraph, and a row based grid stretches every card in
   a row to the height of the longest one, which leaves acres of empty
   panel beside the short ones. CSS columns let each card be its own
   height. The tradeoff is reading order goes down a column rather than
   across, which for a wall of independent quotes costs nothing.
   ============================================================ */

/* The hero: the claim on the left, the scorecard on the right. */
.rv-hero{display:grid;grid-template-columns:1.25fr .75fr;gap:44px;align-items:center}
@media(max-width:900px){.rv-hero{grid-template-columns:1fr;gap:30px}}

.rv-score{padding:28px 26px;text-align:center}
.rv-num{font-size:3.4rem;line-height:1;font-weight:800;letter-spacing:-.02em}
.rv-score .stars{color:#ffc63d;letter-spacing:.16em;font-size:1.05rem;margin-top:10px}
.rv-count{margin:12px 0 4px;font-weight:700;font-size:.95rem}
.rv-meta{margin:0;font-size:.76rem;color:var(--ink-mute);display:flex;align-items:center;
  justify-content:center;gap:7px}
.rv-meta .g-logo{width:14px;height:14px;flex:0 0 14px}

/* The distribution. Every bar but the top one is empty, which is the point:
   it is a shape a reader recognises instantly and cannot be faked into. */
.rv-bars{list-style:none;margin:20px 0 0;padding:16px 0 0;border-top:1px solid var(--line);
  display:grid;gap:7px}
.rv-bars li{display:grid;grid-template-columns:14px 1fr 26px;align-items:center;gap:10px;
  font-size:.72rem;color:var(--ink-mute)}
.rv-bars i{display:block;height:6px;border-radius:99px;background:rgba(var(--wa),.07);overflow:hidden}
.rv-bars b{display:block;height:100%;border-radius:99px;background:#ffc63d}
.rv-bars em{font-style:normal;text-align:right}

/* The featured review beside the laptop. Wider than an .mbp-quote card and
   set on the band's own dark ground rather than floating on white. */
.rv-feat{margin:26px 0 0;padding:20px 22px;border:1px solid var(--line);border-radius:var(--r-sm);
  background:var(--panel);max-width:480px}
.rv-feat .stars{color:#ffc63d;letter-spacing:.14em;font-size:.85rem;margin:12px 0 8px}
.rv-feat blockquote{margin:0;font-size:.95rem;line-height:1.62;color:var(--ink)}

/* The card head: avatar, name and when, with the Google mark held right. */
.rw-head{display:flex;align-items:center;gap:11px}
.rw-head .g-logo{width:16px;height:16px;flex:0 0 16px;margin-left:auto}
.rw-av{width:44px;height:44px;flex:0 0 44px;border-radius:50%;display:grid;place-items:center;
  font-size:.86rem;font-weight:700;letter-spacing:.02em;color:#fff;
  background:hsl(var(--h,150) 42% 34%);border:1px solid rgba(var(--wa),.14)}
.rw-av-img{background:#111;overflow:hidden;padding:6px}
.rw-av-img img{width:100%;height:100%;object-fit:contain;display:block}
.rw-who{display:grid;line-height:1.3;min-width:0}
.rw-who b{font-size:.9rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rw-who span{font-size:.74rem;color:var(--ink-mute)}

/* The wall itself. */
.rw-grid{column-count:3;column-gap:18px}
@media(max-width:1000px){.rw-grid{column-count:2}}
@media(max-width:640px){.rw-grid{column-count:1}}
.rw-card{break-inside:avoid;margin:0 0 18px;padding:20px 20px 18px;display:block}
.rw-card .stars{color:#ffc63d;letter-spacing:.13em;font-size:.82rem;margin:13px 0 9px}
.rw-card p{margin:0;font-size:.92rem;line-height:1.62;color:var(--ink-soft)}

/* The photo strip. Scrolls sideways INSIDE ITSELF on a phone, which is the
   only sideways scroll allowed on a Scalus page: the body never moves. */
.rp-strip{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:30px}
@media(max-width:900px){
  .rp-strip{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;
    -webkit-overflow-scrolling:touch;gap:12px;padding-bottom:8px}
  .rp-shot{flex:0 0 78%;scroll-snap-align:center}
}
.rp-shot{margin:0;border-radius:var(--r-sm);overflow:hidden;border:1px solid var(--line);
  background:var(--bg-2)}
.rp-shot img{display:block;width:100%;height:auto;aspect-ratio:16/9;object-fit:cover}

/* ===========================================================================
   PHONE POLISH AND THE SITE VIEWER (Tim, 25 Sep 2026)
   =========================================================================== */

/* THE CASE STUDY PROSE RAN TO THE GLASS. .cs-prose is a 720px column placed
   straight inside a <section>, with no .wrap around it, so on a phone the brief
   and every paragraph after it sat at 0px from the screen edge. It carries the
   same 22px gutter as .wrap now, and the column widens by the same 44px so the
   desktop measure is unchanged. */
section > .cs-prose{max-width:764px;padding:0 22px}

/* FORM TEXT IN DARK MODE (Tim: "on these forms on dark mode have the text
   whiter", "cant really see it"). The placeholders were --ink-mute, a mid grey
   that all but vanished on the glass panels. Dark mode only: light mode keeps
   its own inks. */
:root:not([data-theme="light"]) .form input,
:root:not([data-theme="light"]) .form select,
:root:not([data-theme="light"]) .form textarea,
:root:not([data-theme="light"]) .hq input[type=text],
:root:not([data-theme="light"]) .hq input[type=tel],
:root:not([data-theme="light"]) .hq input[type=email]{color:#fff;background:rgba(255,255,255,.1);border-color:rgba(255,255,255,.22)}
:root:not([data-theme="light"]) .form ::placeholder,
:root:not([data-theme="light"]) .hq ::placeholder,
[data-theme="light"] .hero:has(.hero-wall) .form ::placeholder{color:rgba(255,255,255,.78);opacity:1}
/* The hero form is a dark island in light mode too, so it takes the dark mode
   placeholder above (Tim, 7 Oct 2026: "make sure you can see, make the text
   lighter"); light mode's grey ink nearly vanished on the glass. */
:root:not([data-theme="light"]) .form label,
:root:not([data-theme="light"]) .form .chk,
:root:not([data-theme="light"]) .form [class*="nosite"]{color:#fff}

/* THREE CLIENT FILMS. Josh, Michael and Idah, side by side on a wide screen and
   stacked on a phone, the grid /info has used since 24 Sep 2026. */
.gv-trio{display:grid;grid-template-columns:repeat(3,1fr);gap:30px;max-width:1080px;margin:0 auto}
@media(max-width:900px){.gv-trio{grid-template-columns:1fr;gap:44px;max-width:340px}}

/* Heroes that reorder their column on a phone (/1 and /2 flatten it with
   display:contents and pull the h1 and the form forward with order) must never
   push the mark down below them: it is always first. No effect where the
   column is an ordinary block. */
.hero-grid .hero-brand{order:-9;align-self:flex-start}
/* A centred hero carries the mark centred, not pulled back for a left edge. */
.hero-brand.hero-brand-c{margin-left:auto;margin-right:auto}

/* ---- the site viewer (sc.js, window.scSiteOpen) --------------------------- */
html.sv-lock,html.sv-lock body{overflow:hidden}
.sv{position:fixed;inset:0;z-index:1200;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;
  padding:18px 16px 16px;background:rgba(4,7,6,.8);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);
  opacity:0;visibility:hidden;transition:opacity .3s cubic-bezier(.22,1,.36,1),visibility 0s linear .3s}
.sv.open{opacity:1;visibility:visible;transition:opacity .3s cubic-bezier(.22,1,.36,1)}
.sv-sheet{position:relative;width:min(1180px,100%);flex:1 1 auto;min-height:0;display:flex;flex-direction:column;
  border-radius:20px;overflow:hidden;background:#0b0f0d;border:1px solid rgba(255,255,255,.12);
  box-shadow:0 30px 90px rgba(0,0,0,.6);transform:translateY(18px) scale(.985);
  transition:transform .45s cubic-bezier(.22,1,.36,1)}
.sv.open .sv-sheet{transform:none}
.sv-bar{flex:none;display:flex;align-items:center;gap:10px;height:54px;padding:0 7px 0 16px;background:#151b18;
  border-bottom:1px solid rgba(255,255,255,.08);color:#fff}
.sv-dots{display:flex;gap:7px;flex:none}
.sv-dots i{width:11px;height:11px;border-radius:50%;background:#ff5f57}
.sv-dots i:nth-child(2){background:#febc2e}
.sv-dots i:nth-child(3){background:#28c840}
.sv-url{flex:1;min-width:0;display:flex;align-items:center;justify-content:center;gap:7px;height:34px;padding:0 12px;
  border-radius:10px;background:rgba(255,255,255,.07);font-size:.84rem;color:rgba(255,255,255,.88)}
.sv-url svg{flex:none;width:13px;height:13px;fill:none;stroke:#32cf75;stroke-width:2.2;stroke-linecap:round}
.sv-url b{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sv-x{flex:none;width:40px;height:40px;border-radius:12px;display:grid;place-items:center;background:#fff;color:#0b0f0d;
  border:0;cursor:pointer;transition:transform .2s cubic-bezier(.22,1,.36,1),background .2s ease}
.sv-x:hover{transform:scale(1.06)}
.sv-x:focus-visible{outline:2px solid #32cf75;outline-offset:2px}
.sv-sheet:focus{outline:none}
.sv-x svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2.6;stroke-linecap:round}
.sv-body{position:relative;flex:1 1 auto;min-height:0;background:#0b0f0d var(--sv-ph,none) top center/100% auto no-repeat}
.sv-body iframe{position:absolute;inset:0;width:100%;height:100%;border:0;background:transparent;opacity:0;
  transition:opacity .45s cubic-bezier(.22,1,.36,1)}
.sv-body.is-ready iframe{opacity:1}
/* the loading pill over the capture, gone the moment the live site is up */
.sv-body::after{content:"Loading the live site";position:absolute;left:50%;bottom:18px;transform:translateX(-50%);
  padding:8px 14px;border-radius:999px;background:rgba(0,0,0,.72);color:#fff;font-size:.78rem;font-weight:600;
  white-space:nowrap;pointer-events:none;transition:opacity .3s ease;animation:svPulse 1.6s ease-in-out infinite}
.sv-body.is-ready::after,.sv-body.sv-still::after{opacity:0;animation:none}
@keyframes svPulse{50%{opacity:.55}}
.sv-body.sv-still{overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;background:#0b0f0d}
.sv-body.sv-still img{display:block;width:100%;height:auto}
.sv-foot{flex:none;display:flex;justify-content:center}
.sv-enq{min-width:132px;box-shadow:0 10px 30px rgba(50,207,117,.25)}
@media(max-width:640px){
  .sv{padding:12px 10px 12px;gap:10px}
  .sv-sheet{border-radius:16px}
  .sv-dots{display:none}
  .sv-bar{padding-left:10px;height:52px}
}
/* The X pulses and glows so nobody feels trapped in the preview (Tim, 25 Sep 2026). */
.sv-x{animation:svXGlow 1.8s ease-in-out infinite}
@keyframes svXGlow{
  0%{box-shadow:0 0 0 0 rgba(50,207,117,.75),0 0 12px rgba(50,207,117,.45)}
  70%{box-shadow:0 0 0 11px rgba(50,207,117,0),0 0 22px rgba(50,207,117,.6)}
  100%{box-shadow:0 0 0 0 rgba(50,207,117,0),0 0 12px rgba(50,207,117,.45)}}
/* No scrollbar down the right. The live site's own bar lives inside the
   cross origin frame and cannot be styled, so the frame runs 18px wider than
   the sheet and the sheet's overflow:hidden clips the bar off. The still
   capture scrolls in our own box, so its bar is simply hidden. */
.sv-body{overflow:hidden}
.sv-body iframe{width:calc(100% + 18px)}
.sv-body.sv-still{scrollbar-width:none}
.sv-body.sv-still::-webkit-scrollbar{display:none}
/* Desktop: a window over the page, not the whole screen. */
@media(min-width:900px){
  .sv{padding:32px}
  .sv-sheet{width:min(1040px,100%);flex:0 1 auto;height:min(760px,calc(100vh - 150px))}
}
@media(prefers-reduced-motion:reduce){.sv,.sv-sheet,.sv-body iframe{transition:none}.sv-x{animation:none;box-shadow:0 0 0 3px rgba(50,207,117,.55),0 0 16px rgba(50,207,117,.5)}.sv-sheet{transform:none}}
/* Everything that opens the viewer says so on hover. */
.swf-ph[data-site],.vs-site{cursor:pointer}

/* ---- /info: the client sites in landscape under the ticker ---------------
   A second, slower row under the names. The gap lives on the ITEM, never on
   the track, so translateX(-50%) lands exactly one set along and the loop has
   no seam. Hover holds it still on a desktop; a tap opens the live site. */
.vs-sites{max-width:1100px;margin:-12px auto 26px;overflow:hidden;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 5%,#000 95%,transparent);
          mask-image:linear-gradient(90deg,transparent,#000 5%,#000 95%,transparent)}
.vs-sites:not(:has(.vs-site)){display:none}
.vs-sites-track{display:flex;width:max-content;animation:mq var(--vs-dur,120s) linear infinite}
.vs-site{flex:none;display:flex;flex-direction:column;gap:8px;width:236px;margin:0 14px 0 0;padding:0;border:0;background:none;
  font:inherit;color:inherit;text-align:left}
.vs-site-img{display:block;aspect-ratio:16/10;border-radius:12px;overflow:hidden;background:#0b0f0d;
  border:1px solid rgba(var(--wa),.14);box-shadow:0 12px 30px rgba(0,0,0,.28);
  transition:transform .45s cubic-bezier(.22,1,.36,1),box-shadow .45s cubic-bezier(.22,1,.36,1)}
.vs-site-img img{display:block;width:100%;height:100%;object-fit:cover;object-position:top center}
.vs-site:hover .vs-site-img,.vs-site:focus-visible .vs-site-img{transform:translateY(-3px);box-shadow:0 18px 40px rgba(0,0,0,.36)}
.vs-site:focus-visible{outline:none}
.vs-site:focus-visible .vs-site-img{outline:2px solid #32cf75;outline-offset:2px}
.vs-site-n{font-size:.8rem;font-weight:700;color:var(--ink-soft);padding-left:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.vs-live-i[data-site]{cursor:pointer}
@media(max-width:760px){.vs-sites{margin:-10px -4px 24px}.vs-site{width:196px;margin-right:12px}}
@media(prefers-reduced-motion:reduce){.vs-sites{overflow-x:auto}.vs-sites-track{animation:none}}
/* The demo call screen on /info, over the booking panel. */
.vs-dmo{max-width:640px;margin:0 auto 44px}
@media(max-width:560px){.vs-dmo{margin-bottom:32px}}

/* ---- AFTER HOURS (Tim, 25 Sep 2026: "remove this example section and replace
   it with icons and animations, just make it simple"). Two moments and one
   line. The icons move on the compositor only, transform and opacity. ------ */
.ah-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:22px;max-width:880px;margin:0 auto}
.ah-card{position:relative;padding:30px 28px 28px;border-radius:var(--r);background:var(--bg-2);border:1px solid var(--line);
  text-align:left}
.ah-card h3{margin:14px 0 8px;font-size:1.22rem}
.ah-card p{margin:0;color:var(--ink-soft);line-height:1.65}
.ah-top{display:flex;align-items:center;justify-content:space-between;gap:12px}
.ah-time{font-size:.74rem;font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:var(--green);
  padding:6px 11px;border-radius:999px;background:rgba(50,207,117,.1);border:1px solid rgba(50,207,117,.25)}
.ah-ico{position:relative;width:64px;height:64px;border-radius:20px;display:grid;place-items:center;
  background:linear-gradient(145deg,rgba(50,207,117,.22),rgba(50,207,117,.06));border:1px solid rgba(50,207,117,.3);color:#32cf75}
.ah-ico svg{position:relative;z-index:1;width:30px;height:30px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
/* the call: the handset rings in short bursts and two rings spread off it */
.ah-call svg{animation:ahRing 3.2s cubic-bezier(.36,.07,.19,.97) infinite;transform-origin:50% 60%}
.ah-call::before,.ah-call::after{content:"";position:absolute;inset:0;border-radius:inherit;border:2px solid rgba(50,207,117,.55);
  opacity:0;animation:ahWave 3.2s cubic-bezier(.22,1,.36,1) infinite}
.ah-call::after{animation-delay:.35s}
@keyframes ahRing{0%,52%,100%{transform:rotate(0)}
  4%,12%,20%,28%{transform:rotate(-13deg)}8%,16%,24%,32%{transform:rotate(13deg)}36%{transform:rotate(0)}}
@keyframes ahWave{0%{opacity:.9;transform:scale(1)}45%,100%{opacity:0;transform:scale(1.55)}}
/* the text: three dots type, then the reply lands */
.ah-dots{position:absolute;right:-10px;top:-10px;z-index:2;display:flex;gap:3px;padding:6px 8px;border-radius:12px 12px 12px 4px;
  background:#32cf75}
.ah-dots i{width:5px;height:5px;border-radius:50%;background:#06120b;animation:ahType 1.3s ease-in-out infinite}
.ah-dots i:nth-child(2){animation-delay:.16s}
.ah-dots i:nth-child(3){animation-delay:.32s}
@keyframes ahType{0%,60%,100%{transform:translateY(0);opacity:.45}30%{transform:translateY(-3px);opacity:1}}
.ah-sms svg{animation:ahBob 3.2s cubic-bezier(.45,0,.55,1) infinite}
@keyframes ahBob{50%{transform:translateY(-3px)}}
/* Orange = the busy state, green = clear and productive (Tim, 1 Oct 2026). */
.ah-busy .ah-time{color:var(--orange);background:rgba(255,77,0,.1);border-color:rgba(255,77,0,.3)}
.ah-busy .ah-ico{background:linear-gradient(145deg,rgba(255,77,0,.24),rgba(255,77,0,.06));border-color:rgba(255,77,0,.35);color:var(--orange)}
.ah-busy .ah-call::before,.ah-busy .ah-call::after{border-color:rgba(255,77,0,.6)}
.ah-line{max-width:720px;margin:34px auto 0;text-align:center;font-size:clamp(1.25rem,1rem + 1.2vw,1.7rem);
  font-weight:800;letter-spacing:-.015em;line-height:1.3;color:var(--ink)}
@media(max-width:700px){
  .ah-grid{grid-template-columns:1fr;gap:16px}
  .ah-card{padding:24px 22px 22px}
  .ah-line{margin-top:26px}
}
@media(prefers-reduced-motion:reduce){
  .ah-call svg,.ah-call::before,.ah-call::after,.ah-dots i,.ah-sms svg{animation:none}
}

/* ---- SMOOTHER MOTION, SITE WIDE (Tim, 25 Sep 2026: "make all the animations
   across the site super smooth and smart"). The scroll reveals ran on the
   browser's default ease, which starts slow and brakes late, over 26px. They
   now use the same long out curve as the .mv cascade, travel less and never
   hold a composited layer once they have landed. ------------------------- */
.js-anim .reveal{transform:translateY(18px);
  transition:opacity .9s cubic-bezier(.22,1,.36,1),transform .9s cubic-bezier(.22,1,.36,1)}
.js-anim .reveal.in{transform:none}
.js-anim .stagger > *{transform:translateY(16px);
  transition:opacity .85s cubic-bezier(.22,1,.36,1),transform .85s cubic-bezier(.22,1,.36,1)}
.js-anim .stagger.in > *{transform:none}
/* The work card hover lift and zoom on the same curve. */
.work-card,.work-card .shot > img{transition-timing-function:cubic-bezier(.22,1,.36,1)}
/* ONE LINEAR GLIDE down each work card (sc.js): no ease at either end, so the
   page moves like somebody reading it at a steady pace, never jerking to a stop. */
.work-card .shot .shot-pan img{transition-timing-function:linear}

/* ===========================================================================
   APPEARANCE — LIGHT MODE  (Tim, 13 Sep 2026)

   The site follows the device (theme.js resolves it before first paint and
   stamps <html data-theme="light|dark">). Everything above this line is the
   dark design, untouched. Everything below is what changes when the attribute
   says light, and nothing else. No attribute at all means dark, so a page that
   never loads theme.js looks exactly as it always did.

   How it stays small:
     · every white-at-an-alpha in the file is rgba(var(--wa),a). In dark --wa is
       white, in light it is the ink colour, so glass fills, hairlines and
       inset highlights invert on their own;
     · the tokens (--bg, --ink, --panel, --line...) flip here once;
     · the "dark islands" (phone mockups, laptop screens, the reel, the video
       frames, the client map stage) reset the tokens back to dark locally, so a
       screen still looks like a screen on a light page, the way it does on
       apple.com;
     · the rest is a short list of things the tokens could not reach: the
       brand logo, the gradient headline colours (the bright mint has no
       contrast on white), the frosted nav and dock, shadows that were sized
       for near black.

   Palette follows the Apple light system: #f5f5f7 ground, white cards, #1d1d1f
   ink, with the Scalus green pulled down to #12a155 for TEXT only. Solid green
   buttons keep the brand #32cf75 with dark ink, which reads in both.
   =========================================================================== */
:root[data-theme="light"]{
  --bg:#f5f5f7; --bg-2:#ffffff; --panel:#ffffff;
  --line:rgba(0,0,0,.08); --line-2:rgba(0,0,0,.14);
  --ink:#1d1d1f; --ink-soft:#4a524e; --ink-mute:#6b736e;
  --green:#12a155; --green-soft:rgba(18,161,85,.12);
  --wa:29,29,31; --blur:blur(26px) saturate(1.6);
  --glass-sm:blur(14px) saturate(170%) brightness(1.05);
  --glass-hi:rgba(255,255,255,.95); --glass-sheen:rgba(255,255,255,.55);
}
html.sc-theming,html.sc-theming *{transition:background-color .35s ease,color .35s ease,border-color .35s ease,box-shadow .35s ease,fill .35s ease,stroke .35s ease !important}

/* dark islands: screens stay screens.
   The second row is the mock furniture on the landing and update pages, found
   on 13 Sep 2026 by walking every page in light mode and flagging any block
   with a dark background whose --wa was still the light ink: a browser frame,
   a WhatsApp thread, a phone screen, a video frame, a sticky call bar, the
   OPEN and LIVE PREVIEW pills, the fake search results and the floating chips.
   Each one was drawing dark text on its own dark ground. */
/* The hero wall is a dark band in light mode, so the hero is a dark ISLAND and
   joins the list below (Tim, 19 Sep 2026: "make the form liquid glass
   transparent as well"). That one line is what turns the hero's lead form from
   a solid white card into the same liquid glass it is in dark mode: the card,
   its fields, the score pill and every hairline inside it read the dark tokens
   and invert on their own, instead of each needing its own light mode override.
   Scoped to a hero that actually CARRIES the wall, so a plain light hero on any
   other page is untouched. */
[data-theme="light"] :is(.hero:has(.hero-wall),.reel,.dmo,.phone,.mbp,.mbp-screen,.vsl-frame,.gv-frame,.ui,.ua-laptop,.rw-av-img,.sec-dark,[data-dark],
  .cs-frame,.cs-bar,.cs-open,.wa-phone,.cta-bar-inner,.tv-frame,.tv-sound,.lv,
  .ph-screen,.ph-hint,.fg-byrow,.fg-pg,.lp,.orb,.chip.c1,.chip.c2,.chip.c3,.toast,
  .itp-shot .tag,.lg-shot .tag,.ts-shot .tag){
  --bg:#070808; --bg-2:#0c0e0d; --panel:rgba(255,255,255,.045);
  --line:rgba(255,255,255,.09); --line-2:rgba(255,255,255,.16);
  --ink:#f5f7f6; --ink-soft:#b8c0bc; --ink-mute:#79827d;
  --green:#32cf75; --green-soft:rgba(50,207,117,.14);
  /* The specular tokens belong in the reset too. A chip on a dark island in
     light mode was taking the light mode highlight, which is a near solid
     white, and wearing it as a hot 1px rim on a dark pill. On an island the
     rim has to behave exactly as it does in dark mode. */
  --glass-hi:rgba(255,255,255,.26); --glass-sheen:rgba(255,255,255,.12);
  --wa:255,255,255; color-scheme:dark; color:var(--ink)}

/* brand */
[data-theme="light"] img[src$="scalus-long-white.png"]:not(footer.site img){content:url("/assets/img/scalus/scalus-long-dark.png")}
/* THE FOOTER IS A DARK BAND IN LIGHT MODE. Tim, 13 Sep 2026, pointing at the
   green wash under the page: "take away the green gradient, the footer can be
   dark in light mode thats ok". It resets the tokens to dark the same way the
   phone and laptop screens do, so every link, pill and line inside it inverts
   on its own, and the wordmark stays the white one. */
[data-theme="light"] footer.site{
  --bg:#070808; --bg-2:#0c0e0d; --panel:rgba(255,255,255,.045);
  --line:rgba(255,255,255,.09); --line-2:rgba(255,255,255,.16);
  --ink:#f5f7f6; --ink-soft:#b8c0bc; --ink-mute:#79827d;
  --green:#32cf75; --green-soft:rgba(50,207,117,.14);
  --wa:255,255,255; color-scheme:dark; color:var(--ink);
  background:#070808}
[data-theme="light"] footer.site > .wrap > a:first-child img{filter:none}
[data-theme="light"] ::selection{background:#32cf75;color:#04130a}
[data-theme="light"] body{background:var(--bg);color:var(--ink)}
/* Tim, 13 Sep 2026: light is a plain grey white ground. No green glow, no
   blurred orbs, no animated node network, no tinted section bands. */
[data-theme="light"] .glow,[data-theme="light"] .orbs,[data-theme="light"] canvas.net-bg{display:none}
/* The film's drifting colour sky is the same kind of thing and goes with them
   (19 Sep 2026). It exists because the film's black is never flat black; a
   light page has no black to lift, so all three blooms did was wash the grey
   white ground lilac. Everything else the film does, the words assembling, the
   conic rims, the typing field and the roll, reads perfectly well on light and
   stays. */
[data-theme="light"] body[data-film] .film-stage{display:none}
[data-theme="light"] section.alt{background:none}

/* headline colour: the bright mint gradient has no contrast on white */
[data-theme="light"] .h1-flow{background-image:linear-gradient(100deg,#0f9d63,#12a155,#0b7f8f,#12a155,#0f9d63)}
[data-theme="light"] .grad-flow,[data-theme="light"] .h1-shine{
  background-image:
    linear-gradient(100deg,transparent 40%,rgba(255,255,255,.55) 50%,transparent 60%),
    linear-gradient(100deg,#0f9d63,#0b8f8a,#1a7fd0,#6b4fd8,#0f9d63);
  filter:drop-shadow(0 2px 14px rgba(11,127,143,.16))}
[data-theme="light"] .glass-phrase>span,[data-theme="light"] .pot-mark::before{
  background-image:
    linear-gradient(100deg,transparent 42%,rgba(255,255,255,.6) 50%,transparent 58%),
    linear-gradient(100deg,#0f9d63,#0b8f8a,#1a7fd0,#12a155,#0f9d63)}
[data-theme="light"] .sub-rot{background-image:linear-gradient(100deg,#0f9d63 0%,#12a155 30%,#3fc98a 50%,#12a155 70%,#0f9d63 100%)}
[data-theme="light"] :is(.grad,.grad-flow + .line,.fr-note-white,.sc-dock-i.on,.btn-flame:hover,.sc-x){color:var(--ink)}

/* buttons keep the brand green */
[data-theme="light"] .btn-green,[data-theme="light"] .nav .links a.btn-green{background:#32cf75;color:#04130a;
  box-shadow:0 6px 18px rgba(50,207,117,.30),0 0 0 1px rgba(20,120,70,.25) inset}
[data-theme="light"] .btn-green:hover{background:#2bc46c}
[data-theme="light"] .btn-ghost{background:#fff;border-color:var(--line-2)}
[data-theme="light"] .btn-ghost:hover{background:#f0f0f2}
[data-theme="light"] .btn-flame{border-color:var(--line-2);
  background:linear-gradient(100deg,transparent 24%,rgba(255,255,255,.9) 47%,transparent 70%),linear-gradient(180deg,#ffffff,#f3f3f5);
  box-shadow:inset 0 1px 0 #fff,0 6px 18px rgba(0,0,0,.08)}
[data-theme="light"] .btn-flame:hover{border-color:rgba(0,0,0,.2);box-shadow:inset 0 1px 0 #fff,0 10px 26px rgba(0,0,0,.12)}
[data-theme="light"] .chip{border-color:rgba(18,161,85,.35)}

/* panels and cards: white on the #f5f5f7 ground, soft shadows */
[data-theme="light"] .panel{box-shadow:0 14px 40px -18px rgba(0,0,0,.14),0 1px 2px rgba(0,0,0,.04),inset 0 1px 0 #fff}
[data-theme="light"] .panel:hover{border-color:var(--line-2)}
[data-theme="light"] :is(.work-card,.post-card):hover{box-shadow:0 22px 50px -20px rgba(0,0,0,.18)}
[data-theme="light"] .pcard:hover{box-shadow:0 30px 70px -30px rgba(0,0,0,.22)}
[data-theme="light"] .faq details{background:#fff}
[data-theme="light"] .form input,[data-theme="light"] .form select,[data-theme="light"] .form textarea{background:#fff;border-color:rgba(0,0,0,.16)}
[data-theme="light"] .form input:focus,[data-theme="light"] .form select:focus,[data-theme="light"] .form textarea:focus{border-color:#12a155;box-shadow:0 0 0 3px rgba(18,161,85,.18)}
[data-theme="light"] .sh-card,[data-theme="light"] .mp-row{background:#fff}
[data-theme="light"] .sec-light{background:#fff}
[data-theme="light"] .hq-panel:not(.hero .hq-panel){background:#fff}
/* THE TWO THAT WALK THROUGH THE ISLAND. Flipping the tokens on the hero covers
   everything drawn FROM a token, but a light mode rule that hardcodes #fff is
   not a token and lands anyway. Inside the hero that produced a white field
   with white text in it, which is a form nobody can type into, so these two put
   the glass back. If a third one ever appears, the way to find it is the scan
   that found these: read every computed background inside .hero in light mode
   and look for a luminance over 170. */
[data-theme="light"] .hero:has(.hero-wall) :is(.form input,.form select,.form textarea){
  background:rgba(255,255,255,.08);border-color:var(--line-2);color:var(--ink)}
[data-theme="light"] .hero:has(.hero-wall) :is(.form input,.form select,.form textarea):focus{
  border-color:var(--green);box-shadow:none}
/* The Google scorecard is NOT in this list on purpose: it is deliberately kept
   as a solid white pill on the dark hero, with its own dark ink. See the rule
   that says so near the end of this file. */
[data-theme="light"] .ua-site{background:#fff;border-color:var(--line-2)}
/* The chip hangs off the laptop, which stays dark in the light theme, so it
   would inherit white words on a light page. */
[data-theme="light"] .ua-laptop .ua-chip-build{color:#0b0f0d;background:rgba(255,255,255,.82);border-color:rgba(0,0,0,.1);
  box-shadow:0 10px 24px -12px rgba(0,0,0,.25)}
[data-theme="light"] .ua-applied .ua-chip{background:rgba(255,255,255,.7);border-color:rgba(0,0,0,.08);box-shadow:0 10px 24px -14px rgba(0,0,0,.2)}
[data-theme="light"] .gq-score{background:#fff;border-color:var(--line-2);box-shadow:0 10px 30px -14px rgba(0,0,0,.14)}

/* the frosted top bar */
[data-theme="light"] .nav .bar{
  background:linear-gradient(180deg,rgba(255,255,255,.82),rgba(255,255,255,.62));
  border-color:rgba(0,0,0,.06);
  -webkit-backdrop-filter:blur(26px) saturate(180%);backdrop-filter:blur(26px) saturate(180%);
  box-shadow:0 12px 34px -14px rgba(0,0,0,.18),0 1px 3px rgba(0,0,0,.05),inset 0 1px 0 #fff}
[data-theme="light"] .nav.scrolled .bar{background:linear-gradient(180deg,rgba(255,255,255,.9),rgba(255,255,255,.74))}
[data-theme="light"] .nav .bar::before{background:conic-gradient(from var(--sc-rim),
    rgba(0,0,0,.14),rgba(0,0,0,.02) 18%,rgba(0,0,0,.06) 38%,rgba(0,0,0,.02) 55%,
    rgba(0,0,0,.12) 76%,rgba(0,0,0,.03) 90%,rgba(0,0,0,.14))}
[data-theme="light"] .nav .bar::after{background:radial-gradient(240px 84px at var(--sc-nav) -18%,
    rgba(255,255,255,.9),rgba(255,255,255,.3) 45%,rgba(255,255,255,0) 72%)}
/* more vibrant on the light bar (Tim, 6 Oct 2026: "make SEO check more
   vibrant"): the spectrum is laid in near full strength, the rim runs at full
   colour, and a soft blue violet glow lifts it off the white glass. */
[data-theme="light"] .nav .links a.seochk{color:#0b1220;
  --glass:
    linear-gradient(105deg,rgba(255,255,255,0) 0%,rgba(255,255,255,.55) 46%,rgba(255,255,255,0) 100%),
    linear-gradient(90deg,
      rgba(46,236,132,.62) 0%,rgba(38,224,214,.58) 20%,rgba(92,168,255,.6) 40%,
      rgba(178,126,255,.6) 60%,rgba(255,132,196,.56) 80%,rgba(46,236,132,.62) 100%),
    linear-gradient(180deg,rgba(255,255,255,.35),rgba(255,255,255,.1));
  -webkit-backdrop-filter:blur(15px) saturate(180%);backdrop-filter:blur(15px) saturate(180%);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.9),inset 0 -1px 0 rgba(0,0,0,.04),
    0 4px 14px rgba(0,0,0,.08),0 0 20px rgba(120,140,255,.32)}
[data-theme="light"] .nav .links a.seochk svg{filter:none}
[data-theme="light"] .nav .links a.navcall{background:#32cf75;color:#04130a}
[data-theme="light"] .nav .links a.navcall:hover{background:#3ddc82;color:#04130a}
[data-theme="light"] .navtoggle{background:rgba(0,0,0,.04)}
@media(max-width:1000px){
  [data-theme="light"] .nav .links{background:rgba(255,255,255,.94);box-shadow:0 16px 50px -10px rgba(0,0,0,.22)}
  [data-theme="light"] .nav .links a:hover{background:rgba(0,0,0,.05)}
}

/* the sun / moon button in the bar */
.sc-theme{order:2;position:relative;z-index:3;flex:0 0 auto;width:38px;height:38px;border-radius:50%;
  display:grid;place-items:center;padding:0;cursor:pointer;font:inherit;
  border:1px solid var(--line);background:rgba(var(--wa),.06);color:var(--ink-soft);
  transition:background .2s,color .2s,border-color .2s,transform .2s}
.sc-theme:hover{color:var(--ink);background:rgba(var(--wa),.11);border-color:var(--line-2)}
.sc-theme:active{transform:scale(.92)}
.sc-theme:focus-visible{outline:2px solid var(--green);outline-offset:2px}
.sc-theme svg{grid-area:1/1;width:18px;height:18px;display:block;transition:opacity .3s,transform .45s cubic-bezier(.2,.8,.2,1)}
.sc-theme .moon{opacity:0;transform:rotate(-70deg) scale(.6)}
[data-theme="light"] .sc-theme .sun{opacity:0;transform:rotate(70deg) scale(.6)}
[data-theme="light"] .sc-theme .moon{opacity:1;transform:none}
@media(max-width:1000px){.sc-theme{order:2;width:40px;height:40px;border-radius:12px;margin-left:0}}

/* ── light mode legibility sweep, 13 Sep 2026 ──────────────────────────────
   Tim: "do a full scan of the website in light mode make sure you can see all
   text". Every page was walked at 1440 in light mode and every run of text was
   measured against the colour actually painted behind it. The dark mocks were
   fixed by adding them to the island list above; what is left here is a handful
   of literals written into individual page <style> blocks that were only ever
   read on a black page. Page styles load after this file, so each override
   carries one extra selector to win on specificity. */
[data-theme="light"] .cs-visit{color:var(--ink)}
[data-theme="light"] .cs-info .cs-cat{color:#12a155}
[data-theme="light"] .rev-score .rs,
[data-theme="light"] .rev-rating .rs,
[data-theme="light"] .cl-when{color:#12a155}
[data-theme="light"] .was.mid{color:#b35c00;text-decoration-color:rgba(179,92,0,.6)}
[data-theme="light"] .ptag{color:#5b5b5b}
/* Headline and panel words painted with the brand green literal. Light mode
   pulls green TEXT down to #12a155 (see the palette note above) and these were
   written before that existed, so they stayed at #32cf75 and washed out. */
[data-theme="light"] h1 .green,[data-theme="light"] h2 .green,[data-theme="light"] .w.green{color:#12a155}

/* the Auto / Light / Dark control in the footer */
.sc-appearance{display:inline-flex;align-items:center;gap:4px;margin:18px auto 6px;padding:4px 4px 4px 14px;
  border:1px solid var(--line);border-radius:999px;background:rgba(var(--wa),.04);font-size:.74rem}
.sc-appearance span{color:var(--ink-mute);font-weight:600;margin-right:6px;letter-spacing:.02em}
.sc-appearance button{font:inherit;font-size:.74rem;font-weight:700;cursor:pointer;padding:7px 13px;border-radius:999px;
  border:1px solid transparent;background:transparent;color:var(--ink-soft);transition:background .2s,color .2s}
.sc-appearance button:hover{color:var(--ink);background:rgba(var(--wa),.07)}
.sc-appearance button.on{background:rgba(var(--wa),.12);color:var(--ink);border-color:var(--line-2)}
[data-theme="light"] .sc-appearance{background:#fff}
[data-theme="light"] .sc-appearance button.on{background:#1d1d1f;color:#fff;border-color:#1d1d1f}
/* ...except in the footer, which is a dark band in light mode, so the control
   follows the dark tokens around it rather than sitting there as a white slab. */
[data-theme="light"] footer.site .sc-appearance{background:rgba(255,255,255,.04)}
[data-theme="light"] footer.site .sc-appearance button.on{background:rgba(255,255,255,.12);color:#f5f7f6;border-color:rgba(255,255,255,.16)}

/* the floating glass dock */
[data-theme="light"] .sc-dock-in{
  background:linear-gradient(180deg,rgba(255,255,255,.6),rgba(255,255,255,.4));
  -webkit-backdrop-filter:blur(14px) saturate(190%);backdrop-filter:blur(14px) saturate(190%);
  box-shadow:0 18px 44px -16px rgba(0,0,0,.26),0 2px 8px rgba(0,0,0,.06),inset 0 1px 0 #fff,inset 0 -1px 0 rgba(0,0,0,.03)}
[data-theme="light"] .sc-dock-lens{opacity:.35;-webkit-backdrop-filter:blur(6px) saturate(2) brightness(1.02);backdrop-filter:blur(6px) saturate(2) brightness(1.02)}
[data-theme="light"] .sc-dock-edge{-webkit-backdrop-filter:blur(.5px) saturate(2.4) brightness(.96) contrast(1.2);backdrop-filter:blur(.5px) saturate(2.4) brightness(.96) contrast(1.2)}
[data-theme="light"] .sc-dock-in::after{background:radial-gradient(130px 66px at var(--sc-x,28%) var(--sc-y,-10%),rgba(255,255,255,.95),rgba(255,255,255,.3) 45%,rgba(255,255,255,0) 72%)}
[data-theme="light"] .sc-dock-pill{background:linear-gradient(180deg,rgba(0,0,0,.07),rgba(0,0,0,.04));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.6),inset 0 -1px 0 rgba(0,0,0,.05),0 2px 8px rgba(0,0,0,.06)}
[data-theme="light"] .sc-dock-i.chat .ic::after{box-shadow:0 0 0 2px #fff,0 0 10px rgba(50,207,117,.6)}

/* floating launchers, chat window and the message sheet */
[data-theme="light"] .talk-now{background:rgba(255,255,255,.85);box-shadow:0 12px 32px -10px rgba(0,0,0,.22),0 0 0 1px rgba(0,0,0,.03)}
[data-theme="light"] .sc-chat{background:linear-gradient(180deg,rgba(255,255,255,.7),rgba(255,255,255,.4));
  -webkit-backdrop-filter:blur(16px) saturate(180%);backdrop-filter:blur(16px) saturate(180%);
  box-shadow:0 14px 36px -12px rgba(0,0,0,.24),0 0 22px rgba(50,207,117,.2),inset 0 1px 0 #fff}
[data-theme="light"] .sc-chat::before{background:conic-gradient(from var(--sc-rim),
    rgba(255,255,255,1),rgba(0,0,0,.08) 18%,rgba(50,207,117,.45) 38%,rgba(0,0,0,.05) 55%,
    rgba(255,255,255,1) 76%,rgba(0,0,0,.1) 90%,rgba(255,255,255,1))}
[data-theme="light"] .sc-chat::after{background:radial-gradient(60px 30px at 35% 0%,rgba(255,255,255,.9),rgba(255,255,255,0) 70%)}
[data-theme="light"] .sc-chat .ask{color:#0b1220;
  background:linear-gradient(180deg,rgba(255,255,255,.88),rgba(255,255,255,.7));
  -webkit-backdrop-filter:blur(16px) saturate(180%);backdrop-filter:blur(16px) saturate(180%);
  box-shadow:0 12px 30px -12px rgba(0,0,0,.25),inset 0 1px 0 #fff,0 0 0 1px rgba(0,0,0,.06)}
[data-theme="light"] .sc-chat:hover{box-shadow:0 18px 44px -12px rgba(0,0,0,.28),0 0 30px rgba(50,207,117,.3)}
/* The launcher disc stays SCALUS GREEN in light mode. Light mode darkens the
   --green token to #12a155 so green TEXT holds up on a white ground, but on a
   filled disc that reads as a muddy bottle green rather than as our brand
   colour, so the disc takes the brand value straight. Tim, 14 Sep 2026. */
[data-theme="light"] .sc-chat .ic{background:#32cf75;color:#04130a;
  box-shadow:0 0 16px rgba(50,207,117,.5)}
[data-theme="light"] .sc-cw{background:linear-gradient(180deg,rgba(255,255,255,.96),rgba(246,247,248,.97));
  box-shadow:0 30px 80px -20px rgba(0,0,0,.3),0 0 40px rgba(50,207,117,.08)}
[data-theme="light"] .sc-cw .cw-msg.bot{background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.06),0 6px 18px -8px rgba(0,0,0,.12)}
[data-theme="light"] .sc-cw .cw-in input::placeholder{color:var(--ink-mute);opacity:1}
[data-theme="light"] .sc-modal{background:rgba(245,245,247,.72)}
[data-theme="light"] .sc-sheet{background:#fff;box-shadow:0 30px 90px -20px rgba(0,0,0,.3)}
[data-theme="light"] .talk-modal .back{background:rgba(245,245,247,.6)}
[data-theme="light"] .talk-modal .box{background:#fff;box-shadow:0 30px 80px -20px rgba(0,0,0,.3)}

/* the spotlight veil sits over a client screenshot, so it takes the page ground */
[data-theme="light"] .sp-veil{background:
  linear-gradient(90deg,rgba(245,245,247,.97) 0%,rgba(245,245,247,.95) 46%,rgba(245,245,247,.9) 72%,rgba(245,245,247,.86) 100%),
  linear-gradient(180deg,rgba(245,245,247,1) 0%,rgba(245,245,247,.8) 26%,rgba(245,245,247,.82) 62%,rgba(245,245,247,1) 100%)}
/* /updates/ hero: the floating copy card goes white, the name plate stays on the photo */
[data-theme="light"] .ssu-card{background:rgba(255,255,255,.96);border:1px solid var(--line);box-shadow:0 24px 60px -24px rgba(0,0,0,.25)}
[data-theme="light"] .ssu-plate{--ink:#fff;--ink-soft:#dfe4e1;--ink-mute:#c3cac6}

/* the client map in light: a paper map, water pale blue, land off white, the
   same green dots. The relief is baked light on dark, so it multiplies in at
   low opacity rather than sitting on top (no CSS filter, tiny viewBox trap). */
[data-theme="light"] .cm-stage{background:transparent;border-color:rgba(0,0,0,.1)}
[data-theme="light"] .cm-plot{background:radial-gradient(90% 70% at 50% 0%,rgba(50,207,117,.05),transparent 60%),#d9e4ec}
[data-theme="light"] .cm-landfill{fill:#f1f3ef;stroke:rgba(70,100,130,.38)}
/* Tim, 13 Sep 2026: at .22 the multiplied hillshade read as dirty grey clouds
   over the pale land rather than as ground. Measured against .10 and .06 in
   light mode, .07 keeps the terrain as a whisper and the smudges go. */
[data-theme="light"] .cm-relief{opacity:.07;mix-blend-mode:multiply}
[data-theme="light"] .cm-back circle{fill:rgba(29,29,31,.14)}
[data-theme="light"] .cm-lab text{fill:rgba(29,29,31,.62);stroke:#f1f3ef}
[data-theme="light"] .cm-lab0 text{fill:rgba(29,29,31,.8)}
[data-theme="light"] .cm-city{fill:rgba(29,29,31,.8)}
[data-theme="light"] .cm-lab2 text{fill:rgba(29,29,31,.5)}
[data-theme="light"] .cm-dot{stroke:#fff}
[data-theme="light"] .cm-pin:focus-visible .cm-dot,
[data-theme="light"] .cm-pin:hover .cm-dot,
[data-theme="light"] .cm-pin.on .cm-dot{fill:#12a155}
[data-theme="light"] .cm-pin:focus-visible .cm-glow,
[data-theme="light"] .cm-pin:hover .cm-glow,
[data-theme="light"] .cm-pin.on .cm-glow{fill:rgba(18,161,85,.32)}
[data-theme="light"] .cm-halo{fill:rgba(18,161,85,.3)}
[data-theme="light"] .cm-tag{background:#fff;box-shadow:0 10px 30px -10px rgba(0,0,0,.25)}
[data-theme="light"] .cm-note{background:linear-gradient(180deg,rgba(255,255,255,.72),rgba(255,255,255,.5));
  box-shadow:0 14px 34px -14px rgba(0,0,0,.3),inset 0 1px 0 rgba(255,255,255,.9)}
[data-theme="light"] .cm-note::before,[data-theme="light"] .cm-ctl button::before{
  background:linear-gradient(150deg,rgba(255,255,255,.95),rgba(0,0,0,.06) 45%,rgba(255,255,255,.7))}
[data-theme="light"] .cm-note::after,[data-theme="light"] .cm-ctl button::after{
  background:radial-gradient(120% 70% at 28% -20%,rgba(255,255,255,.85),rgba(255,255,255,0) 70%)}
[data-theme="light"] .cm-ctl button{border-color:transparent;
  background:linear-gradient(180deg,rgba(255,255,255,.75),rgba(255,255,255,.52));
  box-shadow:0 10px 24px -12px rgba(0,0,0,.35),inset 0 1px 0 rgba(255,255,255,.9)}
/* THE LIGHT VEIL IS A DARK VEIL (Tim, 17 Sep 2026: "make light hero section
   have dark gradient from top to bottom to help with visibility"). It used to
   be a cool grey tint, on the theory that light mode needed less veil so the
   tiles kept reading as websites. What that actually produced was dark grey
   ink on a busy mid grey wall, which is legible on paper and muddy on a
   screen. So light mode now runs the SAME near black the dark theme does.

   Two layers, because the hero has two jobs. The 180deg one is the one Tim
   asked for and carries the readability: heaviest at the top behind the
   headline, easing as it goes down. The 97deg one underneath keeps the old
   left to right character, so the right hand side, where the form card sits
   over everything anyway, stays the lightest corner and the sites still show.
   Multiplied out that is about 87% dark at the top left and 48% at the bottom
   right. */
[data-theme="light"] .hero-wall::after{background:
    linear-gradient(180deg,rgba(7,8,8,.72) 0%,rgba(7,8,8,.62) 45%,rgba(7,8,8,.50) 78%,rgba(7,8,8,.42) 100%),
    linear-gradient(97deg,rgba(7,8,8,.55) 0%,rgba(7,8,8,.40) 40%,rgba(7,8,8,.10) 100%)}
/* The wall's mask fades the veil out with the picture, and at the stock 62%
   the dark ground vanished from under the checks, the buttons and the fine
   print. In light mode it holds to 80% and then dissolves into the page over
   the last fifth, so the hero reads as a dark band that melts into #f5f5f7
   rather than stopping at an edge. */
[data-theme="light"] .hero-wall{
  -webkit-mask-image:linear-gradient(180deg,#000 0%,#000 80%,transparent 100%);
          mask-image:linear-gradient(180deg,#000 0%,#000 80%,transparent 100%)}
[data-theme="light"] .cm-side{border-color:rgba(0,0,0,.08);
  background:linear-gradient(145deg,rgba(255,255,255,.62),rgba(255,255,255,.4) 55%,rgba(255,255,255,.55))}
[data-theme="light"] .cm-cap-c{color:#12a155}
[data-theme="light"] .cm-duo{box-shadow:0 26px 52px -22px rgba(0,0,0,.45),0 0 0 1px rgba(0,0,0,.14)}
[data-theme="light"] .cm-note{background:rgba(255,255,255,.5);border-color:rgba(0,0,0,.08);
  box-shadow:0 8px 22px rgba(20,35,50,.12),inset 0 1px 0 rgba(255,255,255,.85)}
[data-theme="light"] .cm-note span{color:var(--ink-soft)}

/* the appearance menu under the sun/moon button: System, Light, Dark */
.sc-theme-wrap{order:2;position:relative;z-index:4;flex:0 0 auto;display:flex}
.sc-theme-wrap .sc-theme{order:0}
.sc-theme-menu{position:absolute;top:calc(100% + 10px);right:0;min-width:168px;padding:6px;
  border-radius:16px;border:1px solid var(--line-2);background:rgba(12,14,13,.96);
  -webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur);
  box-shadow:0 18px 50px -14px rgba(0,0,0,.6);
  opacity:0;transform:translateY(-6px);pointer-events:none;transition:opacity .2s,transform .2s}
.sc-theme-wrap.open .sc-theme-menu{opacity:1;transform:none;pointer-events:auto}
.sc-theme-menu button{display:flex;align-items:center;gap:10px;width:100%;padding:10px 12px;border:0;border-radius:11px;
  background:transparent;color:var(--ink-soft);font:inherit;font-size:.86rem;font-weight:600;cursor:pointer;text-align:left}
.sc-theme-menu button:hover{background:rgba(var(--wa),.07);color:var(--ink)}
.sc-theme-menu button.on{color:var(--ink)}
.sc-theme-menu button svg{width:16px;height:16px;flex:none}
.sc-theme-menu button .tick{margin-left:auto;opacity:0;color:var(--green)}
.sc-theme-menu button.on .tick{opacity:1}
[data-theme="light"] .sc-theme-menu{background:rgba(255,255,255,.96);box-shadow:0 18px 50px -14px rgba(0,0,0,.25)}
@media(max-width:1000px){.sc-theme-wrap{order:2}}

/* chat window in light: white header, readable quick replies */
[data-theme="light"] .sc-cw .cw-head{background:linear-gradient(135deg,rgba(255,255,255,.9),rgba(240,247,243,.9));border-bottom-color:rgba(0,0,0,.08)}
[data-theme="light"] .sc-cw .cw-chips button{color:#0d7a42;background:rgba(18,161,85,.08);border-color:rgba(18,161,85,.4)}
[data-theme="light"] .sc-cw .cw-chips button:hover{color:#075c30;background:rgba(18,161,85,.16);border-color:rgba(18,161,85,.65)}
/* the Enquire arrow keeps the brand green in light, same as the solid buttons */
[data-theme="light"] .hq-face figcaption.hq-cred .hq-cred-arrow{background:#32cf75;box-shadow:0 0 18px rgba(50,207,117,.35),inset 0 1px 0 rgba(255,255,255,.45)}
/* the light sections were built as white bands on the dark site; on a light
   page they become plain ground so nothing steps at the section edge */
[data-theme="light"] .sec-light,
[data-theme="light"] .sec-light:not(.sec-light + .sec-light),
[data-theme="light"] .sec-light:not(:has(+ .sec-light)),
[data-theme="light"] .sec-light:not(.sec-light + .sec-light):not(:has(+ .sec-light)){background:transparent;box-shadow:none}
[data-theme="light"] .sec-light::before,[data-theme="light"] .sec-light::after,
[data-theme="light"] .sec-light + section::before{background:none;box-shadow:none}
/* SEO Check sits a touch closer to Call (Tim, 13 Sep 2026) */
@media(min-width:1001px){.nav .links a.navcall{margin-left:-8px}}
/* shadows sized for near black are cut by the next section on a light page */
[data-theme="light"] .mbp-lid{box-shadow:0 24px 50px -22px rgba(0,0,0,.38),0 2px 0 rgba(255,255,255,.4) inset,0 0 0 1px rgba(0,0,0,.08)}
[data-theme="light"] .mbp-quote{box-shadow:0 14px 34px -14px rgba(0,0,0,.2)}
/* the floating appearance control on pages with no top bar */
.sc-theme-float{position:fixed;top:14px;right:16px;z-index:80}
.sc-theme-float .sc-theme{background:rgba(var(--wa),.08);-webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur);box-shadow:0 6px 18px rgba(0,0,0,.18)}

/* page specific: the onboarding form and the 404 page carry their own dark styling */
[data-theme="light"] .onb-progress{background:rgba(255,255,255,.86)}
[data-theme="light"] .onb-hero .onb-lead,[data-theme="light"] .onb-intro-txt .tf{color:var(--ink)}
[data-theme="light"] .onb-intro{background:rgba(245,245,247,.97)}
[data-theme="light"] .nf-bg{background:#f5f5f7}
[data-theme="light"] .nf-bg::before,[data-theme="light"] .nf-bg::after{opacity:.16}

/* THE LAPTOP BAND'S OWN GROUND IN LIGHT MODE (Tim, 14 Sep 2026). Dark greens
   are the wrong instrument on a #f5f5f7 page, so the same idea is carried as a
   pale mint that is clearly not the page and clearly not a white card. */
[data-theme="light"] .mbp-band-tint{
  background:
    radial-gradient(120% 150% at 50% 0%, rgba(18,161,85,.09) 0%, rgba(18,161,85,0) 60%),
    linear-gradient(180deg, #eef5f0 0%, #e9f2ec 55%, #eef5f0 100%);
  border-top:1px solid rgba(0,0,0,.06);
  border-bottom:1px solid rgba(0,0,0,.06)}
[data-theme="light"] .sec-light + .mbp-band-tint{
  background:
    radial-gradient(120% 150% at 50% 0%, rgba(18,161,85,.09) 0%, rgba(18,161,85,0) 60%),
    linear-gradient(180deg, #eef5f0 0%, #e9f2ec 55%, #eef5f0 100%)}

/* THE HEADLINE SITS ON THE WALL, NOT BESIDE IT (Tim, 16 Sep 2026).
   The hero wall is a mid grey once the light veil is over it, and dark ink on a
   busy mid grey is legible on paper and muddy on a screen. So the H1 alone keeps
   dark mode's ink at every width: white plain words, the brand green on "Local
   Melbourne" with a glow so it lifts off the wall, and "Websites" wearing the
   bright shine instead of the darkened light mode gradient, because the effect is
   the point and it disappears into a grey ground.

   Nothing else in the column moves: the lead, the checks, the buttons and the
   fine print all sit lower, where the wall's mask has already faded it out to the
   page. Scoped to `.hero:has(.hero-wall)` so it can only ever touch a hero that
   actually has a wall behind it, which today is the homepage and nothing else. */
/* Tim, 17 Sep 2026, second pass: "remove the black background on local
   melbourne" and "add heavier dropshadow on all the hero title text". The
   plate is gone. What holds the words off the wall now is the shadow alone, on
   all three lines, built as a stack rather than one blur: a tight contact
   shadow that darkens the pixel right at the glyph edge and does the actual
   separating, a mid blur that reads as depth, and a wide low alpha one that
   pools under the whole line so the busy wall behind it goes quiet. One big
   soft shadow cannot do the first job and a hard one cannot do the third. */
/* Tim, 17 Sep 2026, third pass: "make it a bit softer, much softer". The
   contact layer is dropped, every blur widened and every alpha roughly halved,
   so the words still float off the wall but the edge no longer reads as ink. */
[data-theme="light"] .hero:has(.hero-wall) h1{color:#fff;
  text-shadow:
    0 2px 6px rgba(8,11,13,.32),
    0 6px 18px rgba(8,11,13,.26),
    0 16px 44px rgba(8,11,13,.24),
    0 30px 80px rgba(8,11,13,.2)}
/* The green line carries the same stack in a green black, which keeps the
   colour clean where the shadow meets the glyph, plus a close glow so the
   brand green still lifts the way it did on the plate. */
[data-theme="light"] .hero:has(.hero-wall) h1 .green{color:#32cf75;
  text-shadow:
    0 2px 6px rgba(3,14,8,.34),
    0 6px 18px rgba(3,14,8,.28),
    0 16px 44px rgba(3,14,8,.24),
    0 30px 80px rgba(3,14,8,.2),
    0 0 30px rgba(50,207,117,.22)}
/* "Websites" is clipped gradient text, so the inherited text-shadow paints a
   dark glyph BEHIND transparent letters and dulls the whole word. It takes its
   lift as a drop-shadow instead, which shadows the gradient itself. */
[data-theme="light"] .hero:has(.hero-wall) h1 .h1-shine{
  text-shadow:none;
  background-image:
    linear-gradient(100deg,transparent 40%,rgba(255,255,255,.92) 50%,transparent 60%),
    linear-gradient(100deg,#32cf75,#5ad1ff,#a97bff,#ff7ac2,#ffb347,#32cf75);
  filter:
    drop-shadow(0 2px 6px rgba(8,11,13,.3))
    drop-shadow(0 8px 20px rgba(8,11,13,.26))
    drop-shadow(0 20px 48px rgba(8,11,13,.22))
    drop-shadow(0 2px 18px rgba(90,209,255,.14))}

/* ---------------------------------------------------------------------------
   AND THE WHOLE COLUMN GOES WHITE WITH IT (Tim, 17 Sep 2026: "change the font
   to all white ... with the animated gradients still there").

   The headline had already been pulled out to white on its own; the lead, the
   checks and the fine print were still light mode ink, which over a near black
   wall would be unreadable. Rather than repaint them one property at a time
   this does what the footer and the phone screens do everywhere else on the
   page: it resets the theme tokens to dark for the hero's copy column, so
   every colour inside it resolves the way it does in dark mode and nothing new
   has to be remembered when a line is added to the hero later.

   Scoped to the FIRST grid child, never the second, because the second holds
   the form card and that stays a white card. On a phone .hero-grid>div is
   display:contents, which removes the box from layout but still passes the
   custom properties down, so the same one rule covers both layouts. */
[data-theme="light"] .hero:has(.hero-wall) .hero-grid > div:first-child{
  --ink:#ffffff; --ink-soft:rgba(255,255,255,.9); --ink-mute:rgba(255,255,255,.72);
  --green:#32cf75; --green-soft:rgba(50,207,117,.14);
  --line:rgba(255,255,255,.12); --line-2:rgba(255,255,255,.2);
  --glass-hi:rgba(255,255,255,.26); --glass-sheen:rgba(255,255,255,.12);
  --wa:255,255,255; color:var(--ink)}
/* The lead and the checks take a much lighter version of the headline's shadow
   stack. At 1rem a shadow sized for a 3.9rem headline reads as a smudge, so it
   is two soft layers and low alpha: enough to stop the wall showing through the
   counters, not enough to thicken the type. */
[data-theme="light"] .hero:has(.hero-wall) .hero-grid > div:first-child :is(.lead,.checks li,.hero-fine){
  text-shadow:0 1px 4px rgba(8,11,13,.4),0 6px 22px rgba(8,11,13,.3)}
/* The living gradients are the point of the hero, so they keep the BRIGHT dark
   mode bed rather than the darkened light mode one, exactly as "Websites" does
   a few rules up. Same colours, same shine and glow animations, untouched. */
[data-theme="light"] .hero:has(.hero-wall) .glass-phrase > span{
  background-image:
    linear-gradient(100deg,transparent 42%,rgba(255,255,255,.95) 50%,transparent 58%),
    linear-gradient(100deg,#32cf75,#2fe3b4,#6ee7ff,#7df0ae,#39d98a,#32cf75)}
/* The Google scorecard is a solid white pill, so it is an island of its own
   inside the island and has to go back to light ink or it would be white on
   white. It keeps the light mode shadow too, which is sized for a pale card. */
[data-theme="light"] .hero:has(.hero-wall) .gq-score{
  --ink:#1d1d1f; --ink-mute:#6b736e; --line-2:rgba(0,0,0,.14);
  background:#fff;box-shadow:0 10px 30px -14px rgba(0,0,0,.35)}
/* Same for the pale SEO Check button: white ground, so dark ink. */
[data-theme="light"] .hero:has(.hero-wall) .btn-flame{color:#1d1d1f;border-color:rgba(0,0,0,.14)}
[data-theme="light"] .hero:has(.hero-wall) .btn-flame:hover{color:#000;border-color:rgba(0,0,0,.22)}

/* ---- testimonial logos and the one-at-a-time quote (sc.js, 25 Sep 2026) ---- */
/* Every logo gets the same 58px box so the names, roles and quotes under all
   three start on the same line (Tim, 28 Sep 2026: "make sure all of these line
   up properly"). The wordmarks sit 36px tall in it, Eden's tree fills it. */
.gv-logo{display:flex;justify-content:center;align-items:center;height:58px;margin-top:18px}
.gv-logo img{height:36px;width:auto;max-width:180px;object-fit:contain}
.gv-logo:has(img[src*="eden"]) img{height:100%}
/* and a role that wraps to two lines (Idah's) no longer pushes her quote down:
   each column shares the trio's rows, so the quotes all start level */
@media(min-width:901px){
  .gv-trio{grid-template-rows:auto auto}
  .gv-trio > div{display:grid;grid-row:span 2;grid-template-rows:subgrid;align-content:start}
}
.gv-logo + .gv-tag{margin-top:10px}
.gv-logo-l{display:none}
[data-theme="light"] .gv-logo:has(.gv-logo-l) .gv-logo-d{display:none}
[data-theme="light"] .gv-logo-l{display:block}
[data-theme="light"] .gv-logo-twentytwo img{filter:brightness(.35) saturate(1.6)}
.gv-quotes.gv-rot{display:block}
.gv-quotes.gv-rot li{transition:opacity .45s ease}
.gv-quotes.gv-rot li.gv-out{opacity:0}
.gv-quotes.gv-rot li.gv-typing::after{content:"";display:inline-block;width:2px;height:1.05em;margin-left:2px;
  vertical-align:-.15em;background:var(--green);animation:gvCaret .8s steps(1) infinite}
@keyframes gvCaret{50%{opacity:0}}

/* 6 Oct 2026: the call back ask is a slim strip docked on top of the chat bar,
   the same height as it, green tinted so it reads as its own thing. */
.sc-cw .cw-call{flex:none;position:relative;z-index:2;display:grid;gap:7px;padding:10px 13px 8px;
  border-top:1px solid rgba(50,207,117,.28);
  background:linear-gradient(180deg,rgba(50,207,117,.13),rgba(50,207,117,.05));
  animation:cwIn .3s cubic-bezier(.2,.8,.2,1) both}
.sc-cw .cw-call b{font-size:.8rem;font-weight:700;line-height:1.3}
.sc-cw .cw-call .cw-row{display:flex;gap:9px}
.sc-cw .cw-call input{flex:1 1 auto;min-width:0;height:42px;padding:0 15px;border-radius:999px;
  font:inherit;font-size:16px;letter-spacing:.06em;font-variant-numeric:tabular-nums;color:var(--ink);
  background:rgba(50,207,117,.1);border:1px solid rgba(50,207,117,.38);
  box-shadow:inset 0 1px 0 rgba(var(--wa),.16)}
.sc-cw .cw-call input::placeholder{color:var(--ink-mute);opacity:1;letter-spacing:.04em}
.sc-cw .cw-call input:focus{outline:none;border-color:rgba(50,207,117,.8);
  box-shadow:inset 0 1px 0 rgba(var(--wa),.16),0 0 0 3px rgba(50,207,117,.16)}
.sc-cw .cw-call.bad input{border-color:#ff8a6a}
.sc-cw .cw-call button{flex:none;height:42px;padding:0 18px;border-radius:999px;border:0;cursor:pointer;
  font:inherit;font-size:.86rem;font-weight:700;color:#04130a;transition:.2s;
  background:linear-gradient(160deg,#5cf09b,#22a75c);
  box-shadow:inset 0 1px 0 rgba(var(--wa),.5),0 4px 16px rgba(50,207,117,.35)}
.sc-cw .cw-call button:hover:not([disabled]){filter:brightness(1.08)}
.sc-cw .cw-call button[disabled]{opacity:.45;box-shadow:none;cursor:default}
.sc-cw .cw-call .fine{margin:0;font-size:.66rem;line-height:1.35;color:var(--ink-mute);text-align:center}
.sc-cw .cw-call .fine a{color:inherit;text-decoration:underline}
/* the chat bar under it drops its own top line so the two read as one block */
.sc-cw .cw-call + .cw-in{border-top-color:transparent}
[data-theme="light"] .sc-cw .cw-call{background:linear-gradient(180deg,rgba(50,207,117,.14),rgba(50,207,117,.06))}

/* "Get in contact", the first line of every lead form (partials/lead-form.html,
   Tim, 28 Sep 2026). Its own class so .hq-bare, which drops the rest of the
   form's header, keeps it. */
.fr-title{text-align:center;font-size:1.3rem;font-weight:800;letter-spacing:-.02em;
  line-height:1.25;color:var(--ink);margin:0 0 18px}
/* Since 1 Oct 2026 the title sits UNDER the film (Tim: "put the get in contact
   underneath the video and make the video fill up the top half and have
   rounded corners"), so the rotator bleeds to the card's top edge again with
   the card's own top radius, and its bottom corners are rounded too. */
@media(min-width:901px){
  .hq-panel .swf-slides{border-radius:calc(var(--r) - 2px) calc(var(--r) - 2px) 16px 16px}
}
.hq-face + .fr-title{margin-top:4px}

/* ── "Do you have a website?" Yes / No (Tim, 3 Oct 2026) ─────────────────
   Two .fr-check pills on one line under the question; Yes opens the web
   address field underneath (sc.js [data-site-q]). Shut = hidden + disabled,
   so it never posts. Same glass pill as the old "I don't have one". */
.site-q{display:grid;gap:9px;margin:2px 0 0}
.site-q-l{font-size:.86rem;font-weight:600;color:var(--ink-soft)}
.site-q-opts{display:flex;gap:9px;flex-wrap:wrap}
.site-q-opts .fr-check{min-width:86px;justify-content:center}
.site-q [data-site-url][hidden]{display:none}
.site-q-yes [data-site-url]{animation:siteq-in .28s ease-out}
@keyframes siteq-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
@media(prefers-reduced-motion:reduce){.site-q-yes [data-site-url]{animation:none}}

/* ── THE CHAT FORM (Tim, 7 Oct 2026) ──────────────────────────────────────
   "What's your name?" types out in white, then each answer brings the next
   question: Your number, Your email, Your business name, Do you have a
   website? The engine is the CHAT FORM block in sc.js, which adds .swf-chat
   and wraps each field in a .chat-q row. Nothing here applies without it, so
   a visitor with no JS keeps the plain all visible form.
   The step is a column so the collection notice and the Enquire under it can
   sit at the bottom while the questions fill in from the top (on the desktop
   hero sc.js holds the step at the full form's height, so the card never
   grows and the headline beside it never moves). max-height is lifted
   because the stage clamp of the old quiz would clip a full chat. */
html.js form.fr.swf-chat > .fr-step.fr-in{max-height:none}
form.fr.swf-chat > .fr-step{display:flex;flex-direction:column;gap:14px}
form.fr.swf-chat > .fr-step > [data-collect-notice]{margin-top:auto;padding-top:4px}
form.fr.swf-chat .chat-q{display:grid;gap:8px;margin:0}
form.fr.swf-chat .chat-q.chat-wait{display:none!important}
form.fr.swf-chat .chat-q.chat-in{animation:chatIn .45s cubic-bezier(.22,1,.36,1) both}
@keyframes chatIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
form.fr.swf-chat .chat-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;min-height:1.4em}
/* the question: white on the dark card (the hero flips its tokens, so --ink
   is white there in both themes), dark on a white form further down a page */
form.fr.swf-chat .chat-ask{margin:0;color:var(--ink);font-size:1.04rem;font-weight:700;
  letter-spacing:-.01em;line-height:1.35;text-align:left}
form.fr.swf-chat .chat-skip{flex:0 0 auto;background:none;border:0;padding:2px 4px;margin:0;cursor:pointer;
  font:inherit;font-size:.8rem;font-weight:600;color:var(--ink-soft);
  text-decoration:underline;text-underline-offset:2px;opacity:0;transition:opacity .3s ease}
form.fr.swf-chat .chat-typed .chat-skip{opacity:.85}
form.fr.swf-chat .chat-skip:hover{opacity:1;color:var(--ink)}
form.fr.swf-chat .chat-q:has(> input:not(:placeholder-shown)) .chat-skip{opacity:0;pointer-events:none}
/* the box (or the Yes / No) waits, holding its space, until its question has
   finished typing, then rises in under it */
form.fr.swf-chat .chat-q:not(.chat-typed) > input,
form.fr.swf-chat .chat-q:not(.chat-typed) .site-q-opts{visibility:hidden}
form.fr.swf-chat .chat-q.chat-typed > input,
form.fr.swf-chat .chat-q.chat-typed .site-q-opts{animation:chatIn .4s cubic-bezier(.22,1,.36,1) both}
/* three dots while it "thinks", then a caret while it types */
.chat-dots{display:inline-flex;align-items:center;gap:5px;height:1.35em;vertical-align:top}
.chat-dots i{width:6px;height:6px;border-radius:50%;background:currentColor;opacity:.3;
  animation:chatDot 1s ease-in-out infinite}
.chat-dots i:nth-child(2){animation-delay:.15s}
.chat-dots i:nth-child(3){animation-delay:.3s}
@keyframes chatDot{0%,60%,100%{opacity:.3;transform:none}30%{opacity:1;transform:translateY(-3px)}}
.chat-caret{display:inline-block;width:2px;height:1.05em;margin-left:3px;vertical-align:-.16em;
  background:var(--green);border-radius:1px;animation:chatBlink .9s steps(1) infinite}
@keyframes chatBlink{50%{opacity:0}}
/* Enter on an empty compulsory box gives it a small shake instead of moving on */
form.fr.swf-chat .chat-q.chat-typed.chat-nudge > input{animation:chatNudge .36s ease}
@keyframes chatNudge{20%{transform:translateX(-5px)}40%{transform:translateX(5px)}60%{transform:translateX(-3px)}80%{transform:translateX(2px)}}
@media(prefers-reduced-motion:reduce){
  form.fr.swf-chat .chat-q.chat-in,form.fr.swf-chat .chat-q.chat-typed > input,
  form.fr.swf-chat .chat-q.chat-typed .site-q-opts,form.fr.swf-chat .chat-nudge > input{animation:none}
  .chat-dots i,.chat-caret{animation:none}
}

/* THE PROGRESS BAR (Tim, 21 Oct 2026: "on the form when fills out each line
   have a small progress bar thats visible at all times"). A thin track under
   "Get in contact", there from the first frame at empty, filling a fifth for
   each question answered (a skipped optional one counts). sc.js adds it with
   the chat form and sets the fill's width. */
.fr-prog{position:relative;height:5px;border-radius:999px;margin:-6px 0 16px;
  background:rgba(var(--wa),.12);overflow:hidden}
.fr-prog > i{position:absolute;inset:0 auto 0 0;width:0;border-radius:inherit;
  background:linear-gradient(90deg,#32cf75,#38bdf8,#8b5cf6);
  box-shadow:0 0 10px rgba(56,189,248,.45);
  transition:width .5s cubic-bezier(.22,1,.36,1)}
@media(prefers-reduced-motion:reduce){.fr-prog > i{transition:none}}

/* ENQUIRE GLOWS AND BOUNCES (Tim, 21 Oct 2026: "the enquire button glows green
   and purple blue multi colour and bounces very softly once every 3 s"). Both
   Enquire buttons: the one at the bottom of every form (grey or live, the halo
   shows either way) and the Enquire pill in the dock. The halo walks green,
   blue, purple; the hop is 3px, once in each 3 second cycle. */
.swf-enquire,.sc-dock-cta .cta-enquire{
  animation:enqGlow 6s linear infinite,enqHop 3s ease-in-out infinite}
.swf-enquire.btn-send:not(.is-off){
  animation:seochkFlow 7s linear infinite,enqGlow 6s linear infinite,enqHop 3s ease-in-out infinite}
.swf-enquire.is-off{animation:enqGlow 6s linear infinite,enqHop 3s ease-in-out infinite;
  border-color:rgba(139,92,246,.45)}
@keyframes enqGlow{
  0%,100%{box-shadow:0 0 0 1.5px rgba(50,207,117,.7),0 0 24px 4px rgba(50,207,117,.5),0 6px 22px rgba(50,207,117,.25)}
  33%{box-shadow:0 0 0 1.5px rgba(56,189,248,.75),0 0 26px 5px rgba(56,189,248,.55),0 6px 22px rgba(56,189,248,.25)}
  66%{box-shadow:0 0 0 1.5px rgba(139,92,246,.8),0 0 26px 5px rgba(139,92,246,.58),0 6px 22px rgba(139,92,246,.25)}}
@keyframes enqHop{
  0%,78%,100%{transform:translateY(0)}
  85%{transform:translateY(-3px)}
  91%{transform:translateY(0)}
  95%{transform:translateY(-1px)}}
/* the home hero brings the card's children in with heroRiseIn (2.5s); Enquire
   keeps that entrance and starts glowing and hopping once it has landed */
html.js [data-intro] .hq-panel > .swf-enquire{
  animation:heroRiseIn .7s cubic-bezier(.22,.68,.34,1) 2.5s both,
    enqGlow 6s linear 3.2s infinite,enqHop 3s ease-in-out 3.2s infinite}
html.js [data-intro] .hq-panel > .swf-enquire.btn-send:not(.is-off){
  animation:heroRiseIn .7s cubic-bezier(.22,.68,.34,1) 2.5s both,seochkFlow 7s linear infinite,
    enqGlow 6s linear 3.2s infinite,enqHop 3s ease-in-out 3.2s infinite}
@media(prefers-reduced-motion:reduce){
  .swf-enquire,.swf-enquire.is-off,.sc-dock-cta .cta-enquire,
  html.js [data-intro] .hq-panel > .swf-enquire,
  html.js [data-intro] .hq-panel > .swf-enquire.btn-send:not(.is-off){animation:enqGlow 6s linear infinite}
}

/* ===========================================================================
   TOP BANNER  (Tim, 6 Oct 2026: "just turn it into a top banner ... make this
   fully width of the website and then when you scroll down it change so the
   subject of the content you are looking at", then "and when you scroll up it
   changes to the top menu")

   The floating pill is now a full width strip on the top edge. Same glass,
   same travelling highlight, and the rotating rim becomes one hairline along
   the bottom edge. The contents keep the 1140px column of the page.

   Two faces, switched by a class the CHROME ON DEMAND controller in dock.js
   sets on <html>:
     · menu     scrolling up, and everywhere the bar showed before: the logo,
                the links, SEO Check, Call and the appearance button
     · subject  scrolling down (html.ch-nav-subject): the logo and the heading
                of the section on screen, kept current by dock.js
   Everything else is as it was: no bar on arrival over a hero that carries the
   wordmark, and out of the way while a form or the calendar is in use.

   The menu face fades rather than leaving, and stays focusable, so a keyboard
   user who tabs into the bar gets the menu straight back (.kb-in).
   Same TRANSFORM TRAP as above: nothing here transforms .nav, and .nav .bar
   rests at none.
   =========================================================================== */
.nav{top:0}
.nav .bar{max-width:none;width:100%;margin:0;border-radius:0;
  padding:calc(10px + env(safe-area-inset-top)) max(20px, calc((100% - 1140px) / 2 + 20px)) 10px;
  box-shadow:0 16px 36px -24px rgba(0,0,0,.8),inset 0 1px 0 rgba(var(--wa),.08)}
/* the rim keeps only its bottom 1px, so the light runs along the edge the page
   scrolls under */
.nav .bar::before{padding:0 0 1px}
[data-theme="light"] .nav .bar{box-shadow:0 12px 30px -22px rgba(0,0,0,.3),inset 0 1px 0 #fff}
/* leaving, the strip slides up off the top edge instead of shrinking to a point */
html.ch-nav-off .nav .bar{transform:translateY(-102%)}
@media(prefers-reduced-motion:reduce){html.ch-nav-off .nav .bar{transform:none}}
/* the mobile sheet hangs off a full width bar now, so it keeps its own margins */
@media(max-width:1000px){.nav .links{left:12px;right:12px}}

/* the subject face, laid over the space the links use */
.nav-subject{position:absolute;z-index:3;top:env(safe-area-inset-top);bottom:0;
  left:var(--ns-left,200px);right:max(20px, calc((100% - 1140px) / 2 + 20px));
  display:flex;align-items:center;gap:10px;min-width:0;
  opacity:0;pointer-events:none;transition:opacity .25s ease}
.nav-subject .ns-dot{flex:0 0 auto;width:7px;height:7px;border-radius:50%;background:var(--green);
  box-shadow:0 0 0 3px var(--green-soft)}
.nav-subject .ns-text{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;
  color:var(--ink);font-size:.95rem;font-weight:700;letter-spacing:-.005em;transition:opacity .16s ease}
.nav-subject.ns-swap .ns-text{opacity:0}
html.ch-nav-subject .nav-subject{opacity:1}
/* the menu face steps back while the subject shows. Opacity only, never display
   or visibility, so Tab still reaches it and brings the menu back */
@media(min-width:1001px){.nav .links{transition:opacity .25s ease}}
.nav .sc-theme-wrap,.navtoggle{transition:opacity .25s ease}
html.ch-nav-subject .nav:not(.kb-in) .links,
html.ch-nav-subject .nav:not(.kb-in) .sc-theme-wrap,
html.ch-nav-subject .nav:not(.kb-in) .navtoggle,
html.ch-nav-subject .nav:not(.kb-in) .bar>.btn{opacity:0;pointer-events:none}
html.ch-nav-subject .nav.kb-in .nav-subject{opacity:0}
@media(max-width:1000px){.nav-subject .ns-text{font-size:.9rem}}
/* THE HANDOVER, DESKTOP (Tim, 7 Oct 2026: "the fade between the menu items
   and the category needs to be smoother, and slower"). Both faces used to fade
   at once over .25s, so for a moment the links and the heading sat on top of
   each other. Now they take turns, like the phone: the face that is leaving
   eases out over about .4s, the one arriving waits .22s and glides in over
   about .6s, the subject rising a few pixels as it lands. Each state's rule
   carries the timing of the face ENTERING it, so going either way plays out
   in the same order. */
@media(min-width:1001px){
  .nav .links,.nav .sc-theme-wrap{transition:opacity .6s cubic-bezier(.4,0,.2,1) .22s}
  html.ch-nav-subject .nav:not(.kb-in) .links,
  html.ch-nav-subject .nav:not(.kb-in) .sc-theme-wrap{transition:opacity .4s cubic-bezier(.4,0,.2,1)}
  .nav-subject{transform:translateY(5px);
    transition:opacity .4s cubic-bezier(.4,0,.2,1),transform .4s cubic-bezier(.4,0,.2,1)}
  html.ch-nav-subject .nav-subject{transform:none;
    transition:opacity .6s cubic-bezier(.4,0,.2,1) .22s,transform .8s cubic-bezier(.22,1,.36,1) .22s}
}
@media(min-width:1001px) and (prefers-reduced-motion:reduce){
  .nav .links,.nav .sc-theme-wrap,.nav-subject,html.ch-nav-subject .nav-subject{transition:opacity .2s ease;transform:none}
}
@media(prefers-reduced-motion:reduce){.nav-subject,.nav-subject .ns-text{transition:none}}
@media(max-width:680px){ .sc-cw .cw-call .fine{font-size:12px} }

/* ===========================================================================
   REVIEW BUBBLES  (Tim, 6 Oct 2026: "make these all unified, and easier to
   read. put them inside maybe google review style bubbles with 5 stars showing
   up", "but dont add google logo", "just stars that animate in")

   Each .gv-quotes list under a testimonial video is one card (sc.js builds it):
   five gold stars that pop in one after another, then the whole quote rises in.
   The cards in a row share a height and a clock, so they always line up and
   change together, a beat apart. A small tail points up at the name above.
   =========================================================================== */
.gv-quotes.gv-rv{display:block;margin-top:20px;--gv-card:#121615}
[data-theme="light"] .gv-quotes.gv-rv{--gv-card:#ffffff}
.gv-quotes.gv-rv > li.gv-card{position:relative;display:flex;flex-direction:column;gap:10px;
  padding:16px 18px 18px;border:1px solid var(--line-2);border-radius:18px;
  background:var(--gv-card);color:var(--ink);font-size:1rem;line-height:1.55;text-align:left;
  box-shadow:0 18px 40px -22px rgba(0,0,0,.75),inset 0 1px 0 rgba(var(--wa),.06);
  opacity:1;transform:none}
[data-theme="light"] .gv-quotes.gv-rv > li.gv-card{box-shadow:0 14px 34px -20px rgba(0,0,0,.22)}
.gv-quotes.gv-rv > li.gv-card::before{content:"";position:absolute;top:-7px;left:50%;width:12px;height:12px;
  margin-left:-6px;background:var(--gv-card);border:1px solid var(--line-2);border-right:0;border-bottom:0;
  border-radius:3px 0 0 0;transform:rotate(45deg)}
.gv-card .gv-q{margin:0;color:var(--ink-soft)}
.gv-card .gv-q em{font-style:normal;font-weight:700;color:var(--ink)}
.gv-stars{display:flex;gap:3px;line-height:0}
.gv-stars svg{width:18px;height:18px;display:block;fill:#fbbc04;opacity:0;transform:scale(.3) rotate(-35deg)}
.gv-card .gv-q{opacity:0;transform:translateY(8px)}

.gv-card.gv-play .gv-stars svg{animation:gvStarIn .55s cubic-bezier(.34,1.56,.64,1) forwards}
.gv-card.gv-play .gv-stars svg:nth-child(2){animation-delay:.07s}
.gv-card.gv-play .gv-stars svg:nth-child(3){animation-delay:.14s}
.gv-card.gv-play .gv-stars svg:nth-child(4){animation-delay:.21s}
.gv-card.gv-play .gv-stars svg:nth-child(5){animation-delay:.28s}
@keyframes gvStarIn{to{opacity:1;transform:none}}
.gv-card.gv-play .gv-q{opacity:1;transform:none;
  transition:opacity .6s ease .32s,transform .7s cubic-bezier(.22,1,.36,1) .32s}
/* leaving: stars and words fade together, then the next review arrives */
.gv-card.gv-leave .gv-stars,.gv-card.gv-leave .gv-q{opacity:0;transition:opacity .3s ease}

@media(max-width:760px){.gv-quotes.gv-rv > li.gv-card{font-size:1.02rem}}
@media(prefers-reduced-motion:reduce){
  .gv-stars svg,.gv-card .gv-q{opacity:1;transform:none;animation:none!important;transition:none!important}
  .gv-card.gv-leave .gv-stars,.gv-card.gv-leave .gv-q{opacity:0;transition:opacity .2s ease!important}
}

/* ===========================================================================
   PHONE TOP BAR, SMOOTHER  (Tim, 6 Oct 2026: "on mobile lets just make sure
   the menu on the top comes down smoother", "since it cant show everything
   just show the icon not the whole scalus logo ... so the summary of whats
   being shown on the page when the user is scrolling down has more space and
   maybe summarise more than the desktop version", "make everything on
   scalus.app smoother ... so smooth and seamless")

   · the bar glides down on a long soft curve and leaves on a quicker one
   · the menu sheet unrolls from under the bar (a clip that opens downward)
     and its rows settle in one after another, instead of popping in whole.
     The sheet is solid now, so the page behind never shows through it
   · scrolling down, the wordmark folds away to the bars icon and the section
     subject takes the space. dock.js shows a short phone summary of the
     heading (assets/data/nav-short.json) rather than cutting it off
   · the two faces hand over in turn (one leaves, then the other arrives)
     instead of sitting on top of each other for a frame
   Desktop is untouched. Same TRANSFORM TRAP as above: nothing transforms .nav,
   and .links carries its own transform, which is allowed because it is the
   element with the backdrop-filter, not an ancestor of one.
   =========================================================================== */
@media(max-width:1000px){
  .nav .bar{transition:background .3s,transform .62s cubic-bezier(.16,1,.3,1),opacity .4s ease}
  html.ch-nav-off .nav .bar{transform:translateY(-104%);
    transition:background .3s,transform .42s cubic-bezier(.55,0,.75,.2),opacity .32s ease .06s}

  /* the menu sheet */
  /* solid, not glass: through a see through sheet the page's own text sat
     behind the links and made both harder to read */
  .nav .links{background:#0c0e0d;visibility:hidden;
    clip-path:inset(0 0 100% 0 round 22px);transform:translateY(-6px);opacity:0;
    transition:clip-path .3s cubic-bezier(.4,0,.2,1),transform .3s cubic-bezier(.4,0,.2,1),
      opacity .22s ease .06s,visibility 0s linear .3s}
  .nav.open .links{visibility:visible;opacity:1;transform:none;
    clip-path:inset(-40px -40px -80px -40px round 22px);
    transition:clip-path .62s cubic-bezier(.22,1,.36,1),transform .62s cubic-bezier(.22,1,.36,1),
      opacity .2s ease,visibility 0s}
  [data-theme="light"] .nav .links{background:#ffffff}
  .nav .links > a{opacity:0;translate:0 -8px;transition:opacity .16s ease,translate .2s ease}
  .nav.open .links > a{opacity:1;translate:0 0;
    transition:opacity .42s ease var(--d,0s),translate .55s cubic-bezier(.22,1,.36,1) var(--d,0s)}
  .nav.open .links > a:nth-child(1){--d:.06s}
  .nav.open .links > a:nth-child(2){--d:.09s}
  .nav.open .links > a:nth-child(3){--d:.12s}
  .nav.open .links > a:nth-child(4){--d:.15s}
  .nav.open .links > a:nth-child(5){--d:.18s}
  .nav.open .links > a:nth-child(6){--d:.21s}
  .nav.open .links > a:nth-child(7){--d:.24s}
  .nav.open .links > a:nth-child(8){--d:.27s}
  .nav.open .links > a:nth-child(9){--d:.30s}
  .nav.open .links > a:nth-child(n+10){--d:.33s}
  .navtoggle span,.navtoggle span::before,.navtoggle span::after{transition:.38s cubic-bezier(.22,1,.36,1)}

  /* the wordmark folds to the bars icon while the subject shows. The bars sit
     at 7.5% to 23% across the 2000 x 355 lockup, so at 30px tall they are the
     12.5px to 39.5px slice: a 27px window, the image slid 12.5px left */
  .nav .bar>a:first-child{overflow:hidden;width:169px;flex:0 0 auto;
    transition:width .5s cubic-bezier(.22,1,.36,1)}
  .nav .bar>a:first-child img{width:169px;max-width:none;transition:transform .5s cubic-bezier(.22,1,.36,1)}
  html.ch-nav-subject .nav:not(.kb-in):not(.open) .bar>a:first-child{width:27px}
  html.ch-nav-subject .nav:not(.kb-in):not(.open) .bar>a:first-child img{transform:translateX(-12.5px)}

  /* the subject gets everything right of the icon */
  .nav-subject{left:calc(max(20px, env(safe-area-inset-left)) + 41px);transform:translateY(5px);
    transition:opacity .16s ease,transform .3s cubic-bezier(.22,1,.36,1)}
  html.ch-nav-subject .nav-subject{transform:none;
    transition:opacity .36s ease .16s,transform .5s cubic-bezier(.22,1,.36,1) .16s}
  .nav-subject .ns-text{font-size:1rem}
  .nav-subject.ns-swap .ns-text{opacity:0;transform:translateY(-4px)}
  .nav-subject .ns-text{transition:opacity .16s ease,transform .3s cubic-bezier(.22,1,.36,1)}

  /* and the menu buttons wait for the subject to clear before coming back */
  .nav .sc-theme-wrap,.navtoggle{transition:opacity .34s ease .14s}
  html.ch-nav-subject .nav:not(.kb-in) .sc-theme-wrap,
  html.ch-nav-subject .nav:not(.kb-in) .navtoggle{transition:opacity .16s ease}
}
@media(max-width:1000px) and (prefers-reduced-motion:reduce){
  .nav .links,.nav.open .links{clip-path:none;transform:none;transition:opacity .2s ease,visibility 0s linear .2s}
  .nav.open .links{transition:opacity .2s ease,visibility 0s}
  .nav .links > a,.nav.open .links > a{opacity:1;translate:none;transition:none}
  .nav .bar>a:first-child,.nav .bar>a:first-child img,.nav-subject,.nav-subject .ns-text{transition:none}
}

/* PAGE TO PAGE, NO WHITE FLASH (Tim, 6 Oct 2026: "make everything on scalus.app
   smoother, even if it needs to load a bit longer ... so smooth and seamless").
   Moving between pages on the site crossfades: the browser holds the page you
   are leaving until the next one is ready to paint, then dissolves into it.
   Safari 18.2+ and Chrome; anything older simply loads the page as before. */
@view-transition{navigation:auto}
::view-transition-old(root),::view-transition-new(root){
  animation-duration:.42s;animation-timing-function:cubic-bezier(.22,1,.36,1)}
@media(prefers-reduced-motion:reduce){@view-transition{navigation:none}}

/* OFF SCREEN SECTIONS STOP ANIMATING (Tim, 21 Oct 2026, scrolling glitch).
   sc.js marks a section .sc-off while it is more than 200px off screen; every
   looping animation inside it holds still until it scrolls back in. */
.sc-off,.sc-off::before,.sc-off::after,
.sc-off *,.sc-off *::before,.sc-off *::after{animation-play-state:paused!important}

/* ===========================================================================
   SCALUS MOTION, the one hand on the site and in the app (Tim, 23 Oct 2026:
   "smooth and like ios27", "a hybrid of samsung and apple latest os").
   The springs are UIKit style duration plus bounce sampled into linear(), the
   same four curves as my.scalus.app (app/globals.css) so a button, a sheet
   and a menu feel identical on scalus.app and in the Scalus app. Paste the
   curves verbatim, never retype them.
     --sc-spring         the pop (bounce .32): bubbles, menus, a reply snapping back
     --sc-spring-soft    bounce .18: sheets, panels, the drawer, anything big
     --sc-spring-snap    bounce .4: a pressed control letting go
     --sc-spring-elastic bounce .5, the One UI hand: small things that fold
   =========================================================================== */
:root{
  --sc-spring:linear(0, 0.0199 2.1%, 0.0722 4.2%, 0.1468 6.3%, 0.2353 8.3%, 0.331 10.4%, 0.4284 12.5%, 0.5234 14.6%, 0.613 16.7%, 0.6951 18.8%, 0.7684 20.8%, 0.8324 22.9%, 0.8869 25%, 0.9322 27.1%, 0.9688 29.2%, 0.9976 31.3%, 1.0193 33.3%, 1.0349 35.4%, 1.0453 37.5%, 1.0514 39.6%, 1.054 41.7%, 1.0539 43.8%, 1.0518 45.8%, 1.0483 47.9%, 1.0438 50%, 1.0387 52.1%, 1.0334 54.2%, 1.0282 56.3%, 1.0232 58.3%, 1.0185 60.4%, 1.0143 62.5%, 1.0106 64.6%, 1.0074 66.7%, 1.0047 68.8%, 1.0025 70.8%, 1.0008 72.9%, 0.9994 75%, 0.9985 77.1%, 0.9978 79.2%, 0.9973 81.3%, 0.9971 83.3%, 0.9971 85.4%, 0.9971 87.5%, 0.9973 89.6%, 0.9975 91.7%, 0.9978 93.8%, 0.9981 95.8%, 0.9983 97.9%, 1);
  --sc-spring-soft:linear(0, 0.0196 2.1%, 0.0697 4.2%, 0.1397 6.3%, 0.2211 8.3%, 0.3077 10.4%, 0.3947 12.5%, 0.4788 14.6%, 0.5578 16.7%, 0.6301 18.8%, 0.6951 20.8%, 0.7525 22.9%, 0.8024 25%, 0.845 27.1%, 0.881 29.2%, 0.911 31.3%, 0.9355 33.3%, 0.9553 35.4%, 0.971 37.5%, 0.9832 39.6%, 0.9924 41.7%, 0.9993 43.8%, 1.0042 45.8%, 1.0075 47.9%, 1.0096 50%, 1.0107 52.1%, 1.0111 54.2%, 1.011 56.3%, 1.0105 58.3%, 1.0097 60.4%, 1.0088 62.5%, 1.0079 64.6%, 1.0069 66.7%, 1.006 68.8%, 1.0051 70.8%, 1.0043 72.9%, 1.0035 75%, 1.0029 77.1%, 1.0023 79.2%, 1.0018 81.3%, 1.0014 83.3%, 1.0011 85.4%, 1.0008 87.5%, 1.0005 89.6%, 1.0004 91.7%, 1.0002 93.8%, 1.0001 95.8%, 1 97.9%, 1);
  --sc-spring-snap:linear(0, 0.0201 2.1%, 0.0737 4.2%, 0.1511 6.3%, 0.2441 8.3%, 0.3457 10.4%, 0.45 12.5%, 0.5523 14.6%, 0.6492 16.7%, 0.7382 18.8%, 0.8175 20.8%, 0.8862 22.9%, 0.944 25%, 0.9912 27.1%, 1.0282 29.2%, 1.0559 31.3%, 1.0753 33.3%, 1.0875 35.4%, 1.0935 37.5%, 1.0947 39.6%, 1.0919 41.7%, 1.0861 43.8%, 1.0784 45.8%, 1.0693 47.9%, 1.0596 50%, 1.0497 52.1%, 1.0401 54.2%, 1.0311 56.3%, 1.0228 58.3%, 1.0156 60.4%, 1.0093 62.5%, 1.0041 64.6%, 0.9999 66.7%, 0.9966 68.7%, 0.9942 70.8%, 0.9925 72.9%, 0.9915 75%, 0.9911 77.1%, 0.9911 79.2%, 0.9914 81.3%, 0.992 83.3%, 0.9928 85.4%, 0.9937 87.5%, 0.9946 89.6%, 0.9955 91.7%, 0.9964 93.7%, 0.9973 95.8%, 0.998 97.9%, 1);
  --sc-spring-elastic:linear(0, 0.0082 2.1%, 0.0313 4.2%, 0.0671 6.2%, 0.1135 8.3%, 0.1683 10.4%, 0.2298 12.5%, 0.2962 14.6%, 0.3656 16.7%, 0.4367 18.8%, 0.5081 20.8%, 0.5787 22.9%, 0.6473 25%, 0.7132 27.1%, 0.7756 29.2%, 0.8339 31.2%, 0.8878 33.3%, 0.9368 35.4%, 0.9809 37.5%, 1.0198 39.6%, 1.0536 41.7%, 1.0825 43.8%, 1.1064 45.8%, 1.1258 47.9%, 1.1407 50%, 1.1515 52.1%, 1.1586 54.2%, 1.1623 56.2%, 1.1629 58.3%, 1.1608 60.4%, 1.1565 62.5%, 1.1501 64.6%, 1.1421 66.7%, 1.1328 68.8%, 1.1225 70.8%, 1.1115 72.9%, 1.1001 75%, 1.0885 77.1%, 1.0769 79.2%, 1.0654 81.2%, 1.0544 83.3%, 1.0438 85.4%, 1.0338 87.5%, 1.0245 89.6%, 1.0159 91.7%, 1.0082 93.8%, 1.0012 95.8%, 0.9951 97.9%, 1);
}

/* THE PRESS, ONCE. Every control answers a press the same way: down is quick
   and flat so it lands under the finger, letting go rides --sc-spring-snap, so
   it pops back past its size and settles, the way an iOS 27 button does. The
   hover lift the green button already has still works: a press simply wins
   while the finger is down. */
.btn,.sc-dock-in a,.sc-dock-in button,.navtoggle,.sc-theme-wrap button,
.nav .links > a,.sc-cw .cw-in button,.sc-cw .cw-x,.sc-cw .cw-close{
  transition-property:transform,background,background-color,border-color,color,box-shadow,opacity,filter;
  transition-duration:480ms,.3s,.3s,.3s,.3s,.3s,.3s,.3s;
  transition-timing-function:var(--sc-spring-snap),ease,ease,ease,ease,ease,ease,ease}
.btn:active,.sc-chat:active,.talk-now:active,.sc-dock-in a:active,.sc-dock-in button:active,
.navtoggle:active,.sc-theme-wrap button:active,.sc-cw .cw-in button:active,.sc-cw .cw-x:active,.sc-cw .cw-close:active{
  transform:scale(.94)!important;transition-duration:90ms;transition-timing-function:ease-out}
/* the launcher and Talk now keep their own quick transition (they also step
   out of the way on scroll and focus, and an exit should not bounce) */
.sc-chat:active,.talk-now:active{transition-duration:90ms}
/* a whole menu row shrinking by a tenth reads as a fault, so it gives a touch */
.nav .links > a:active{transform:scale(.985);transition-duration:90ms;transition-timing-function:ease-out}

/* THE CHAT PANEL arrives on the soft spring (a small overshoot that settles),
   the iOS 27 half; closing keeps the flat Apple sheet curve, a surface leaving
   should not bounce. Its contents rise a beat behind the glass, as before. */
.sc-cw.open{transition:opacity .28s ease,transform .64s var(--sc-spring-soft),border-radius .64s var(--sc-spring-soft)}
.sc-cw.open .cw-head,.sc-cw.open .cw-note,.sc-cw.open .cw-body,.sc-cw.open .cw-in{
  transition:opacity .4s ease .18s,transform .58s var(--sc-spring-soft) .18s}

/* THE PHONE MENU unfolds on the soft spring and its rows cascade on the pop,
   22ms apart, the One UI list arrival. */
@media(max-width:1000px){
  .nav.open .links{transition:clip-path .68s var(--sc-spring-soft),transform .68s var(--sc-spring-soft),opacity .2s ease,visibility 0s}
  /* closed: the rows sit 12px left, and the close keeps its own quick fade
     with translate in it (the press rule above left translate out, so the
     rows jumped left while still visible, Gatekeeper 23 Oct 2026) */
  .nav .links > a{translate:-12px 0;transition:opacity .16s ease,translate .2s ease,transform 480ms var(--sc-spring-snap)}
  .nav.open .links > a{transition:opacity .42s ease var(--d,0s),translate .6s var(--sc-spring) var(--d,0s),transform 480ms var(--sc-spring-snap)}
  .nav.open .links > a:nth-child(1){--d:.05s}
  .nav.open .links > a:nth-child(2){--d:.072s}
  .nav.open .links > a:nth-child(3){--d:.094s}
  .nav.open .links > a:nth-child(4){--d:.116s}
  .nav.open .links > a:nth-child(5){--d:.138s}
  .nav.open .links > a:nth-child(6){--d:.16s}
  .nav.open .links > a:nth-child(7){--d:.182s}
  .nav.open .links > a:nth-child(8){--d:.204s}
  .nav.open .links > a:nth-child(9){--d:.226s}
  .nav.open .links > a:nth-child(n+10){--d:.248s}
}

/* THE DOCK comes up on the soft spring too. */
.sc-dock-in{animation-timing-function:var(--sc-spring-soft)}

@media(prefers-reduced-motion:reduce){
  .btn:active,.sc-chat:active,.talk-now:active,.sc-dock-in a:active,.sc-dock-in button:active,.navtoggle:active,
  .sc-theme-wrap button:active,.nav .links > a:active,.sc-cw .cw-in button:active,.sc-cw .cw-x:active,.sc-cw .cw-close:active{transform:none!important}
  .nav .links > a,.nav.open .links > a{translate:none;transition:none}
}
