/* GetDigital — shared design system.
   Every page links this. thanks.html once drifted onto an older palette because
   the tokens were copied per file; a customer who had just paid landed on a page
   that looked like a different company. One file now, so that cannot recur. */

:root{
  --white:#FFFFFF;
  --surface:#F5F5F7;
  --surface-2:#EBEBEF;
  --ink:#1D1D1F;      /* never #000 — pure black vibrates on white */
  --ink-2:#6E6E73;
  --accent:#0A5FFF;   /* the only saturated colour on any page */
  --accent-press:#0846C4;

  --bg:var(--white); --bg-alt:var(--surface); --fg:var(--ink); --fg-dim:var(--ink-2);
  --rule:var(--surface-2); --card:var(--white);

  --sans:'Inter',-apple-system,BlinkMacSystemFont,'SF Pro Text','Segoe UI',Roboto,sans-serif;
  --wrap:1120px;
  --pill:980px;
}
@media (prefers-color-scheme: dark){
  :root{--bg:#0B0B0C;--bg-alt:#141416;--fg:#F5F5F7;--fg-dim:#9A9AA0;
    --rule:#28282C;--card:#141416;--accent:#4C8DFF;--accent-press:#3A78E8}
}
:root[data-theme="light"]{
  --bg:var(--white);--bg-alt:var(--surface);--fg:var(--ink);--fg-dim:var(--ink-2);
  --rule:var(--surface-2);--card:var(--white);--accent:#0A5FFF;--accent-press:#0846C4}
:root[data-theme="dark"]{
  --bg:#0B0B0C;--bg-alt:#141416;--fg:#F5F5F7;--fg-dim:#9A9AA0;
  --rule:#28282C;--card:#141416;--accent:#4C8DFF;--accent-press:#3A78E8}

*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);
  font-size:17px;line-height:1.5;-webkit-font-smoothing:antialiased;
  font-feature-settings:"cv02","cv03","cv04"}
img,video{max-width:100%}
a{color:var(--accent)}
.wrap{max-width:var(--wrap);margin:0 auto;padding:0 24px}

/* Weight 600, never black. Negative tracking scaled to size. Sentence case. */
h1,h2,h3{margin:0;font-weight:600;color:var(--fg)}
h1{font-size:clamp(2.4rem,5vw,3.6rem);line-height:1.05;letter-spacing:-.032em}
h2{font-size:clamp(1.6rem,3vw,2.2rem);line-height:1.1;letter-spacing:-.026em}
h3{font-size:1.2rem;line-height:1.25;letter-spacing:-.018em}
p{margin:0 0 1rem}
.lede{font-size:clamp(1.08rem,1.6vw,1.28rem);color:var(--fg-dim);max-width:52ch;
  letter-spacing:-.011em}
.eyebrow{font-size:.9rem;font-weight:600;color:var(--accent);margin:0 0 12px}

header{position:sticky;top:0;z-index:20;
  background:color-mix(in srgb,var(--bg) 82%,transparent);
  backdrop-filter:saturate(180%) blur(20px);
  -webkit-backdrop-filter:saturate(180%) blur(20px);
  border-bottom:1px solid transparent;transition:border-color .3s}
header.stuck{border-bottom-color:var(--rule)}
.bar{display:flex;align-items:center;gap:20px;height:56px}
.logo{font-size:1.14rem;font-weight:600;letter-spacing:-.024em;text-decoration:none;
  color:var(--fg);display:flex;align-items:center;gap:8px}
/* The mark. Same two shapes at every size — header, favicon, film, previews.
   Was a bare 9px circle here while the film used a rounded square with a dot;
   three surfaces, three different logos. */
.logo .dot{width:22px;height:22px;flex:0 0 auto;background:var(--accent);
  border-radius:7px;display:grid;place-items:center}
.logo .dot::after{content:"";width:7px;height:7px;border-radius:50%;background:#fff}
nav{margin-left:auto;display:flex;gap:28px;align-items:center}
nav a{font-size:.88rem;text-decoration:none;color:var(--fg-dim);letter-spacing:-.006em}
nav a:hover{color:var(--fg)}
/* 940px, not 720. The full homepage nav — Clients, Why us, Pricing, How it
   works, Questions, Guides, About, the phone and the CTA — needs about 900px
   beside the logo. Hiding the secondary links only below 720px left every
   tablet in portrait (768px is the common one) with a nav 107px wider than the
   screen, which pushed the whole document sideways. */
@media(max-width:940px){nav a.hide-sm{display:none}nav{gap:18px}}

/* Small phones. At 390px the bar needed 447px: logo 112, "Pricing" 66, the
   phone 127 and the CTA 86, plus gaps — against 342px of usable width. The
   header overflowed and took the whole document with it.

   Everything except the two conversion controls goes: the phone number is a
   one-tap call and the button is the primary action, while every hidden link
   is still reachable by scrolling and from the footer. Written as
   :not(.hdr-tel):not(.btn) rather than a new class so it needs no markup
   change and covers the homepage, the builder and all 39 generated pages
   identically. */
@media(max-width:430px){
  nav a:not(.hdr-tel):not(.btn){display:none}
  nav{gap:12px}
  .bar{gap:12px}
  .hdr-tel{font-size:.82rem}
  nav .btn{padding:10px 14px;font-size:.85rem}
}
/* The HEADER pill must never wrap: "Build yours free" broke onto two lines at
   390px and spilled out of the 56px bar. The overflow check passed because it
   wrapped rather than overflowed, so this was only ever visible in a
   screenshot.

   Scoped to `nav` deliberately. Applying it to every .btn clipped the hero's
   own CTA — "Build yours free — see it in 10 seconds" is a deliberately long
   label that has to wrap to two lines on a phone. */
nav .btn{white-space:nowrap}

/* On phones the header carries the logo and the phone number, and nothing
   else. All three will not fit on one line at any phone width: logo 112 +
   phone 118 + "Build yours free" 133 is 375px against 342px of usable space
   at 390.

   The phone is what stays, and not by preference — AGENTS.md requires the
   phone, address and named founder on every page, because their absence is
   what got the domain flagged by trust scanners in the first place. The
   builder CTA is not lost: it is the hero's own button, immediately below. */
@media(max-width:480px){
  nav .btn{display:none}
}
@media(max-width:380px){
  .wrap{padding:0 18px}
}

.btn{display:inline-block;font-size:.95rem;font-weight:500;text-decoration:none;
  padding:12px 22px;border-radius:var(--pill);border:1px solid var(--rule);
  color:var(--fg);background:transparent;cursor:pointer;letter-spacing:-.008em;
  transition:background .2s,border-color .2s,transform .08s}
.btn:hover{background:var(--bg-alt)}
.btn:active{transform:scale(.985)}
.btn-solid{background:var(--accent);border-color:var(--accent);color:#fff}
.btn-solid:hover{background:var(--accent-press);border-color:var(--accent-press)}
.btn:focus-visible,a:focus-visible,button:focus-visible{outline:2px solid var(--accent);
  outline-offset:3px}

/* Long-form pages: legal, about, guarantee. */
.doc{max-width:760px;margin:0 auto;padding:72px 24px 96px}
.doc h1{margin-bottom:14px}
.doc .updated{color:var(--fg-dim);font-size:.92rem;margin-bottom:44px}
.doc h2{margin:44px 0 14px}
.doc h3{margin:28px 0 10px}
.doc p,.doc li{color:var(--fg-dim)}
.doc strong{color:var(--fg);font-weight:600}
.doc ul{padding-left:22px;margin:0 0 1rem}
.doc li{margin-bottom:9px}
.doc .box{background:var(--bg-alt);border-radius:14px;padding:22px 24px;margin:26px 0}
.doc .box p:last-child{margin-bottom:0}

footer{padding:52px 0;font-size:.9rem;color:var(--fg-dim);
  border-top:1px solid var(--rule);margin-top:20px}
.foot{display:flex;justify-content:space-between;gap:24px;flex-wrap:wrap;
  align-items:flex-start}
.foot-links{display:flex;gap:22px;flex-wrap:wrap}
.foot-links a{color:var(--fg-dim);text-decoration:none}
.foot-links a:hover{color:var(--fg)}
.theme-btn{background:none;border:1px solid var(--rule);color:var(--fg-dim);
  font-size:.82rem;padding:8px 14px;border-radius:var(--pill);cursor:pointer;
  font-family:inherit}
.theme-btn:hover{color:var(--fg);background:var(--bg-alt)}


/* ===== INTERACTION =====
   Native equivalents of the react-bits effects, without React. Every rule here
   is additive: it changes how something looks on hover or scroll, and none of
   it can make content invisible. That constraint is deliberate — a reveal
   animation previously left the entire pricing table at opacity 0 on the live
   site, which is a far worse outcome than having no effect at all. */

/* Pointer-tracked spotlight on cards. --mx/--my are set by JS; if the script
   never runs the gradient simply sits at the default position and nothing
   breaks. */
.spot{position:relative;isolation:isolate}
.spot::before{content:"";position:absolute;inset:0;border-radius:inherit;
  background:radial-gradient(340px circle at var(--mx,50%) var(--my,0%),
    color-mix(in srgb,var(--accent) 13%,transparent), transparent 62%);
  opacity:0;transition:opacity .35s ease;pointer-events:none;z-index:-1}
.spot:hover::before{opacity:1}

/* Reading progress. Sits on the header's lower edge. */
.progress{position:absolute;left:0;bottom:-1px;height:2px;width:100%;
  transform:scaleX(var(--p,0));transform-origin:left;background:var(--accent);
  transition:transform .1s linear}
header{position:sticky}

/* Trade marquee. Duplicated track, translated by half its width, so the loop
   is seamless. Pauses on hover and for reduced-motion users. */
/* min-width:0 so a max-content track inside can never widen its own column. */
.marquee{min-width:0;overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,
  #000 8%,#000 92%,transparent);mask-image:linear-gradient(90deg,transparent,
  #000 8%,#000 92%,transparent)}
.marquee-track{display:flex;gap:8px;width:max-content;animation:slide 38s linear infinite}
.marquee:hover .marquee-track{animation-play-state:paused}
@keyframes slide{to{transform:translateX(-50%)}}

/* Buttons get a press response, never a looping animation — motion on a CTA
   measurably underperforms a still, high-contrast one. */
.btn{will-change:transform}

@media (prefers-reduced-motion: reduce){
  .marquee-track{animation:none}
  .spot::before{display:none}
  .progress{display:none}
}


/* 3D tilt. Card leans toward the pointer. transform-style preserve-3d so the
   inner content comes with it rather than sliding flat. */
.tilt{transform-style:preserve-3d;
  transform:perspective(900px) rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg))
    translateY(var(--ty,0px));
  transition:transform .35s cubic-bezier(.22,.61,.36,1),box-shadow .35s}
.tilt:hover{--ty:-4px;transition-duration:.08s}

/* Aurora. Three slow drifting blobs behind the hero. Purely decorative and
   pointer-transparent, sitting under the content at z-index -1. */
.aurora{position:absolute;inset:-20% -10% auto -10%;height:130%;z-index:-1;
  pointer-events:none;overflow:hidden;filter:blur(58px);opacity:.5}
@media (prefers-color-scheme: dark){.aurora{opacity:.34}}
.aurora i{position:absolute;display:block;border-radius:50%;
  background:radial-gradient(circle,var(--c) 0%,transparent 70%)}
.aurora i:nth-child(1){--c:#0A5FFF;width:46%;padding-bottom:46%;left:2%;top:6%;
  animation:drift1 22s ease-in-out infinite alternate}
.aurora i:nth-child(2){--c:#7C4DFF;width:38%;padding-bottom:38%;right:6%;top:0;
  animation:drift2 27s ease-in-out infinite alternate}
.aurora i:nth-child(3){--c:#00C2A8;width:32%;padding-bottom:32%;left:38%;top:38%;
  animation:drift3 31s ease-in-out infinite alternate}
@keyframes drift1{to{transform:translate(16%,12%) scale(1.15)}}
@keyframes drift2{to{transform:translate(-14%,18%) scale(1.1)}}
@keyframes drift3{to{transform:translate(10%,-16%) scale(1.2)}}
/* overflow-x:clip, not hidden.
   The aurora is inset -10% on each side so its blobs bleed past the hero, and
   with nothing clipping them the document grew 144px wider than the viewport —
   the whole page scrolled sideways, at every width, worst on a phone.

   `clip` rather than `hidden` deliberately: overflow:hidden creates a scroll
   container, which breaks `position:sticky` on descendants and would have
   silently killed the sticky header. `clip` crops without that side effect. */
.hero{position:relative;isolation:isolate;overflow-x:clip}

/* Counting prices. The final figure is in the HTML; JS only animates toward it,
   so a script failure leaves the correct number on screen. */
.price{font-variant-numeric:tabular-nums}

@media (prefers-reduced-motion: reduce){
  .tilt{transform:none!important}
  .aurora i{animation:none}
}


/* ===== SELLING STRUCTURE =====
   57% of viewing time is spent above the fold, so the hero carries a trust
   strip as well as the promise. Baymard puts visible trust signals at up to a
   35% conversion lift. The CTA repeats three times down the page rather than
   once — hero, after the comparison, and after the objections. */

.trust-strip{display:flex;flex-wrap:wrap;gap:8px 20px;margin-top:26px;
  padding-top:22px;border-top:1px solid var(--rule)}
.trust-strip span{display:flex;align-items:center;gap:8px;font-size:.88rem;
  color:var(--fg-dim)}
.trust-strip span::before{content:"";width:12px;height:7px;flex:0 0 auto;
  border-left:2px solid var(--accent);border-bottom:2px solid var(--accent);
  transform:rotate(-45deg) translateY(-2px)}

/* comparison table */
.cmp-wrap{margin-top:48px;overflow-x:auto;-webkit-overflow-scrolling:touch}
.cmp{width:100%;border-collapse:separate;border-spacing:0;min-width:680px;
  font-size:.95rem}
.cmp th,.cmp td{text-align:left;padding:16px 18px;vertical-align:top;
  border-bottom:1px solid var(--rule)}
.cmp thead th{font-weight:600;color:var(--fg);font-size:1rem;
  letter-spacing:-.014em;border-bottom:2px solid var(--rule);
  padding-bottom:14px}
.cmp thead th span{display:block;font-weight:400;font-size:.82rem;
  color:var(--fg-dim);margin-top:3px;letter-spacing:0}
.cmp tbody th{font-weight:500;color:var(--fg);width:190px}
.cmp tbody th em{font-style:italic}
.cmp td{color:var(--fg-dim)}
.cmp .mine{background:color-mix(in srgb,var(--accent) 7%,transparent)}
.cmp thead .mine{border-bottom-color:var(--accent)}
.cmp td.mine{color:var(--fg)}
.cmp td.mine strong,.cmp tbody td strong{color:var(--fg);font-weight:600}

/* the hourly-rate arithmetic */
.math{margin-top:40px;background:var(--bg-alt);border-radius:18px;padding:30px 32px;
  max-width:720px}
.math-k{font-weight:600;color:var(--fg);letter-spacing:-.016em;margin-bottom:10px}
.math p{color:var(--fg-dim);margin:0}
.math strong{color:var(--fg);font-weight:600}

/* objections */
.objections{background:var(--bg-alt)}
.obj-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:52px}
@media(max-width:980px){.obj-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:660px){.obj-grid{grid-template-columns:1fr}}
.obj{background:var(--card);border:1px solid var(--rule);border-radius:18px;
  padding:26px 24px}
.obj h3{font-size:1.04rem;margin-bottom:10px}
.obj p{color:var(--fg-dim);font-size:.95rem;margin:0}
.obj strong{color:var(--fg);font-weight:600}

/* risk reversal band */
.risk-in{display:grid;grid-template-columns:1.1fr .9fr;gap:56px;align-items:center}
@media(max-width:900px){.risk-in{grid-template-columns:1fr;gap:34px}}
.risk-list{list-style:none;margin:0;padding:0;display:grid;gap:12px}
.risk-list li{position:relative;padding-left:30px;color:var(--fg-dim);font-size:.98rem}
.risk-list li::before{content:"";position:absolute;left:2px;top:.42em;width:13px;
  height:7px;border-left:2px solid var(--accent);border-bottom:2px solid var(--accent);
  transform:rotate(-45deg)}

/* repeated mid-page calls to action */
.cta-band{margin-top:52px;display:flex;gap:14px;flex-wrap:wrap;align-items:center}
.cta-band p{margin:0;color:var(--fg-dim);font-size:.95rem}

/* ===== PROOF =====
   Placed directly after the hero: trust signals sit highest on the page for a
   reason. Attribution is by the name each client gave — no stock photography,
   no invented job titles, no logo wall of companies that were never clients. */
.proof{background:var(--bg-alt)}
.quotes{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:52px}
@media(max-width:900px){.quotes{grid-template-columns:1fr;max-width:520px;margin-inline:auto}}
.quote{margin:0;background:var(--card);border:1px solid var(--rule);
  border-radius:18px;padding:28px 26px}
.stars{color:#F5A623;letter-spacing:2px;font-size:1rem;margin-bottom:14px}
.quote blockquote{margin:0 0 16px;font-size:1.12rem;line-height:1.42;
  letter-spacing:-.016em;color:var(--fg);font-weight:500}
.quote figcaption{color:var(--fg-dim);font-size:.92rem}
.proof-note{margin-top:26px;font-size:.86rem;color:var(--fg-dim);max-width:64ch}

/* Results band. Deliberately separated from the testimonials above: those are
   clients speaking, these are projects we've run ourselves. Blurring the two is
   the kind of claim that unravels the moment somebody asks a follow-up. */
.results{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:22px;
  padding-top:34px;border-top:1px solid var(--rule)}
@media(max-width:820px){.results{grid-template-columns:1fr;gap:18px}}
.res-item p{margin:0;color:var(--fg-dim);font-size:.95rem}
.res-n{font-size:2rem;font-weight:600;letter-spacing:-.032em;color:var(--accent);
  margin-bottom:6px}

/* ===== BUILDER ADVERT =====
   A dark band directly under the hero. Everything else on this page argues that
   a website is worth having; this offers to just show them one. */
.advert{background:var(--ink);color:#fff;padding:78px 0}
@media (prefers-color-scheme: dark){.advert{background:#101014}}
:root[data-theme="dark"] .advert{background:#101014}
.advert-in{display:grid;grid-template-columns:1.05fr .95fr;gap:56px;align-items:center}
@media(max-width:940px){.advert-in{grid-template-columns:1fr;gap:38px}}
.advert h2{color:#fff;font-size:clamp(1.9rem,3.4vw,2.7rem);letter-spacing:-.03em;
  line-height:1.08}
.advert p{color:rgba(255,255,255,.7);margin-top:18px;max-width:50ch}
.advert-cta{display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin-top:30px}
.advert-cta span{font-size:.88rem;color:rgba(255,255,255,.55)}
.btn-invert{background:#fff;border-color:#fff;color:var(--ink)}
.btn-invert:hover{background:#EDEDF0;border-color:#EDEDF0}

/* A miniature of the generated page. Pure CSS — no screenshot to go stale. */
.advert-demo{border-radius:14px;overflow:hidden;background:#fff;
  box-shadow:0 24px 70px rgba(0,0,0,.45);transform:rotate(-.6deg)}
.ad-bar{display:flex;align-items:center;gap:6px;padding:9px 12px;background:#F1F2F5;
  border-bottom:1px solid #E3E5EA}
.ad-bar i{width:8px;height:8px;border-radius:50%;background:#D3D6DD}
.ad-bar span{margin-left:8px;font-size:.66rem;color:#8A8F9A;
  font-family:ui-monospace,Menlo,monospace}
.ad-body{background:#fff}
.ad-util{display:flex;justify-content:space-between;padding:7px 14px;
  background:#101820;color:rgba(255,255,255,.72);font-size:.62rem}
.ad-util b{color:#fff}
.ad-head{display:flex;align-items:center;gap:9px;padding:12px 14px;
  border-bottom:1px solid #E9EBEF}
.ad-mark{width:26px;height:26px;border-radius:7px;background:#2563C9;color:#fff;
  display:grid;place-items:center;font-size:.6rem;font-weight:700}
.ad-head strong{display:block;font-size:.78rem;color:#12161C;letter-spacing:-.01em}
.ad-head small{display:block;font-size:.53rem;color:#7B818C;letter-spacing:.06em}
.ad-hero{padding:26px 16px 22px;
  background:radial-gradient(90% 80% at 88% 8%,rgba(37,99,201,.13) 0%,transparent 60%)}
.ad-pill{display:inline-block;font-size:.53rem;letter-spacing:.12em;
  text-transform:uppercase;color:#2563C9;border:1px solid #2563C9;
  border-radius:100px;padding:4px 9px}
.ad-hero h3{margin:11px 0 0;font-size:1.02rem;line-height:1.15;color:#12161C;
  letter-spacing:-.024em;max-width:15ch}
.ad-btns{display:flex;gap:6px;margin-top:14px}
.ad-btns i{width:70px;height:20px;border-radius:5px;border:1px solid #DFE2E8}
.ad-btns i.fill{background:#2563C9;border-color:#2563C9;width:86px}
.ad-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:#E9EBEF;
  border-top:1px solid #E9EBEF}
.ad-grid i{background:#fff;height:44px}

/* The one action offered after a preview renders. There is deliberately no
   download and no blob link — handing over the file is the product. */
.mini-cta{background:var(--accent);border-color:var(--accent);color:#fff;
  font-weight:600}
.mini-cta:hover{background:var(--accent-press);border-color:var(--accent-press);
  color:#fff}


/* ===================================================================== */
/* ===== MOTION LAYER ==================================================
   Shared by index.html and builder.html.

   THE SAFETY CONTRACT, which every rule below obeys:

   Nothing here does anything unless <html> carries `.anim`. That class is set
   by a tiny inline script before first paint, and only when the visitor has
   NOT asked for reduced motion. If the script is blocked, never runs, or the
   motion JS throws — the JS removes `.anim` in its own catch block — every
   selector here stops matching and the page is simply static and fully
   visible.

   This is not caution for its own sake. A reveal animation once shipped to
   production with the entire pricing table stuck at opacity 0. A page with no
   animation sells websites. A page with invisible prices does not. When those
   two outcomes are in tension, the static page wins every time.

   Corollary: never write a rule here that sets opacity:0 or a large transform
   without an `.anim` ancestor selector, and never add one whose only "on"
   state comes from a JS class that has no watchdog behind it. */

/* --- 1. Reveal on scroll ------------------------------------------------
   The workhorse. Rise sixteen pixels and fade. Short enough not to be a
   waiting room, long enough to read as deliberate. */
.anim .reveal{opacity:0;transform:translateY(16px);
  transition:opacity .6s cubic-bezier(.22,.61,.36,1),
             transform .6s cubic-bezier(.22,.61,.36,1);
  transition-delay:var(--d,0s)}
.anim .reveal.in{opacity:1;transform:none}

/* --- 2. Staggered children ----------------------------------------------
   `.seq` on a grid or flex parent; children arrive one after another. The
   delay is a custom property so the JS can set it per child and the stagger
   keeps working past whatever nth-child list we happened to write. */
.anim .seq>*{opacity:0;transform:translateY(14px);
  transition:opacity .55s cubic-bezier(.22,.61,.36,1),
             transform .55s cubic-bezier(.22,.61,.36,1);
  transition-delay:var(--d,0s)}
.anim .seq.in>*{opacity:1;transform:none}

/* Opacity-only stagger, for children that already own their `transform`.
   The pricing tiers carry `.tilt`, whose whole job is a transform; the rule
   above finishes on `transform:none` and would flatten the lean permanently
   the moment the entrance completed. Anything with .tilt, .float or its own
   transform gets this variant instead. */
.anim .seq-fade>*{opacity:0;transition:opacity .55s ease;
  transition-delay:var(--d,0s)}
.anim .seq-fade.in>*{opacity:1}

/* --- 3. Split-line headline ---------------------------------------------
   Each word rides up from behind a mask, so the line assembles itself rather
   than fading in as a block. The wrapping is done in JS and only committed if
   it succeeds; on failure the original markup is put back untouched. */
.anim .split .w{display:inline-block;overflow:hidden;vertical-align:top;
  padding-bottom:.05em;margin-bottom:-.05em}
.anim .split .w>i{display:inline-block;font-style:inherit;
  transform:translateY(110%);
  transition:transform .75s cubic-bezier(.16,.84,.28,1);
  transition-delay:var(--d,0s)}
.anim .split.in .w>i{transform:none}

/* --- 4. Magnetic buttons ------------------------------------------------
   The button leans a few pixels toward the cursor. Small enough that most
   people register it as responsiveness rather than as an effect. Capped at
   6px by the JS so the hit area never runs away from the pointer. */
.anim .mag{transition:transform .35s cubic-bezier(.22,.61,.36,1)}
.anim .mag:hover{transition-duration:.12s}
.anim .mag[style*="--tx"]{transform:translate(var(--tx,0),var(--ty,0))}
/* Keep the press response. `.btn:active{transform:scale(.985)}` is a weaker
   selector than the magnetic transform above, so without this the tactile
   squash disappears on exactly the buttons people click most. */
.anim .mag:active{transform:translate(var(--tx,0),var(--ty,0)) scale(.985)}

/* --- 5. Shine sweep -----------------------------------------------------
   A single specular pass across a surface. Used on the "Most pick this" flag
   and the primary CTA. Decorative and pointer-transparent. */
.anim .shine{position:relative;overflow:hidden}
.anim .shine::after{content:"";position:absolute;top:0;left:-140%;
  width:60%;height:100%;pointer-events:none;
  background:linear-gradient(100deg,transparent,rgba(255,255,255,.55),transparent);
  animation:sweep 4.2s ease-in-out infinite}
@keyframes sweep{0%,62%{left:-140%}92%,100%{left:160%}}

/* --- 6. Gentle float ----------------------------------------------------
   Four pixels, eight seconds. Applied to the hero film so the page is never
   completely still, which is most of what separates "loading" from "alive". */
.anim .float{animation:bob 8s ease-in-out infinite}
@keyframes bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-6px)}}

/* --- 7. Drawn connector -------------------------------------------------
   The line threading the four numbered steps draws left to right as the
   section is read. Purely ornamental — the numbers stand on their own. */
.step-rail{position:relative}
/* Inset to the centre of the first and last step numbers rather than the full
   grid width, so it reads as a thread between them and not as a rule that ran
   off the end. 15px is half the 30px circle; 25% is one of four columns. */
.step-rail::before{content:"";position:absolute;left:15px;right:calc(25% - 15px);
  top:15px;height:2px;
  background:var(--rule);transform:scaleX(0);transform-origin:left;
  transition:transform 1.1s cubic-bezier(.22,.61,.36,1)}
.anim .step-rail::before{transform:scaleX(0)}
.step-rail.in::before,html:not(.anim) .step-rail::before{transform:scaleX(1)}
@media(max-width:900px){.step-rail::before{display:none}}

/* --- 8. Underline that draws on hover -----------------------------------
   Replaces the browser's instant underline on text links in prose. */
.anim .ul{background-image:linear-gradient(var(--accent),var(--accent));
  background-repeat:no-repeat;background-position:0 100%;background-size:0 1.5px;
  transition:background-size .3s cubic-bezier(.22,.61,.36,1);text-decoration:none}
.anim .ul:hover{background-size:100% 1.5px}

/* --- 9. Marquee pills ---------------------------------------------------
   The track's spans had no styling at all and rendered as bare words in a
   row. They are chips, matching .cov-list. */
.marquee-track span{font-size:.9rem;color:var(--fg-dim);background:var(--card);
  border:1px solid var(--rule);border-radius:var(--pill);padding:7px 14px;
  white-space:nowrap;flex:0 0 auto}

/* --- 10. Cross-document view transitions --------------------------------
   Two lines that upgrade every navigation on the site from a white flash to a
   cross-fade. This is a 41-page site where people move between trade pages,
   guides and city pages constantly, and the hard cut between them was the
   single most "static HTML" thing left about it.

   Progressive by construction: browsers without the API ignore the at-rule and
   navigate exactly as before. No JavaScript, no router, no framework — the
   browser does the whole thing off the main thread. */
@view-transition{navigation:auto}

/* Reduced-motion users get the navigation without the fade. */
@media (prefers-reduced-motion: reduce){
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*){animation:none!important}
}

/* --- 11. Animated disclosures -------------------------------------------
   The FAQ on every generated page snapped open. `::details-content` plus
   `interpolate-size` animates a <details> to its natural height with no
   wrapper element and no JavaScript — which matters because the FAQ markup is
   generated by seo.py and shared by 39 pages.

   Unsupported browsers ignore all of it and get the instant toggle they had. */
:root{interpolate-size:allow-keywords}
details::details-content{
  block-size:0;overflow:hidden;
  transition:block-size .34s cubic-bezier(.22,.61,.36,1),
             content-visibility .34s allow-discrete}
details[open]::details-content{block-size:auto}
@media (prefers-reduced-motion: reduce){
  details::details-content{transition:none}
}

/* --- 12. Section reveals for the generated pages ------------------------
   The 39 pages written by seo.py had no motion at all: they link this file, so
   they had the primitives, but nothing ever armed `.anim` and no element
   carried a reveal class.

   Rather than tag markup across five different page builders in seo.py — five
   chances to miss one — the reveal is declared structurally here. Every
   generated page is a series of `<section>`s wrapping a `.wrap`, so the
   section reveals and its direct children stagger. No markup changes, nothing
   for a future page type to forget, and the same watchdog behind it. */
   SCOPED TO `main.gen`, AND THAT IS LOad-BEARING. This was written as
   `.anim main > section`, which also matched builder.html — a hand-authored
   page with its own `.rv` driver that reveals `.rv` elements and nothing else.
   Its three sections were hidden by this rule and never revealed by anything.
   The form and the whole preview stage were invisible on the live site.

   The class is emitted by seo.py, in the same file that emits the driver, so
   the rule that hides content and the code that reveals it can never again be
   shipped apart. A page that does not opt in cannot be affected at all. My
   test harness force-added `.in` to every section before measuring, which is
   exactly why this passed six viewport checks and still shipped broken —
   never assert against state the test itself created.

   Trade, guide and city pages wrap their body in <article>, while the hub
   pages (trades, guides, areas, about) put sections straight under <main>.
   Both shapes are matched explicitly rather than with a descendant selector,
   because `main section` would also catch any future nested section and
   animate it twice. */
.anim main.gen>section,
.anim main.gen>article>section{opacity:0;transform:translateY(18px);
  transition:opacity .6s cubic-bezier(.22,.61,.36,1),
             transform .6s cubic-bezier(.22,.61,.36,1)}
.anim main.gen>section.in,
.anim main.gen>article>section.in{opacity:1;transform:none}
.anim main.gen>section>.wrap>*,
.anim main.gen>article>section>.wrap>*{opacity:0;transform:translateY(12px);
  transition:opacity .5s cubic-bezier(.22,.61,.36,1),
             transform .5s cubic-bezier(.22,.61,.36,1)}
.anim main.gen>section.in>.wrap>*,
.anim main.gen>article>section.in>.wrap>*{opacity:1;transform:none}
.anim main.gen>section.in>.wrap>*:nth-child(2),
.anim main.gen>article>section.in>.wrap>*:nth-child(2){transition-delay:.05s}
.anim main.gen>section.in>.wrap>*:nth-child(3),
.anim main.gen>article>section.in>.wrap>*:nth-child(3){transition-delay:.10s}
.anim main.gen>section.in>.wrap>*:nth-child(4),
.anim main.gen>article>section.in>.wrap>*:nth-child(4){transition-delay:.15s}
.anim main.gen>section.in>.wrap>*:nth-child(5),
.anim main.gen>article>section.in>.wrap>*:nth-child(5){transition-delay:.20s}
.anim main.gen>section.in>.wrap>*:nth-child(n+6),
.anim main.gen>article>section.in>.wrap>*:nth-child(n+6){transition-delay:.25s}


/* --- 13. Reduced motion -------------------------------------------------
   Belt as well as braces. `.anim` is already withheld from anyone with the OS
   setting on, but the setting can change after load and this catches that
   without a reload. */
@media (prefers-reduced-motion: reduce){
  .anim .reveal,.anim .seq>*,.anim .seq-fade>*,.anim .split .w>i,
  .anim main.gen>section,.anim main.gen>article>section,
  .anim main.gen>section>.wrap>*,.anim main.gen>article>section>.wrap>*{
    opacity:1!important;transform:none!important;transition:none!important}
  .anim .float,.anim .shine::after{animation:none}
  .anim .mag{transform:none!important}
  .step-rail::before{transform:scaleX(1)!important;transition:none}
}

/* ---- Looks showcase (homepage) -----------------------------------------
   Full-page screenshots in tall frames. The picture is the page, so it
   scrolls: slowly on hover or focus, back quickly when released, and on its
   own, gently, where there is no hover. Transform only, gated on .anim. */
.looks-sec{padding:96px 0}
.looks-sec-head{max-width:640px;margin-bottom:44px}
.looks-sec-head h2{font-size:clamp(2rem,4vw,3rem);letter-spacing:-.035em;line-height:1.05;margin:10px 0 16px}
.looks-sec-head p:last-child{color:var(--fg-dim)}
.looks-sec-grid{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(260px,calc((100% - 48px) / 3));gap:24px;
  overflow-x:auto;scroll-snap-type:x mandatory;padding:4px 2px 18px;scrollbar-width:thin}
.looks-sec-grid>*{scroll-snap-align:start}
@media(max-width:900px){.looks-sec-grid{grid-auto-columns:78%}}
.look-card{display:flex;flex-direction:column;gap:14px;text-decoration:none;color:inherit}
.look-frame{display:block;position:relative;aspect-ratio:3/4;overflow:hidden;border-radius:16px;background:var(--bg-alt);
  box-shadow:0 0 0 1px var(--rule),0 18px 50px rgba(0,0,0,.10);transition:box-shadow .3s ease}
.look-frame img{display:block;width:100%;height:auto;transition:transform 1.1s cubic-bezier(.22,.61,.36,1)}
.anim .look-card:hover .look-frame img,.anim .look-card:focus-visible .look-frame img{transform:translateY(var(--scroll));
  transition:transform 7s cubic-bezier(.45,.05,.55,.95)}
.look-card:hover .look-frame,.look-card:focus-visible .look-frame{box-shadow:0 0 0 2px var(--accent),0 24px 60px rgba(0,0,0,.14)}
.look-card:focus-visible{outline:none}
@media (hover:none){.anim .look-frame img{animation:lookDrift 16s ease-in-out infinite alternate}}
@keyframes lookDrift{0%,12%{transform:none}88%,100%{transform:translateY(var(--scroll))}}
.look-cap{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-inline:2px}
.look-cap strong{font-size:1.12rem;letter-spacing:-.02em}
.look-cap span{color:var(--fg-dim);font-size:.9rem}
.looks-sec-cta{display:flex;align-items:center;gap:18px;flex-wrap:wrap;margin-top:40px}
.looks-sec-cta span{color:var(--fg-dim);font-size:.9rem}
@media (prefers-reduced-motion: reduce){.look-frame img{animation:none!important;transition:none!important;transform:none!important}}
