/* ============================================================
   Scoutline — production stylesheet

   BRAND IDENTITY, REBUILT 2026-08-01. Everything below the token block
   is a reskin of the same component set, not a new site: class names,
   DOM hooks and behaviour are unchanged, because js/*.js binds to them.

   THE CANVAS IS NEAR-BLACK, AND THAT FOLLOWS FROM THE LOGO. The mark is
   now plain white (images/logo-mark.svg), and a white mark cannot sit on
   a white page — so black is the base, with a small number of sections
   deliberately INVERTED to bone (`section.invert`) so the scroll has a
   heartbeat instead of one flat tone. Two adjacent sections must never
   share a rung; that rule carries over from the graphite palette.

   The ladder is NEUTRAL now, not blue-grey. The old #0C1015 family was
   cool enough to read as "dashboard chrome"; a luxury-register black
   wants no hue at all, so depth comes from luminance only.

     --canvas   #08090A   the page itself
     --surface  #0D0E10   lifted bands, the header
     --raised   #131417   cards and tiles
     --raised-2 #1A1C1F   the hover state of anything --raised

   ONE ACCENT, AND IT IS ELECTRIC LIME. --acc #D7FF3E replaces the old
   the brand accent #D9A441 everywhere (the variables were RENAMED, not
   re-pointed, so nothing is left called --gold holding a green). Text on
   the accent is --acc-ink, never white: lime is a light colour and white
   on it is unreadable.

   COLOUR THAT IS NOT THE ACCENT ONLY EVER APPEARS AS LIGHT. The three
   --spec-* hues exist for blurred aurora washes and the 3D ribbon's
   gradient, i.e. things with no edges. They must never become a flat
   fill, a border or a text colour — the moment one does, the site has
   two brand colours and stops reading as monochrome. Status hues
   (--ok/--warn/--bad) are functional, not brand, and are exempt.

   Type — self-hosted from /fonts, latin subset only (~89KB for seven
   faces, down from 223KB for the old Chakra Petch / Geo / Aleo set):
     --font-display : Geist — h1, h2, h3, the brand wordmark. A neutral
                      geometric grotesk carrying the whole display voice
                      through size and tracking rather than through
                      personality. Set TIGHT: -0.035em at hero scale.
     --font-body    : Geist — paragraphs, buttons, inputs, nav.
     --font-mono    : Geist Mono — every technical micro-label on the
                      site: eyebrows, pills, gauge numerals, price
                      amounts, table data, the .mono class. This is the
                      single biggest carrier of the new identity, and it
                      is why --font-display was renamed to --font-mono
                      rather than re-pointed: 64 of its old uses were
                      small labels that always wanted a mono voice, and
                      only two were headlines.
     --font-serif   : Instrument Serif, ITALIC ONLY. Reserved for accent
                      words inside an otherwise grotesk headline
                      (`.ser`). One or two words per page. It is the only
                      thing on the site with any warmth, so spending it
                      freely is what would make it worthless.

   Radius is TWO-TIER on purpose: --r 6px on controls and chips (crisp,
   instrument-like) and --r-lg 20px on panels, cards and media (soft,
   premium). A single radius could do one or the other, not both.

   All motion stays gated behind prefers-reduced-motion.
   ============================================================ */

@font-face{
  font-family:'Geist';
  src:url('../fonts/Geist-400.woff2') format('woff2');
  font-weight:400; font-style:normal; font-display:swap;
}
@font-face{
  font-family:'Geist';
  src:url('../fonts/Geist-500.woff2') format('woff2');
  font-weight:500; font-style:normal; font-display:swap;
}
@font-face{
  font-family:'Geist';
  src:url('../fonts/Geist-600.woff2') format('woff2');
  font-weight:600; font-style:normal; font-display:swap;
}
@font-face{
  font-family:'Geist';
  src:url('../fonts/Geist-700.woff2') format('woff2');
  font-weight:700; font-style:normal; font-display:swap;
}
@font-face{
  font-family:'Geist Mono';
  src:url('../fonts/GeistMono-400.woff2') format('woff2');
  font-weight:400; font-style:normal; font-display:swap;
}
@font-face{
  font-family:'Geist Mono';
  src:url('../fonts/GeistMono-500.woff2') format('woff2');
  font-weight:500; font-style:normal; font-display:swap;
}
/* Italic only — there is no upright cut loaded, deliberately. See the
   --font-serif note above. */
@font-face{
  font-family:'Instrument Serif';
  src:url('../fonts/InstrumentSerif-Italic.woff2') format('woff2');
  font-weight:400; font-style:italic; font-display:swap;
}

:root{
  /* ==== TELL THE BROWSER THIS SITE IS DARK, added 2026-07-30 ==========
     Everything the USER-AGENT draws rather than us — scrollbars, the popup
     list a <select> opens, the accounts page's date picker, number-input
     spinners, form-control focus rings — was rendering in LIGHT mode on
     every page, because this had never been declared. Still true, and
     still one declaration for the whole site. The one deliberately-light
     element (the browser mock on sample-output.html, which depicts a real
     generated customer site) contains no native controls at all. */
  color-scheme: dark;

  /* ---- the ladder, darkest to lightest. Neutral: no hue, only
     luminance. A new surface should pick a rung, not invent a shade. */
  --canvas:#08090A;
  --surface:#0D0E10;
  --raised:#131417;
  --raised-2:#1A1C1F;

  /* ---- text, three steps of emphasis ---- */
  --text:#F4F5F7;
  --dim:#8B9096;           /* secondary copy                — 6.2:1     */
  /* Raised from #5A5E64, which measured 3.06:1 against the footer and
     failed AA for small text. --faint carries captions, legends and
     disclaimers — including the footer's "AI-generated previews,
     honestly labelled" and the illustrative-scan caption. Those are the
     lines this project's honesty rule makes non-negotiable, so they are
     the last text on the site that should be hard to read. */
  /* Measured on the LIGHTEST rung it ever lands on (--raised-2), not on
     --canvas — a caption inside a hovered card is the worst case, and
     tuning against the darkest background is how you ship a value that
     passes in one place and fails in three others. */
  --faint:#80858C;         /* captions, legends, disclaimers — 4.9:1+   */

  /* Micro-captions that sit OVER the spectral wash, added 2026-08-04 when
     .aurora went 0.28 -> 0.62. The wash is a saturated near-black violet,
     so it barely moves a surface's luminance but it does move its hue and
     chroma, and 9-10px grey mono on top of it loses separation fast.
     MEASURED, not chosen: sampling the real rendered pixels behind each
     run put .meta-corners at 3.12:1 and the eyebrow's figure at 3.98:1
     against a 4.5 requirement. This value clears every one of them at
     5.2:1 over the worst pixel the wash produces.
     Only for text over the wash — --faint remains correct everywhere
     else, and making this the global caption colour would brighten
     captions on plain surfaces that never had a problem. */
  --text-on-wash:#A8ADB5;

  /* ---- the one accent ---- */
  --acc:#D7FF3E;
  --acc-ink:#0B0F02;       /* text ON the accent. Never white.          */
  --acc-soft:rgba(215,255,62,0.12);
  --acc-line:rgba(215,255,62,0.32);
  --acc-glow:rgba(215,255,62,0.50);

  /* ---- light, not paint. Aurora washes and the ribbon gradient only.
     See the header: these must never become a flat fill or a text
     colour. */
  --spec-1:#6E5BFF;        /* violet */
  --spec-2:#2BB8FF;        /* cyan   */
  --spec-3:#FF5A2B;        /* orange */

  /* ---- functional status, exempt from the one-accent rule ---- */
  --ok:#7FD1A8;
  --warn:#FFC24B;
  --bad:#F08A78;

  --line:rgba(244,245,247,0.08);
  --line-strong:rgba(244,245,247,0.16);

  --r:6px;                 /* controls, chips, inputs                   */
  --r-lg:20px;             /* panels, cards, media                      */
  --radius:6px;            /* legacy alias — older rules still use it   */
  --r-pill:999px;          /* buttons, and ONLY buttons — see below     */

  /* ---- the content well, widened 2026-08-04 ---------------------------
     Was a hardcoded 1180/820 with a flat 26px gutter. "Narrower margins"
     is the ask, and the way you get them is a WIDER well: the outer
     margin shrinks, the column grows, and the same copy has more room
     around it. The gutter became fluid at the same time so the gain
     doesn't come out of the phone's edge padding, where there is none to
     spare.

     THESE ARE TOKENS, NOT LITERALS, because at least four rules used to
     repeat 1180 by hand and one of them (.hero .wrap) carries a measured
     constraint against the hero mark that has to move with it. */
  --well:1340px;
  --well-narrow:940px;
  /* Floor is 26px — the ORIGINAL flat gutter — so this change is purely
     additive: nothing below ~765px viewport gets less edge padding than
     it had before, and wide screens get up to 54px. A clamp() floor of
     20px would have quietly taken 6px off every phone, which is the one
     place there is no room to give. */
  --gutter:clamp(26px, 3.4vw, 54px);

  --font-display:'Geist',system-ui,-apple-system,'Segoe UI',sans-serif;
  --font-body:'Geist',system-ui,-apple-system,'Segoe UI',sans-serif;
  --font-mono:'Geist Mono',ui-monospace,SFMono-Regular,Menlo,monospace;
  --font-serif:'Instrument Serif',Georgia,'Times New Roman',serif;

  /* Measured at runtime by js/main.js — never hardcode these, the header
     grows/shrinks with font loading and viewport width. */
  --header-h:62px;
}

/* ==== INVERTED SECTIONS =============================================
   `section.invert` flips the whole token ladder to bone-white for one
   band. Because every component reads the ladder through variables
   rather than through hard-coded hex, a card, a pill, a hairline and a
   button all re-theme with no per-component override — which is the
   entire reason the ladder is variables in the first place.

   The accent does NOT survive the flip unchanged: lime on bone is about
   1.4:1 and unreadable as text, so --acc darkens to --acc-ink-level
   olive for text while the FILL stays electric (a lime button on bone
   still has black text and still reads correctly). */
section.invert{
  --canvas:#F2F1ED;
  --surface:#EAE9E4;
  --raised:#FFFFFF;
  --raised-2:#F7F6F2;
  --text:#0A0B0D;
  --dim:#55585E;
  /* NOT the mirror of the dark --faint. #83878E measured 3.19:1 on this
     ground — under AA for small text, and the thing it colours on the
     homepage is the "Illustrative scan — example businesses, not live
     data" caption. Under this project's own honesty rule that caption is
     not optional, so it cannot be the least legible text on the page.
     #61656C measures ~5.1:1. Checked, not guessed. */
  --faint:#61656C;
  --line:rgba(10,11,13,0.10);
  --line-strong:rgba(10,11,13,0.20);
  --acc-soft:rgba(97,124,10,0.12);
  --acc-line:rgba(97,124,10,0.30);
  --acc-glow:rgba(97,124,10,0.28);
  --acc-text:#4E6605;      /* lime is unreadable on bone — see above    */
  background:var(--canvas);
  color:var(--text);
}
:root{ --acc-text:var(--acc); }

*{ box-sizing:border-box; }
html{ scroll-behavior:smooth; }
/* `clip`, NOT `hidden`. overflow-x:hidden on html/body makes it a scroll
   container, which silently breaks every position:sticky descendant
   (the header, the how-it-works sub-nav). `clip` contains stray
   horizontal overflow without creating one. */
html{ overflow-x:clip; }
body{
  margin:0;
  background:var(--canvas);
  color:var(--text);
  font-family:var(--font-body);
  font-size:1rem;
  line-height:1.6;
  letter-spacing:-0.005em;
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
  overflow-x:clip;
}

/* ---- grain -------------------------------------------------------
   A fixed, non-interactive film of noise over the whole page. On a
   near-black canvas, large flat areas and wide gradients band visibly on
   8-bit displays; a couple of percent of noise breaks the banding up and
   is most of what separates "black background" from "expensive black
   background". Inline SVG turbulence so it costs no request, and
   pointer-events:none so it can never eat a click. */
body::after{
  content:"";
  position:fixed; inset:0; z-index:9999;
  pointer-events:none; opacity:0.035;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ---- type --------------------------------------------------------
   Sizes are fluid via clamp() rather than stepped at breakpoints, so a
   headline never lands at an awkward size between two media queries.
   Tracking tightens as size grows — that relationship is what makes a
   neutral grotesk read as display type rather than as big body copy. */
h1,h2,h3,h4,.brand{ font-family:var(--font-display); }
h1{
  font-size:clamp(2.5rem,6.6vw,5.4rem);
  font-weight:600; letter-spacing:-0.038em; line-height:0.98;
  margin:0 0 18px;
}
h2{
  font-size:clamp(1.85rem,3.6vw,3rem);
  font-weight:600; letter-spacing:-0.032em; line-height:1.06;
  margin:0 0 14px;
}
h3{
  font-size:1.12rem; font-weight:600;
  letter-spacing:-0.018em; line-height:1.3;
}
h4{
  font-size:0.98rem; font-weight:600;
  letter-spacing:-0.014em; line-height:1.3;
}
/* The one warm thing on the site. Italic serif inside a grotesk
   headline, one or two words per page — see the --font-serif note. */
.ser{
  font-family:var(--font-serif); font-style:italic; font-weight:400;
  letter-spacing:-0.01em;
  /* An italic serif at the same font-size as a grotesk optically reads
     smaller, because its x-height is lower. 1.06em restores the match. */
  font-size:1.06em;
}
.mono{ font-family:var(--font-mono); font-weight:400; letter-spacing:-0.02em; }
a{ color:inherit; }
::selection{ background:var(--acc); color:var(--acc-ink); }

/* Focus is one visible ring everywhere, on the accent, and it is never
   removed — only replaced. :focus-visible so a mouse click doesn't draw
   it but a Tab key does. */
:focus-visible{
  outline:2px solid var(--acc);
  outline-offset:3px;
  border-radius:2px;
}

/* ==== THE `hidden` ATTRIBUTE ACTUALLY HIDES THINGS ===================
   ONE RULE, GLOBAL, AND IT REPLACES FIVE PIECEMEAL PATCHES.

   `hidden` is enforced by the USER AGENT stylesheet, as `[hidden]{
   display:none }`. Author CSS beats user-agent CSS in the cascade no
   matter what the specificity looks like — origin is compared before
   specificity ever is. So ANY author rule that sets `display` on an
   element silently defeats `hidden` on that element, and `.prof-groups`,
   `.prof-cat-body`, `.sheet-backdrop`, `.side-act.bug` and `.fs-more-body`
   each hit it and each got their own one-line override further down.

   THE ONE THAT WASN'T PATCHED WAS THE ADMIN LINK, and it mattered most.
   `.side-nav a{ display:flex }` beat `[hidden]` on the Accounts link in
   the workspace sidebar, so the `hidden` attribute the markup carries did
   nothing: measured at 855x45 and fully painted. Every non-admin loaded
   every app page with a real, visible "Accounts" link until
   ScoutlineAuth.me() came back and js/app.js removed it — five static
   pages with no shared shell means that round trip happens fresh on every
   single navigation, so it was not one flash, it was every click.

   `!important` is deliberate and is the point: this has to outrank a
   `display` set by any component rule anywhere below, including ones not
   written yet. An element that must be laid out should not be carrying
   `hidden`. */
[hidden]{ display:none !important; }

.wrap{ max-width:var(--well); margin:0 auto; padding:0 var(--gutter); }
.wrap-narrow{ max-width:var(--well-narrow); margin:0 auto; padding:0 var(--gutter); }
.visually-hidden{
  position:absolute; width:1px; height:1px; margin:-1px; padding:0;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
}

/* ==== MICRO-LABEL ===================================================
   The workhorse of the new identity, lifted from the Inai and Pulsar
   instrument panels: tiny uppercase mono, widely tracked, --dim. It
   labels a section, a stat, a panel, a table column. `.lbl` is the bare
   type treatment; `.eyebrow` (further down, unchanged in name because
   the markup uses it everywhere) is this plus a leading accent dot. */
.lbl{
  font-family:var(--font-mono); font-size:0.66rem; font-weight:500;
  text-transform:uppercase; letter-spacing:0.15em;
  color:var(--dim); line-height:1;
}
.lbl-acc{ color:var(--acc-text); }

/* ==== HAIRLINE COLUMN GRID ==========================================
   Faint full-height vertical rules behind a section — the "blueprint"
   texture from the Inai dashboard, and an unusually literal fit for a
   product called Scoutline. Decorative only: it is a pseudo-element on a
   wrapper, never real columns, so it can't affect layout.

   repeating-linear-gradient rather than N absolutely-positioned divs:
   one paint, no DOM, and the column count follows the width for free. */
.ruled{ position:relative; isolation:isolate; }
.ruled::before{
  content:""; position:absolute; inset:0; z-index:-1; pointer-events:none;
  background:repeating-linear-gradient(
    to right,
    var(--line) 0 1px,
    transparent 1px calc(100% / 6)
  );
  /* Fade the rules out top and bottom so they don't collide with the
     section edges as hard lines. */
  -webkit-mask-image:linear-gradient(to bottom,transparent,#000 12%,#000 88%,transparent);
          mask-image:linear-gradient(to bottom,transparent,#000 12%,#000 88%,transparent);
  opacity:0.9;
}
@media (max-width:700px){
  .ruled::before{ background-size:100% 100%;
    background:repeating-linear-gradient(to right,var(--line) 0 1px,transparent 1px 33.333%); }
}

/* ==== AURORA ========================================================
   Blurred spectral light behind a section. This is the ONLY place the
   --spec-* hues are allowed, and they only ever appear this far out of
   focus — see the token header. Sits at z-index -1 inside an isolated
   parent so it can never paint over content or catch a pointer. */
/* STRENGTHENED 2026-08-04. Was opacity 0.28 / blur(90px), which on the
   deployed site read as a faint stain rather than light. Now 0.62 with a
   tighter blur and a touch of saturation, which is what actually makes it
   look like coloured light rather than a grey smudge.

   The rule it does NOT break: these are still --spec-* hues, still only
   ever this far out of focus, still never a fill, a border or a text
   colour. --acc remains the only accent. Turning the wash up is a change
   of intensity, not of the one-accent rule.

   CONTRAST IS THE CONSTRAINT ON HOW FAR THIS CAN GO. --dim is documented
   at 6.2:1 and --faint at 4.9:1, and both were MEASURED against the
   surfaces they land on. A wash under them changes that measurement, so
   these numbers were re-measured over the brightest point of the wash
   after the change, not assumed. --faint carries the honesty captions,
   which are the last text on this site allowed to get hard to read. */
.aurora{
  position:absolute; inset:-20% -10% auto; height:70%;
  z-index:-1; pointer-events:none;
  /* NO CSS BLUR, AND THAT IS A PERFORMANCE FIX, NOT A LOOK CHANGE.
     Measured 2026-08-04 at 4x CPU throttle while scrolling the homepage:

       filter:blur(66px)            mean frame 308ms
       filter:blur(24px)            mean frame 195ms
       no filter                    mean frame 149ms
       no filter + promoted layer   mean frame 130ms

     A large blur re-rasterises a full-viewport layer on every scroll
     frame, and its cost scales with the radius. The gradients underneath
     are ALREADY soft — `transparent 70%` stops have no hard edge to
     smooth — so the blur was buying very little and costing more than
     everything else on the page combined. Alphas are lifted slightly to
     put back the bloom the blur was contributing.

     translateZ(0) promotes this to its own compositor layer so a scroll
     moves it instead of repainting it. Worth the memory here because it
     is one layer covering one viewport, not a per-card promotion. */
  opacity:0.88; transform:translateZ(0);
  background:
    radial-gradient(38% 60% at 18% 40%, var(--spec-1), transparent 70%),
    radial-gradient(34% 55% at 62% 30%, var(--spec-2), transparent 70%),
    /* The orange pool is held at half alpha rather than the full token.
       Un-blurred it became the brightest thing on the page, and it is the
       one the corner captions sit on — measured 3.2:1 against a 4.5
       requirement. Dimming the WHOLE wash to fix one pool would have cost
       the brightness everywhere for a problem in one corner. */
    radial-gradient(34% 54% at 88% 62%, rgba(255,90,43,0.5), transparent 72%);
  /* TWO masks, intersected.
     HORIZONTAL: held back on the left, where the copy lives; full strength
     on the right, where the hero mark is and there is nothing to read.
     This is what lets the wash be genuinely bright without the text paying
     for it — the alternative was dimming the whole thing, which measured
     worse AND looked worse. The eyebrow's figure is fixed by this alone.
     VERTICAL: two jobs now. The bottom fade stops the element's own hard
     `height:70%` edge showing (see below). The TOP fade was added
     2026-08-04 when the CSS blur came off for performance: without the
     blur every pool is far more concentrated — the blur had been
     spreading and diluting it — and the peak moved up under the nav,
     putting several nav links below AA at 3.9-4.4:1. Holding the wash
     back across the top 20% puts them back over 4.5 while leaving the
     hero body at full strength, which is where the colour is actually
     wanted.
     At the old 0.28 that edge was invisible; at 0.62 it rendered as a
     straight horizontal seam across the hero, clearly visible in the
     orange pool on the right. Blur alone does not hide it, because the
     blur is applied INSIDE the box and the box still ends where it ends.
     Fading the mask out before that boundary is what removes it. */
  -webkit-mask-image:
    linear-gradient(90deg,rgba(0,0,0,.28) 0%,rgba(0,0,0,.62) 30%,#000 58%),
    linear-gradient(180deg,rgba(0,0,0,.30) 0%,#000 20%,#000 44%,transparent 97%);
  -webkit-mask-composite:source-in;
  mask-image:
    linear-gradient(90deg,rgba(0,0,0,.28) 0%,rgba(0,0,0,.62) 30%,#000 58%),
    linear-gradient(180deg,rgba(0,0,0,.30) 0%,#000 20%,#000 44%,transparent 97%);
  mask-composite:intersect;
}
@media (max-width:700px){ .aurora{ opacity:0.74; } }
/* A large blurred layer is a genuinely expensive paint; there is no
   reason to keep compositing it at full strength for someone who asked
   for less motion, and its whole job is atmosphere. Reduced, not removed —
   removing it entirely would leave those pages visibly flatter than the
   design intends, which is a different experience, not a calmer one. */
@media (prefers-reduced-motion:reduce){ .aurora{ opacity:0.55; } }

/* ==== PAGE WASH =====================================================
   The aurora is per-section and only mounted on a few. This is the
   whole-document version: two very low-alpha spectral pools fixed to the
   viewport so every page carries a little coloured light, including the
   long text pages (privacy, terms, guides) that have no aurora at all and
   read as flat black without it.

   `fixed` and z-index:-1 on a pseudo of <body>, so it costs one layer for
   the whole document and can never catch a pointer or paint over content.
   Alpha is deliberately an order of magnitude below .aurora — this is the
   floor of the effect, not the effect itself. */
body::before{
  content:""; position:fixed; inset:0; z-index:-1; pointer-events:none;
  /* Promoted for the same reason as .aurora above: a fixed, full-viewport
     painted layer otherwise repaints on every scroll frame. */
  transform:translateZ(0);
  background:
    radial-gradient(58% 46% at 74% -12%, rgba(110,91,255,0.135), transparent 70%),
    radial-gradient(52% 42% at 20% -10%, rgba(43,184,255,0.10), transparent 68%),
    radial-gradient(50% 40% at 2% 26%, rgba(43,184,255,0.085), transparent 66%),
    radial-gradient(46% 46% at 94% 80%, rgba(255,90,43,0.07), transparent 66%);
}
@media (max-width:700px){
  body::before{ background:
    radial-gradient(110% 30% at 76% -8%, rgba(110,91,255,0.13), transparent 68%),
    radial-gradient(110% 28% at 14% -6%, rgba(43,184,255,0.095), transparent 66%),
    radial-gradient(100% 28% at 2% 22%, rgba(43,184,255,0.07), transparent 66%); }
}

/* ==== CORNER META ===================================================
   The four-corner technical caption from Optikka and Pulsar: tiny mono
   facts pinned to the corners of a section, connected to nothing,
   stating something true. Purely typographic; no box, no rule.
   aria-hidden in the markup where it repeats content already on screen. */
.meta-corners{ position:absolute; inset:0; pointer-events:none; z-index:2; }
.meta-corners span{
  position:absolute;
  font-family:var(--font-mono); font-size:0.62rem; font-weight:400;
  letter-spacing:0.12em; text-transform:uppercase;
  /* --text-on-wash, not --faint: these are pinned to the corners of the
     exact sections that carry the aurora. See the token's note. */
  color:var(--text-on-wash); line-height:1.5; max-width:38%;
}
/* ==== GLASS =========================================================
   The frosted panel from the "ai-powered editing" reference: a barely
   tinted surface, a hairline edge, and the section behind it blurred
   through it.

   BACKDROP-FILTER ONLY EARNS ITS COST WHERE THERE IS SOMETHING BEHIND
   IT TO BLUR. On a flat --canvas section it produces a slightly lighter
   rectangle for a real per-frame compositing cost, which is why this is
   a named utility applied deliberately rather than a default on every
   card. Use it over the aurora, over the starfield, or over an image —
   nowhere else.

   The inset top highlight is what sells it as glass rather than as a
   translucent box: real glass catches light on its top edge. */
.glass{
  background:rgba(244,245,247,0.045);
  -webkit-backdrop-filter:blur(16px) saturate(150%);
          backdrop-filter:blur(16px) saturate(150%);
  border:1px solid rgba(244,245,247,0.11);
  box-shadow:inset 0 1px 0 rgba(244,245,247,0.10),
             0 24px 60px -34px rgba(0,0,0,0.95);
}
/* Safari and Firefox both ship backdrop-filter now, but if it is ever
   unsupported the panel would be a near-invisible 4.5% tint over the
   section. Opaque fallback so it is always a readable surface. */
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .glass{ background:var(--raised); }
}

/* ==== CROSSHAIRS ====================================================
   The thin registration marks pinned to the corners of a panel in the
   "ai-powered editing" reference — the detail that makes a dark layout
   read as an instrument rather than as a slide. Pure CSS: each <i> is a
   box with a 1px vertical and a 1px horizontal rule through its middle,
   so there is no icon font, no SVG and no request.

   Decorative and inert: aria-hidden in the markup, pointer-events:none
   here, and it sits behind content at z-index 0 so it can never take a
   click from a link. */
.xhairs{ position:absolute; inset:clamp(18px,3vw,42px); pointer-events:none; z-index:0; }
.xhairs i{ position:absolute; width:11px; height:11px; opacity:0.85; }
.xhairs i::before,
.xhairs i::after{ content:""; position:absolute; background:var(--line-strong); }
.xhairs i::before{ left:50%; top:0; bottom:0; width:1px; transform:translateX(-0.5px); }
.xhairs i::after{ top:50%; left:0; right:0; height:1px; transform:translateY(-0.5px); }
.xhairs i:nth-child(1){ top:0; left:0; }
.xhairs i:nth-child(2){ top:0; right:0; }
.xhairs i:nth-child(3){ bottom:0; left:0; }
.xhairs i:nth-child(4){ bottom:0; right:0; }
/* Below the point where the content column reaches the viewport edges
   they would sit on top of text rather than outside it. */
@media (max-width:1000px){ .xhairs{ display:none; } }

/* Reverted to a plain 18px offset on 2026-08-05. It was briefly offset by
   `--header-h` to clear the floating dock, which was right for the hero
   and WRONG for how-it-works, where `.meta-corners` is pinned inside the
   ribbon figure rather than to the top of the page — it pushed "Fig. 01"
   78px down inside its own box. The hero no longer uses this component at
   all (see `.hero-topline`), so the offset had nothing left to fix. */
/* ---- the hero topline ----------------------------------------------
   The eyebrow and the one remaining meta label share a row, the label
   pushed right. Replaces the two absolutely-positioned corner labels the
   hero used to carry: with a floating dock, anything pinned to the top
   corners of a full-bleed section sits underneath it.

   `align-items:baseline` rather than centre, so the label sits on the
   eyebrow's own baseline even though the eyebrow is taller (it carries
   the accent dot). Wraps below 720px, where the label is dropped anyway. */
/* FULL WIDTH, against `.hero .wrap > *{ max-width:min(48%,600px) }`.
   That cap keeps the headline and body copy to a readable measure on the
   left half of the hero, which is right for them and wrong for this row:
   capped, the label landed at 688px — the middle of the page — instead of
   at its right edge. This row holds no prose, so the measure does not
   apply to it. */
/* `.hero .wrap > .hero-topline`, NOT `.hero-topline` — the cap it has to
   beat is `.hero .wrap > *`, which is (0,2,0); a bare class is (0,1,0) and
   silently loses. Written the short way first and measured at 596px, the
   capped width, with the label wrapped onto a second line. */
.hero .wrap > .hero-topline{
  display:flex; align-items:baseline; justify-content:space-between;
  gap:20px; flex-wrap:nowrap; margin-bottom:6px;
  max-width:none; width:100%;
}
/* TWO LINES, HARD RIGHT — and that is a collision fix, not a preference
   (2026-08-05). On one line at `nowrap` this measured 229px, and the 3D
   mark behind it is `min(58vw,860px)` pinned to the right edge: at 1440
   the label ran x=1112→1341 and the mark's box is 763→1599, so the label
   sat squarely over the brightest part of the artwork — the dense
   vertical spine of dots reads straight through the type. Legibility was
   never the problem (the copy column is z-index:1 over the mark's 0); it
   simply looked like two things fighting for the same corner.

   Wrapping to a ~2-line column pulls the left edge in by ~100px, clear of
   the spine, and keeps the label where it belongs — tight to the right
   margin, sharing the eyebrow's baseline. `balance` splits it evenly
   rather than leaving one word stranded on line two.

   The height change is absorbed by the flex row, which is
   `align-items:baseline`, so the eyebrow beside it does not move. */
.hero-meta{
  font-family:var(--font-mono); font-size:0.62rem; font-weight:400;
  letter-spacing:0.12em; text-transform:uppercase;
  color:var(--text-on-wash); text-align:right;
  /* 132px, NOT a `ch` value — `ch` is the font's own advance width and
     knows nothing about `letter-spacing`, so with 0.12em of tracking on
     every character a `15ch` cap fits about eight of them. Measured: it
     wrapped to FOUR lines at 89px wide. The one-line width was 229px for
     31 characters (~7.4px each), so a two-line break wants room for the
     longer half, "Lead discovery &" at 16 characters ≈ 118px. 132px
     clears that and still refuses a third word on either line.
     If the wording changes, re-measure — this number is tied to it. */
  white-space:normal; max-width:132px; text-wrap:balance;
  line-height:1.5;
}
/* Decorative, and the first thing worth losing when the row gets tight —
   the eyebrow beside it carries a real figure and must keep the width. */
@media (max-width:760px){ .hero-meta{ display:none; } }

.meta-corners .tl{ top:18px; left:26px; }
.meta-corners .tr{ top:18px; right:26px; text-align:right; }
.meta-corners .bl{ bottom:18px; left:26px; }
.meta-corners .br{ bottom:18px; right:26px; text-align:right; }
@media (max-width:900px){ .meta-corners{ display:none; } }

/* ==== VERTICAL_STRIP ================================================
   Reusable component: two accent marquees framing the viewport, black
   text, scrolling down, mirrored. Built and mounted by js/vertical-
   strip.js — the phrase lives in ONE constant at the top of that file.

   ORIENTATION. On both sides the baseline faces the centre of the
   screen, which means the two sides necessarily read in opposite
   vertical directions (left bottom-to-top, right top-to-bottom). That
   IS the mirror: a reflection reverses reading direction.

   MIRROR ALIGNMENT. Phrase boundaries land at the same screen-y on both
   sides at every moment, so the strips read as one continuous frame.
   That only works because neither rail's transform depends on viewport
   height. Deriving it: with the left mapped to screenY = -(u + dL) and
   the right to screenY = u + dR (u = distance along the rail), phrase
   boundaries coincide iff dL + dR is a whole number of phrase-lengths.
   The keyframes below run dL from -50%..-100% and dR from -50%..0, so
   dL + dR is a constant -100% = one full run = a whole number of
   phrases, at every t. Hence: aligned always, on any screen.

   An earlier version anchored the left rail with translateY(100vh),
   which put viewport height into that sum and broke the alignment on
   every screen except a few exact heights. Don't reintroduce a vh term
   in either rail transform.

   REDUCED MOTION: base transforms live on the rail itself, not only in
   the keyframes, so switching the animation off leaves the text
   correctly oriented and still covering the screen.

   Only shown at >=1220px, where there's room outside the 1120px content
   column — never overlapping content, never on mobile. */
:root{ --vstrip-w:34px; }
.vstrip{
  position:fixed; top:0; bottom:0; width:var(--vstrip-w);
  background:var(--acc); overflow:hidden;
  z-index:45; pointer-events:none; display:none;
}
.vstrip-left{ left:0; box-shadow:inset -1px 0 0 rgba(12,16,21,0.22); }
.vstrip-right{ right:0; box-shadow:inset 1px 0 0 rgba(12,16,21,0.22); }
.vstrip-rail{
  position:absolute; top:0; left:0;
  height:var(--vstrip-w); width:max-content;
  display:flex; align-items:center; white-space:nowrap;
  transform-origin:0 0;
}
.vstrip-left  .vstrip-rail{ transform:rotate(-90deg) translateX(-50%); }
.vstrip-right .vstrip-rail{ transform:translateX(var(--vstrip-w)) rotate(90deg) translateX(-50%); }
.vstrip-run{
  display:block; flex:none;
  font-family:var(--font-body); font-size:0.68rem; font-weight:700;
  letter-spacing:0.16em; text-transform:uppercase;
  color:var(--canvas); line-height:var(--vstrip-w);
}
.vstrip-run .sep{ opacity:0.45; padding:0 3px; }
/* Travel is exactly one run per loop, and the two runs in each rail are
   identical, so the loop is seamless. */
@keyframes vstrip-scroll-left{
  from{ transform:rotate(-90deg) translateX(-50%); }
  to  { transform:rotate(-90deg) translateX(-100%); }
}
@keyframes vstrip-scroll-right{
  from{ transform:translateX(var(--vstrip-w)) rotate(90deg) translateX(-50%); }
  to  { transform:translateX(var(--vstrip-w)) rotate(90deg) translateX(0); }
}
@media (min-width:1220px){
  .vstrip{ display:block; }
  .vstrip-left  .vstrip-rail{ animation:vstrip-scroll-left  92s linear infinite; }
  .vstrip-right .vstrip-rail{ animation:vstrip-scroll-right 92s linear infinite; }
  .to-top{ right:calc(var(--vstrip-w) + 20px); }  /* clear the right strip */
}
@media (prefers-reduced-motion:reduce){
  .vstrip-left .vstrip-rail, .vstrip-right .vstrip-rail{ animation:none; }
}
/* ==== end VERTICAL_STRIP ============================================ */

/* ==== EDGE_LOOP =====================================================
   ONE accent ribbon around the WHOLE DOCUMENT — top of the page, down
   both sides past everything on it, and back along the bottom under the
   footer — with the words curling around all four corners. Built and
   sized by js/edge-loop.js; the phrase lives in ONE constant at the top
   of that file. Nothing here does anything without that script: the
   frame is created by it, and the layout rules are gated on the
   `has-eloop` class it puts on <body>.

   REBUILT 2026-08-01 FROM FOUR ROTATED RAILS TO ONE SVG PATH. The old
   version framed the VIEWPORT with four separately-animated strips, and
   most of the block that used to be here was the geometry proving those
   four travelled at one speed and never opened a seam. None of it
   survives, because none of it is needed: there is now one path, stroked
   for the band and followed by a single <textPath> for the words, so
   there is no second thing to keep in step and no corner to seam. See
   js/edge-loop.js for why four rails could not curl a corner.

   WHY IT IS BEHIND THE HEADER (z-index 41). The frame is absolutely
   positioned in the DOCUMENT now, so it scrolls away rather than sitting
   over the window. The sticky header must pass IN FRONT of the top band
   on the way past — at the old z-index of 60 the ribbon painted over the
   nav on every scroll. 41 clears ordinary page content and stays under
   the header (48), the drawer, the toasts (200) and the page-transition
   veil (9000).

   SPACING. The document is inset top and bottom by one band width so
   nothing lands underneath it. Left and right need no inset: this only
   renders at >=1280px, where the 1180px content column already leaves
   50px of clearance each side. The header no longer needs its own offset
   — it is inside the padded body, and once it sticks the ribbon is
   behind it.

   Only shown at >=1280px — never overlapping content, never on mobile,
   never on the app pages. The min-width here and MQ in js/edge-loop.js
   must stay in step. */
:root{
  --eloop-w:34px;   /* must match BAND in js/edge-loop.js */
}
.eloop-frame{
  position:absolute; top:0; left:0; width:100%;
  z-index:41; pointer-events:none;
  display:none;
  /* Height is set in px by the script from the document's scrollHeight.
     `height:100%` would resolve against <body>, which is not the same
     number and is wrong by exactly whatever the margins are. */
}
.eloop-frame svg{ display:block; }
.eloop-band{
  fill:none; stroke:var(--acc); stroke-width:var(--eloop-w);
  stroke-linejoin:round;
}
.eloop-text{
  fill:var(--acc-ink);
  font-family:var(--font-body); font-size:0.68rem; font-weight:700;
  letter-spacing:0.16em; text-transform:uppercase;
}

@media (min-width:1280px){
  .eloop-frame{ display:block; }
  /* Inset the document so nothing lands under the ribbon. The frame is
     absolutely positioned and adds no height of its own, which is what
     lets the script measure scrollHeight without chasing its own tail. */
  body.has-eloop{ padding-top:var(--eloop-w); padding-bottom:var(--eloop-w); }
  /* The back-to-top button is fixed to the VIEWPORT, so at the bottom of
     the page it lands on the corner of the ribbon. Both offsets stay. */
  .has-eloop .to-top{
    right:calc(var(--eloop-w) + 20px);
    bottom:calc(var(--eloop-w) + 20px);
  }
  /* THE CORNER META IS THE ONE THING NOT IN THE CONTENT COLUMN. The
     1180px column has 50px of clearance each side at this breakpoint, so
     nothing in it can reach the ribbon — but `.meta-corners` is pinned to
     the SECTION's own edges at 26px, which is inside a 34px band. Found
     by looking: the hero's "Scoutline — UK" lost its S behind the strip.
     Pushed clear rather than hidden; it is a real caption, not filler. */
  .has-eloop .meta-corners .tl,
  .has-eloop .meta-corners .bl{ left:calc(var(--eloop-w) + 18px); }
  .has-eloop .meta-corners .tr,
  .has-eloop .meta-corners .br{ right:calc(var(--eloop-w) + 18px); }
}
/* No `animation` to switch off — the travel is a SMIL <animate> element,
   and js/edge-loop.js simply doesn't create it under reduced motion. The
   ribbon is then a static frame, which is a correct one. */
/* ==== end EDGE_LOOP ================================================= */

/* ---- intro sequence (homepage, once per session) ------------------
   The overlay is solid so nothing shows through mid-animation, and the
   page underneath is scroll-locked for its ~2.4s. js/intro.js removes
   both; if it never runs, none of this applies and the page is normal. */
.intro-overlay{
  position:fixed; inset:0; z-index:200;
  background:
    radial-gradient(900px 600px at 50% 42%, rgba(35,45,58,0.75), transparent 70%),
    var(--canvas);
  opacity:1; transition:opacity .38s ease;
}
.intro-overlay canvas{ display:block; width:100%; height:100%; }
.intro-skip{
  position:absolute; right:20px; bottom:20px;
  background:none; border:1px solid var(--line); border-radius:999px;
  color:var(--dim); font-family:var(--font-body);
  font-size:0.75rem; padding:7px 16px; cursor:pointer; opacity:0.7;
}
.intro-skip:hover{ opacity:1; color:var(--text); }
html.intro-active, html.intro-active body{ overflow:hidden; }
/* Hold the hero's reveals until the intro hands over, so they don't
   play out of sight behind the overlay. */
html.intro-active .reveal{ opacity:0; transform:translateY(20px); }

/* ---- scroll reveal ------------------------------------------------
   Slightly further and slower than the old 20px/.7s, with a blur that
   resolves as it rises — the C8VN entrance. Blur is the cheap part of
   the effect and the part that reads as "expensive", but it is also the
   part that costs a repaint, so it is dropped entirely under reduced
   motion along with everything else. */
.reveal{ opacity:0; transform:translateY(26px); filter:blur(6px); }
.reveal.in{
  opacity:1; transform:none; filter:blur(0);
  transition:opacity .8s ease var(--d,0s),
             transform .9s cubic-bezier(.16,1,.3,1) var(--d,0s),
             filter .8s ease var(--d,0s);
}
@media (prefers-reduced-motion:reduce){
  .reveal{ opacity:1; transform:none; filter:none; transition:none; }
}

/* ---- scroll-scrubbed text -----------------------------------------
   Paired with js/scrubtext.js. `.is-scrub` is added by that script, so
   the dim state only ever exists once the script has run — no JS and the
   paragraph is a normal, fully-legible paragraph. Don't move the opacity
   onto `[data-scrub] .scrub-w`: that would dim text the script might
   never light. */
.is-scrub .scrub-w{
  opacity:0.26;
  transition:opacity .2s linear;
}
@media (prefers-reduced-motion:reduce){
  .is-scrub .scrub-w{ opacity:1 !important; transition:none; }
}

/* ---- buttons ------------------------------------------------------
   Mono, uppercase and tracked, not sentence-case body text. Every CTA
   on the site now reads in the same technical voice as the eyebrows and
   the gauge numerals, which is most of what makes the set feel like one
   instrument rather than a page of web buttons.

   .btn-acc is PHYSICAL, adapted from the supplied `3d button` snippet:
   a stack of hard offset shadows under a lifted face. The snippet drove
   it with six layered purple shadows and a skew; that reads as a toy at
   this scale, so what is kept is only the mechanic — the face sits above
   an offset shadow stack, hover lifts it further, :active drops it onto
   the stack. Travel is small (2px/1px) because the button is small.

   NOTE .btn-acc's text colour is --acc-ink, never white. Lime is a light
   colour; white text on it fails contrast outright. */
/* STADIUM, 2026-08-04. Buttons only — cards stay on --r-lg (20px) and
   inputs/chips stay on --r (6px). A pill button against a 20px card is the
   contrast that makes the button read as the thing you press; pilling the
   cards too (which is what the reference design did, at 24px) flattens
   that back out and costs the layout its only sharp edges.
   Applies on the app pages as well, since .btn is shared — deliberate, so
   the two halves of the product don't disagree about what a button is. */
.btn{
  display:inline-flex; align-items:center; justify-content:center; gap:9px;
  padding:13px 22px; border-radius:var(--r-pill);
  font-family:var(--font-mono);
  font-size:0.72rem; font-weight:500;
  text-transform:uppercase; letter-spacing:0.10em;
  text-decoration:none; white-space:nowrap;
  border:1px solid transparent; cursor:pointer;
  position:relative;
  transition:transform .2s cubic-bezier(.2,.9,.3,1), box-shadow .2s ease,
             background .2s ease, border-color .2s ease, color .2s ease;
}
.btn .arr{ transition:transform .2s ease; display:inline-block; }
.btn:hover .arr{ transform:translateX(4px); }
.btn-lg{ padding:16px 28px; font-size:0.76rem; }

.btn-acc{
  background:var(--acc); color:var(--acc-ink); font-weight:500;
  box-shadow:0 2px 0 0 rgba(97,124,10,0.55),
             0 4px 0 0 rgba(97,124,10,0.25),
             0 10px 30px -12px var(--acc-glow);
}
.btn-acc:hover{
  transform:translateY(-2px);
  box-shadow:0 4px 0 0 rgba(97,124,10,0.55),
             0 7px 0 0 rgba(97,124,10,0.25),
             0 18px 40px -14px var(--acc-glow);
}
/* Drops onto its own shadow stack — the press. */
.btn-acc:active{
  transform:translateY(1px);
  box-shadow:0 1px 0 0 rgba(97,124,10,0.55),
             0 2px 0 0 rgba(97,124,10,0.20),
             0 6px 18px -12px var(--acc-glow);
}

/* --line-strong, not --line. --line is a 0.08-alpha hairline meant for
   dividers inside a panel; on a button it left the site's most-used
   secondary CTA (including "Sign in" in the header) with an edge barely
   distinguishable from the background. */
.btn-ghost{ border-color:var(--line-strong); color:var(--text); background:transparent; }
.btn-ghost:hover{
  border-color:var(--acc-line); color:var(--text);
  background:var(--acc-soft);
  transform:translateY(-2px);
}
.btn-ghost:active{ transform:translateY(0); }

.btn-white{ background:var(--text); border-color:var(--text); color:var(--canvas); }
.btn-white:hover{ transform:translateY(-2px); box-shadow:0 12px 30px -14px rgba(0,0,0,0.7); }
.btn-white svg{ flex-shrink:0; }

@media (prefers-reduced-motion:reduce){
  .btn, .btn .arr{ transition:none; }
  .btn:hover, .btn:active{ transform:none; }
}
/* `nowrap` keeps a normal CTA on one line, but the site has a few long
   labels ("Find businesses without a website") that are wider than a
   small phone. Left unbreakable they don't just look bad — they set a
   min-content floor that drags any grid track containing them past the
   viewport. Below 560px they are allowed to wrap. */
@media (max-width:560px){
  .btn{ white-space:normal; text-align:center; }
}

/* ---- nav ---------------------------------------------------------- */
/* `:not(.app-head)` matters: `interface.html`/`find.html`/etc reuse the
   `<header>` tag for their own in-app content header (`<header
   class="app-head">`), a completely different component with its own
   rules under APP SHELL below. A bare `header{}` selector silently styles
   both. Found 2026-07-29 the hard way: bumping this block's z-index from
   40 to 48 to fix the marketing mobile-nav-vs-scrim bug (BUG #2 below)
   also pushed the APP's content header to 48, one above `.app-body
   .sidebar`'s z-index:47 — so the app header started painting over its
   own open mobile sidebar drawer. Scoping this whole block to the
   marketing header only, and giving `.app-body .app-head` its own
   explicit position/z-index (see APP SHELL), stops the two from ever
   colliding again regardless of what either value changes to. */
/* ==== NAME THE THING YOU ARE STYLING (2026-08-07) ====================
   These were `header:not(.app-head)` and a bare `footer`. Both are claims
   about EVERY header and EVERY footer in the codebase, and both were
   false:

     - `admin.html` has 34 header elements — the top dock plus 33 card
       headers (.acard-h, .brief-h) — and NOT ONE carries `.app-head`, so
       every card header was being made position:sticky at z-index 48
       with a blurred, tinted ::before painted behind its text. That is
       the "black box around the text" in the control room.
     - `bookings.html` uses semantic <header> and <footer> for the
       business owner's page and picked up the marketing header's tint
       and the marketing footer's gradient, on a page that shares nothing
       else with the marketing site.

   CLAUDE.md has carried this rule since the 2026-08-05 regression and
   this is the same mistake a third time, so the selectors now name their
   component. Every marketing page carries BOTH `.site-head` and
   `footer.pin-field` — verified across all 13 before the change — so
   nothing marketing-side moves. A new <header> or <footer> anywhere else
   now inherits nothing, which is the point. */
header.site-head{
  position:sticky; top:0; z-index:48;
  border-bottom:1px solid var(--line);
}
/* THE DOCK CLEARANCE MOVED TO THE SECTIONS' OWN `padding` SHORTHANDS.
   It used to live here as two `padding-top` longhands, and BOTH WERE
   DEAD: `.hero{ padding:… }` and `.page-head{ padding:… }` are declared
   further down the file at equal specificity, and a `padding` shorthand
   RESETS `padding-top` whether or not it mentions it. So the values that
   actually applied were the shorthands' — and on a 620px-tall phone that
   meant 72px against a 78px dock, i.e. 16px between the wordmark island
   and the first line of the page. Measured, not inferred.

   Nothing overlapped, which is why it survived: it just read cramped,
   and the comment describing the fix made it look handled. Set the
   clearance where the padding is actually declared instead — one place
   per section, no tie to lose. Search for "DOCK CLEARANCE" to find them. */
/* The blur/tint live on a pseudo-element, not on `header` itself.
   BUG #1 (found 2026-07-29): `backdrop-filter` on an element makes it a
   containing block for its `position:fixed` descendants — same as
   `transform`/`filter`/`perspective`. The mobile nav panel (`ul#navlinks`,
   see the max-width:820px rules below) is `position:fixed` and lives
   inside `header`, so with backdrop-filter on `header` directly its
   `top:0;bottom:0` resolved against header's own ~70px box instead of the
   viewport — the panel was squashed to a sliver and effectively invisible
   and untappable, while the toggle button (not fixed-position) still
   rendered fine. Moving the filter to `::before` keeps the same visual
   header and frees `header` from being a containing block, so the panel
   is fixed to the real viewport again. Don't put backdrop-filter back on
   `header` itself.
   BUG #2 (found immediately after, same session): fixing #1 wasn't enough
   — `header` still has an explicit `z-index`, which still makes it (and
   therefore every descendant, including the now-correctly-sized nav
   panel and the toggle button) its OWN stacking context capped at
   header's z-index. `.scrim` (js/main.js) is appended straight to
   `<body>`, outside `header`, at z-index:44 — HIGHER than header's old
   z-index:40 — so the entire header box painted as one unit BELOW the
   scrim. Once bug #1 was fixed the panel was finally the right size, but
   it was still trapped under the scrim, so the scrim's blur+dark tint
   visibly covered the whole panel too, not just the page behind it.
   Raised header's z-index from 40 to 48 (above the scrim's 44, and above
   .nav-toggle/.scrim/mobile-drawer's own local 45/46) so header — and
   the real-viewport-sized panel inside it — paints above the scrim. Only
   the page content behind the drawer should ever look blurred/dimmed;
   the header bar and the open drawer itself must stay crisp. */
/* ==== THE ISLAND DOCK (2026-08-05) ====================================
   The header used to be one full-width bar with a tinted, blurred
   ::before behind it. It is now THREE FLOATING ISLANDS — wordmark, a
   glass link pill, and the call to action — with the page scrolling
   visibly between them.

   The tint moved OFF `header::before` and onto the islands themselves,
   so there is no full-bleed layer left. Two things that did not change,
   because they are load-bearing and were each a real bug once:

     - `backdrop-filter` still never goes on `header` itself. It makes an
       element a containing block for its `position:fixed` descendants,
       and `ul#navlinks` IS fixed on mobile (it is the drawer). Putting it
       on the islands is fine: the pill has no fixed descendants.
     - `header` keeps its z-index:48, above the scrim's 44. See BUG #2
       above.

   The blur is 14px on three small elements rather than 18px on a
   full-viewport layer. That distinction matters here — the 2026-08-04
   performance work found a full-viewport `filter: blur()` was the single
   most expensive thing on the site. A backdrop-filter on a 350px pill is
   not the same cost at all, but do not let it grow to cover the viewport.
   -------------------------------------------------------------------- */
/* ==== .site-head — THE MARKETING DOCK, AND ONLY IT ====================
   These used to be written as `header:not(.app-head)`, which was wrong in
   a way that only showed up in the control room: `admin.html` contains
   THIRTY-FOUR <header> elements — the top dock plus 33 card headers
   (.acard-h, .brief-h) — and not one of them carries `.app-head`. So the
   negative margin below was applied to every card header on the page,
   pulling each card's own content up over its heading. Hundreds of
   broken cards from one selector.

   `:not(.app-head)` means "any header that is not THAT one", which is a
   guess about everything else in the codebase. `.site-head` names the
   thing being styled instead. Every rule specific to the floating dock
   lives on it; `header:not(.app-head)` is back to exactly what it was.
   ==================================================================== */
header.site-head{
  /* No bar: the islands carry their own surface and the page is meant to
     scroll visibly between them. */
  border-bottom:0;
  /* THE DOCK FLOATS OVER THE PAGE. The header still occupies ~78px of
     flow, and with no background of its own that strip showed bare
     `body` — a black bar, visible only on the homepage because `.hero`
     clips its aurora while `.page-head` does not. Pulling the next
     section up by the measured header height puts sections at y=0 and
     the dock on top of them. `--header-h` comes from offsetHeight in
     js/main.js, so sticky offsets keyed off it are unaffected. */
  margin-bottom:calc(-1 * var(--header-h, 78px));
}
header.site-head::before{
  content:""; position:absolute; inset:0; z-index:-1;
  background:rgba(8,9,10,0.66);
  backdrop-filter:blur(18px) saturate(150%);
  -webkit-backdrop-filter:blur(18px) saturate(150%);
}
/* THE FULL-WIDTH BAR IS OFF ON THE MARKETING DOCK, AND THIS LINE HAS TO
   SIT AFTER THE RULE IT CANCELS. Both selectors are (0,1,1): `:not()`
   contributes its argument's specificity, so `header:not(.app-head)::before`
   scores exactly the same as `header.site-head::before` and SOURCE ORDER
   decides. Written above the rule (as it was from 2026-08-05 until this
   was measured), `content:none` silently lost every time and the
   translucent grey bar the island dock was meant to replace kept painting
   behind it — the islands' own tint on top of a 66% wash plus an 18px
   full-width backdrop blur, which is also the one shape the performance
   note above says not to let back in. Confirmed by reading the computed
   value: `content` was `""`, not `none`.
   Don't move this line back above, and don't reach for !important —
   the ordering is the fix. */
header.site-head::before{ content:none; }

/* One recipe, three islands. The inset top highlight is what reads as
   glass — a lit top edge falling off into the tint — and it does more
   work than the blur does. */
header.site-head .brand, header.site-head #navlinks, .nav-end{
  /* TINT DOWN, BLUR UP (2026-08-06). Was rgba(…,0.62) + blur(14px): more
     than half the island's opacity came from a flat black wash, which
     makes it read as a dark plate that happens to be translucent rather
     than as glass. Glass hides what is behind it by SCATTERING, not by
     covering — so the tint drops to 0.34 and the blur nearly doubles.

     THE TWO ARE NOT INTERCHANGEABLE FOR LEGIBILITY, AND THAT IS WHY THIS
     TRADE IS SAFE. Tint lowers the backdrop's contrast uniformly; blur
     destroys its high-frequency detail, which is the part that actually
     competes with small nav text.

     COLOURLESS NOW, AND THE DARKENING IS DONE BY THE FILTER (2026-08-06,
     third pass). The tint was rgba(18,23,30,0.34) — a blue-black wash,
     which is what made the islands read as dark plates laid over the page
     rather than as glass. It has been replaced by two things that do the
     same job without adding a colour:

       - a 4% NEUTRAL white lift, which is the surface of the glass
         itself catching light, not a veil over the page;
       - `brightness(0.72)` INSIDE the backdrop-filter, which dims what is
         behind the panel without tinting it. That is the part that keeps
         white nav labels legible, and unlike a black wash it works the
         same way over the light promo band as over the dark hero — it
         scales the backdrop rather than painting over it.

     Blur is up from 26px to 34px, which is the other half of what was
     asked for: glass hides what is behind it by SCATTERING, not by
     covering. */
  background:rgba(255,255,255,0.04);
  border:1px solid rgba(255,255,255,0.085);
  border-radius:999px;
  backdrop-filter:blur(34px) saturate(150%) brightness(0.72);
  -webkit-backdrop-filter:blur(34px) saturate(150%) brightness(0.72);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.10),
    inset 0 -1px 0 rgba(0,0,0,0.28),
    0 10px 30px -18px rgba(0,0,0,0.95);
}

/* ==== LIQUID GLASS ON THE DOCK ISLANDS (2026-08-06) ==================
   The islands were flat frosted panes: one even blur, edge to edge. Real
   glass with thickness does not do that — it is CLEAREST IN THE MIDDLE
   and bends hardest at the rim, where the surface curves away, and it
   catches a specular highlight along its lit edge. That difference is
   the whole of what makes the iOS look read as a physical object rather
   than a translucent rectangle.

   BUILT IN TWO LAYERS, AND THE SPLIT IS THE IMPORTANT PART:

     1. `::before` / `::after` below — the rim lens and the specular.
        Pure `backdrop-filter`, masks and gradients, so this works in
        EVERY engine including WebKit, i.e. every browser on iOS.
     2. `html.glass-liquid` further down — true pixel displacement via
        an SVG filter. CHROMIUM ONLY.

   THE SECOND LAYER CANNOT BE THE WHOLE EFFECT, AND ASSUMING IT COULD IS
   THE MISTAKE THIS BLOCK EXISTS TO RECORD. `backdrop-filter` accepts an
   SVG filter reference in Chromium and nowhere else: WebKit and Gecko
   implement only the shorthand filter functions, so `url(#…)` there is
   not a weaker effect, it is NO effect — the declaration is dropped and
   the island renders exactly as it did before. A first pass shipped the
   displacement alone and was invisible to anyone on an iPhone, which is
   the device this dock is most often looked at on. So the layer that
   works everywhere carries the look, and the displacement is a bonus on
   top for the engines that can do it.

   NEITHER PSEUDO IS CLIPPED BY THE PARENT, AND NEITHER NEEDS TO BE. This
   block used to set `overflow:clip` on the islands and say that was what
   confined them. It was removed with the WebKit fix below and the comment
   is corrected rather than the property restored: both pseudos are
   `inset:0` with `border-radius:inherit`, so they already describe exactly
   the parent's rounded box and have nothing to spill past. `overflow`
   other than `visible` also makes the element a scroll container, which is
   not something a nav island should quietly become.

   #navlinks IS SCOPED TO min-width:821px THROUGHOUT THIS BLOCK, AND THAT
   IS NOT TIDINESS. Below 820px that same element stops being a pill and
   becomes the full-height mobile drawer — `overflow:clip` on a panel
   that can grow taller than the viewport would cut its last links off
   with no way to scroll to them, and a rim lens masked to a 50px band
   means nothing on a full-screen sheet. `.brand` and `.nav-end` stay
   pills at every width, so they carry the treatment unconditionally. */
header.site-head .brand,
header.site-head .nav-end{
  position:relative;
  isolation:isolate;
}
@media (min-width:821px){
  header.site-head #navlinks{
    position:relative;
    isolation:isolate;
  }
}

/* ---- 1a. THE RIM LENS -----------------------------------------------
   A second, much heavier backdrop pass masked to the outer band. It sits
   over the island's own blur, so the two COMPOUND at the edge and the
   centre keeps only the base blur — dense rim, clear middle, which is
   the read we want and is free: no extra element, no script.

   The mask is two linear gradients rather than one radial. A radial
   cannot describe a 700x50 pill — sized to reach the left and right ends
   it swallows the whole height, and sized to the height it never reaches
   the ends. Two axis-aligned ramps give an even band on all four sides
   at any width the islands ever take. Deliberately no `mask-composite`:
   the default (`add` in the standard, `source-over` in the -webkit-
   model) unions them either way, which is exactly what is wanted, so the
   rule needs no engine-specific variant. */
header.site-head .brand::before,
header.site-head .nav-end::before{
  content:""; position:absolute; inset:0; z-index:-1;
  border-radius:inherit; pointer-events:none;
  /* NO backdrop-filter HERE — see the .glass-liquid rule below. */
  background:linear-gradient(180deg,
    rgba(255,255,255,0.055), rgba(255,255,255,0) 46%,
    rgba(255,255,255,0.028));
  -webkit-mask-image:
    linear-gradient(90deg,#000 0,transparent 17%,transparent 83%,#000 100%),
    linear-gradient(180deg,#000 0,transparent 34%,transparent 66%,#000 100%);
  mask-image:
    linear-gradient(90deg,#000 0,transparent 17%,transparent 83%,#000 100%),
    linear-gradient(180deg,#000 0,transparent 34%,transparent 66%,#000 100%);
}
@media (min-width:821px){
  header.site-head #navlinks::before{
    content:""; position:absolute; inset:0; z-index:-1;
    border-radius:inherit; pointer-events:none;
    background:linear-gradient(180deg,
      rgba(255,255,255,0.055), rgba(255,255,255,0) 46%,
      rgba(255,255,255,0.028));
    -webkit-mask-image:
      linear-gradient(90deg,#000 0,transparent 17%,transparent 83%,#000 100%),
      linear-gradient(180deg,#000 0,transparent 34%,transparent 66%,#000 100%);
    mask-image:
      linear-gradient(90deg,#000 0,transparent 17%,transparent 83%,#000 100%),
      linear-gradient(180deg,#000 0,transparent 34%,transparent 66%,#000 100%);
  }
}

/* ---- 1b. THE SPECULAR ------------------------------------------------
   A lit top-left shoulder falling off to nothing by the middle, plus a
   faint bounce along the bottom — the two highlights a convex body picks
   up from a light above it. Kept under 0.3 alpha: this sits behind live
   nav text, and a highlight strong enough to admire is strong enough to
   hurt the contrast of the label on top of it. */
header.site-head .brand::after,
header.site-head .nav-end::after{
  content:""; position:absolute; inset:0; z-index:-1;
  border-radius:inherit; pointer-events:none;
  background:
    linear-gradient(158deg,
      rgba(255,255,255,0.26) 0%,
      rgba(255,255,255,0.07) 18%,
      rgba(255,255,255,0) 42%),
    linear-gradient(0deg,
      rgba(255,255,255,0.07) 0%,
      rgba(255,255,255,0) 26%);
}
@media (min-width:821px){
  header.site-head #navlinks::after{
    content:""; position:absolute; inset:0; z-index:-1;
    border-radius:inherit; pointer-events:none;
    background:
      linear-gradient(158deg,
        rgba(255,255,255,0.26) 0%,
        rgba(255,255,255,0.07) 18%,
        rgba(255,255,255,0) 42%),
      linear-gradient(0deg,
        rgba(255,255,255,0.07) 0%,
        rgba(255,255,255,0) 26%);
  }
}

/* A thicker, brighter rim than a flat pane gets. The inset ring is what
   reads as the glass's own edge catching light all the way round, rather
   than a 1px border drawn on a rectangle. */
header.site-head .brand,
header.site-head #navlinks,
header.site-head .nav-end{
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.22),
    inset 0 0 0 1px rgba(255,255,255,0.05),
    inset 0 -1px 0 rgba(0,0,0,0.34),
    0 10px 30px -18px rgba(0,0,0,0.95);
}

/* ---- 2. TRUE REFRACTION, CHROMIUM ONLY -------------------------------
   `.glass-liquid` is added by js/main.js only after CSS.supports agrees,
   so this never lands as a dropped declaration — and an engine that
   gains support later gets it with no change here. See that file's
   installLiquidGlass() for how the displacement map encodes the vector
   field and why the vertical push is gentler than the horizontal one.

   THE BASE BLUR DROPS TO 5px HERE, AND THAT IS THE POINT. Blur and
   displacement fight each other: 14px of blur smears the bent pixels
   back into an even wash, so you pay for the filter and see almost none
   of it. Thick glass barely diffuses — it bends. The rim pseudo above
   still supplies the heavy edge blur, so the island does not lose its
   frost, it just stops applying it flat across the middle. */
/* ==== THE SVG REFERENCE NEVER SHARES A DECLARATION WITH THE BLUR ======
   THIS IS THE BUG THAT MADE THE DOCK LOOK UNBLURRED FOR THREE ROUNDS, and
   it is worth stating exactly, because everything about it reads as
   working right up until you look at the page.

   The rule here used to put both in one chain — first
   `blur(16px) saturate(170%) url(#sl-glass)`, then, after the ordering
   was corrected, `url(#sl-glass) blur(13px) saturate(165%)`. Both PARSE.
   `CSS.supports('backdrop-filter','url(#x)')` returns true, the computed
   style reads back exactly as written, the filter element is in the DOM
   and its feImage href is a real PNG. Every check said yes.

   It still rendered nothing on the live site — reported three times, from
   three different machines, always as "no blur at all".

   REPRODUCED AND A/B'd, after two wrong guesses. The islands were set up
   over a large sharp headline and photographed twice, changing only the
   one declaration:

     `url(#sl-glass) blur(13px) saturate(165%)`  -> the headline reads
        straight through the islands. Sharp. No frost whatsoever.
     `blur(34px) saturate(150%) brightness(0.72)` -> the same headline is
        smeared to an unreadable wash inside the islands and stays sharp
        one pixel outside them.

   IT IS NOT "url() IN A BACKDROP-FILTER", which is what this comment
   claimed first and what two earlier sessions assumed. A probe with a
   SIMPLE referenced filter — one feImage plus one feDisplacementMap —
   blurs and warps perfectly well in a backdrop-filter chain, with the
   <svg> appended to <body> or to <html> alike. The discriminator is the
   COMPLEXITY OF THE FILTER GRAPH: `#sl-glass` is eight primitives (one
   feImage, three feDisplacementMap, three feColorMatrix, two feBlend, for
   the chromatic aberration). Chromium runs the simple graph on a backdrop
   and silently declines the elaborate one — and when it declines, it
   drops THE WHOLE DECLARATION rather than the parts it cannot do, taking
   the `blur()` standing beside it.

   That is why three rounds of "fix the warp" never restored the frost:
   every one of them rewrote the very declaration that was suppressing it.

   THE STRUCTURE BELOW MAKES THE CAUSE STOP MATTERING. Whatever a browser
   declines can only cost what shares its declaration. So: WHEN AN
   ENHANCEMENT AND A BASELINE SHARE A DECLARATION, THE ENHANCEMENT CAN
   DELETE THE BASELINE. Everything else here already fails open by keeping
   the two apart — `html.js` and `.reveal`, the opaque background under
   the generated sites' glass. This is that rule applied here, and it
   holds for the graphs no one has tried yet as well as this one.

   So the element carries the blur ALONE, above, where nothing can take it
   away. The displacement is asked for only on the rim pseudo-element,
   which has no job other than decoration: if Chromium renders it, the rim
   refracts; if it drops it, the pseudo loses a gradient nobody was
   depending on and the island keeps its frost either way. */

/* THE HEAVY RIM PASS IS CHROMIUM-ONLY, AND THAT IS A BUG FIX, NOT A
   PREFERENCE (2026-08-06). It used to sit on the bare `::before`, which
   put a `backdrop-filter` INSIDE an element that already had one — and
   **WebKit does not render a nested backdrop-filter at all**. Worse, it
   did not merely skip the inner one: the islands came back with no
   visible blur whatsoever on iOS, where every browser (Arc Search
   included) is required to use WebKit. Reported as "no blur nor warp".

   So the base blur now lives alone on the element, which is the
   construct WebKit renders reliably, and the second pass is asked for
   only where nesting works. The rim keeps its gradient on every engine,
   so the edge still reads as lit — it just is not also re-blurred.

   DO NOT MOVE THIS BACK ONTO THE UNPREFIXED SELECTOR.

   THE DISPLACEMENT RIDES HERE TOO, AND ONLY HERE. It is in the same
   declaration as this rim blur deliberately: both are Chromium-only
   extras on a decorative pseudo-element, so if the SVG reference makes
   Chromium drop the declaration, what is lost is a rim treatment — never
   the island's own frost, which lives on the element itself and shares a
   declaration with nothing. That is the whole point of the split
   documented above. */
html.glass-liquid header.site-head .brand::before,
html.glass-liquid header.site-head .nav-end::before{
  backdrop-filter:url(#sl-glass) blur(22px) saturate(180%) brightness(1.10);
}
@media (min-width:821px){
  html.glass-liquid header.site-head #navlinks::before{
    backdrop-filter:url(#sl-glass) blur(22px) saturate(180%) brightness(1.10);
  }
}

nav.wrap{
  display:flex; align-items:center; justify-content:space-between;
  gap:14px; padding:14px 26px;
}
/* ONE HEIGHT FOR ALL THREE, set here rather than left to each island's own
   contents. Before this the wordmark was 48px, the link pill 46px and the
   CTA island 60px — three different pills on three different baselines,
   which is exactly the "not quite aligned" look the islands are meant to
   avoid. Height is fixed and the contents centre inside it, so adding a
   link or swapping "Sign in" for "Interface" cannot resize an island. */
header.site-head .brand, header.site-head #navlinks, .nav-end{
  height:50px; box-sizing:border-box;
  display:flex; align-items:center;
}
header.site-head .brand{ padding:0 20px 0 10px; }
header.site-head #navlinks{ padding:0 8px; }
.nav-end{ gap:6px; padding:0 6px; }
.nav-end .btn{ padding:9px 18px; }
/* THE CTA MUST NOT WRAP, AND ON A PHONE IT DID (fixed 2026-08-05).
   `.nav-end` is a flex row holding this button and the 42px hamburger,
   and a flex item's default `min-width:auto` only stops it shrinking
   below its MIN-CONTENT width — which for "SIGN IN" is the wider of the
   two words, not the phrase. At 375px the row ran out of room, the button
   shrank to 93px, and the label broke into "SIGN" / "IN": two lines of
   mono caps in a pill pinned to 50px, so it overflowed its own island.
   `nowrap` makes the whole phrase the minimum, and `flex-shrink:0` on the
   toggle means the button gives up width last rather than the tap target
   doing it. Measured before: 93x57 in a 50px island. */
.nav-end .btn{ white-space:nowrap; }
.nav-end .nav-toggle{ flex:0 0 auto; }
/* AN ISLAND WITH NOTHING IN IT MUST NOT PAINT. Above 820px the hamburger
   is `display:none`, so a `.nav-end` holding only the toggle is an empty
   pill — 14px of tinted, blurred, hairlined glass floating in the top
   right corner with no content and no explanation. That is exactly what
   sign-in.html shipped: it was the one marketing page never updated when
   the CTA moved out of `#navlinks` into `.nav-end`, so it had the island
   and not the button. Reported as "an unexplained blob at the top right",
   which is precisely what it looked like.

   The markup is fixed, so this rule fires on nothing today — it exists so
   the next page that drifts from the shell renders one fewer island
   rather than one mystery. Desktop only: below 821px the same element is
   the real home of the hamburger and must always show.

   `:has()` is the whole selector, so an engine without it drops the rule
   and gets today's behaviour. It fails toward showing, not hiding. */
@media (min-width:821px){
  header.site-head .nav-end:not(:has(#nav-auth)){ display:none; }
}
/* The CTA sits INSIDE the right-hand island, so it must not carry a
   second border of its own — two concentric pills read as a mistake. */
.nav-end .btn{ border-color:transparent; background:transparent; }
.nav-end .btn:hover{ background:rgba(255,255,255,0.06); }
.nav-end .btn-white{ background:var(--text); border-color:var(--text); }

/* ==== THE INTERFACE BUTTON'S HOVER (fixed 2026-08-06) =================
   IT WAS LOSING ITS BACKGROUND ENTIRELY. `.nav-end .btn:hover` scores
   (0,3,0) and `.nav-end .btn-white` scores (0,2,0), so on hover the
   generic ghost-button rule OUTRANKED the white fill and repainted the
   solid pill as `rgba(255,255,255,0.06)` — 6% white over a dark page.
   The signed-in CTA, the one control a returning customer aims at every
   visit, went from solid white to almost invisible the moment the
   pointer touched it, and its dark label went with it. Reported as the
   button not responding well; it was responding by disappearing.

   THE FIX IS A HOVER OF ITS OWN, NOT A SPECIFICITY PATCH. Bumping the
   white rule to out-rank the ghost one would leave the button looking
   identical hovered and unhovered, which is its own fault — a control
   that never acknowledges the pointer reads as disabled.

   A solid white pill cannot get lighter, so the feedback has to come
   from elsewhere: it lifts a little, gains a soft ring of its own colour
   so the edge reads as lit rather than as a hard cut, and the mark
   inside picks up the same lift. Everything here is transform, shadow
   and colour — no layout, so it stays on the compositor. */
.nav-end .btn-white{
  transition:transform .18s cubic-bezier(.2,.8,.3,1),
             box-shadow .18s ease,
             background-color .18s ease;
}
.nav-end .btn-white:hover,
.nav-end .btn-white:focus-visible{
  background:#fff;
  border-color:#fff;
  transform:translateY(-1px);
  box-shadow:0 6px 18px -8px rgba(0,0,0,0.85),
             0 0 0 4px rgba(244,245,247,0.14);
}
.nav-end .btn-white:active{ transform:translateY(0); }
/* The icon inside is an inline <svg> written by js/auth.js, so it
   inherits `currentColor` and needs nothing but a matching nudge. */
.nav-end .btn-white svg{ transition:transform .18s cubic-bezier(.2,.8,.3,1); }
.nav-end .btn-white:hover svg{ transform:translateX(1px); }
@media (prefers-reduced-motion:reduce){
  .nav-end .btn-white,
  .nav-end .btn-white svg{ transition:none; }
  .nav-end .btn-white:hover{ transform:none; }
  .nav-end .btn-white:hover svg{ transform:none; }
}

/* ==== THE IN-BETWEEN DOCK: mark only, sign-in as an icon (2026-08-06) ==
   MEASURED, NOT GUESSED. At its natural size the row needs 955px:
   brand 154 + links 613 + CTA 108, plus 52px of nav padding and 2x14px
   of gap. Below that the links do not wrap — `#navlinks` is a flex row
   with no wrap — they COMPRESS, and each label then wraps inside its own
   pill. At 870px "How it works" measured 53px against a natural 121px,
   which is the two-lines-in-a-pill mess this fixes. It is not a mobile
   bug: the shelf does not take over until 820px, so there were ~135px of
   width where the dock was visibly broken and nothing handled it.

   THE TWO THINGS THAT GO ARE THE TWO THAT ARE NOT NAVIGATION. The
   wordmark restates the logo beside it, and the CTA's label restates an
   icon everyone already reads. Dropping both frees 92 + 56 = 148px, so
   the full seven-link row fits unsqueezed all the way down to ~807px —
   below the 821px shelf boundary, which means there is no longer ANY
   width at which the links are compressed. That is the whole design
   goal, and it is why the numbers above matter: pick a narrower saving
   and a broken band survives underneath it.

   THE 955px FIGURE IS THE SIGNED-OUT ROW, AND THAT IS WHY THE THRESHOLD
   WAS 7px TOO LOW UNTIL 2026-08-06. `js/auth.js` replaces the contents of
   #nav-auth on every page where a session is found, and the "Interface"
   button it swaps in is WIDER than the "Sign in" link it replaces —
   measured 150px against 108px, because it carries an inline icon as well
   as a longer word. So a signed-in row needs 154 + 613 + 150 + 52 + 28 =
   997px, and collapsing at 990 left 991-996 uncovered. Measured in that
   band, signed in: "How it works" came back 60px wide and 55px TALL —
   wrapped to two lines inside a 50px island, i.e. exactly the mess this
   block exists to prevent, surviving just above its own ceiling.

   THE THRESHOLD MUST BE SET BY THE WIDER OF THE TWO STATES, NOT BY THE
   ONE THAT HAPPENS TO BE IN THE MARKUP. The signed-out row is what a
   static page renders and therefore what anyone reading the HTML measures,
   but the signed-in row is what a paying customer actually sees on every
   visit — and it is the one that needs more room. Collapsing at 1040px
   covers the 997px signed-in fit with 43px of slack, the same reasoning
   the 990/955 pair used: a zero-slack fit puts the mess back the moment a
   webfont lands a fraction wide or a nav link is added. Signed-out
   visitors between 998 and 1040 get the collapsed dock a little earlier
   than they strictly need it, which costs nothing — a mark and an icon are
   not a degraded state, and one breakpoint for both is one thing to keep
   true rather than two. */
@media (min-width:821px) and (max-width:1040px){
  header.site-head nav.wrap{ padding:14px 18px; }

  /* `Scout` IS A BARE TEXT NODE in the markup, with only `line` wrapped
     in a span — so it cannot be hidden with display:none. font-size:0 on
     the container collapses both it and the span, and leaves the <img>
     alone because the mark is sized by its width/height attributes.
     It also keeps the accessible name intact: the text is still in the
     DOM and still in the accessibility tree, so the link continues to
     announce as "Scoutline" rather than becoming an unlabelled image
     (the mark's alt is deliberately empty). `gap:0` is restated because
     this is the flex container CLAUDE.md warns about — any gap here
     lands inside the word and renders "Scout line". */
  /* A CIRCLE, NOT A SHORT OVAL. The islands are pinned to height:50px, so
     "collapsed" only reads as deliberate if the width matches — left at
     `padding:0 13px` this measured 70x50, which looks like a pill that
     failed to fit its text rather than a mark in a circle. Fixed width
     plus `justify-content:center` centres the 32px mark in 50px. */
  header.site-head .brand{
    font-size:0; gap:0; padding:0;
    width:50px; justify-content:center;
  }
  /* AND THE MARK'S OWN MARGIN HAS TO GO WITH THE WORD IT WAS SPACING.
     `.brand` keeps `gap:0` for the reason above, so the space between the
     mark and "Scoutline" is carried by `.brand-mark{ margin-right:10px }`
     instead. Collapsing the text leaves that margin behind, and
     `justify-content:center` then centres a 42px flex line (32px image +
     10px of nothing) inside the 50px circle — so the mark itself lands
     5px LEFT of centre while the disc around it is symmetrical. Measured:
     image centre 38, island centre 43.

     Only the left island had it. The right one holds an icon with no
     trailing margin, so it centred correctly all along, and a lone
     off-centre mark reads as a rendering fault rather than a style. */
  header.site-head .brand .brand-mark{ margin-right:0; }

  /* The CTA becomes its target: a door with an arrow going in. Applied
     to `.btn-ghost` specifically, i.e. the SIGNED-OUT state — js/auth.js
     swaps this slot for a `.btn-white` "Interface" link that already
     carries its own inline <svg>, so that one needs no icon supplying
     and `font-size:0` alone collapses it correctly. */
  /* Matching circle on the right. The island itself is sized rather than
     the button, so the whole 50px disc is the tap target instead of a
     42x21 button floating inside a 56px pill. Safe to fix the width here
     because `.nav-toggle` is display:none above 820px — below that this
     same element has to hold the hamburger too, which is why this lives
     in the 821-990 block and not on the base rule. */
  header.site-head .nav-end{
    width:50px; padding:0; gap:0; justify-content:center;
  }
  header.site-head .nav-end #nav-auth{ display:flex; width:100%; height:100%; }
  header.site-head .nav-end #nav-auth .btn{
    font-size:0; gap:0; padding:0;
    width:100%; height:100%; justify-content:center;
  }
  header.site-head .nav-end #nav-auth .btn-ghost::before{
    content:""; width:19px; height:19px; flex:0 0 auto;
    background:currentColor;
    -webkit-mask:var(--icon-sign-in) center / contain no-repeat;
            mask:var(--icon-sign-in) center / contain no-repeat;
  }
  /* Phosphor `sign-in`, regular weight, transcribed to a mask. Strokes
     are #000 rather than currentColor: a mask reads ALPHA, and a colour
     keyword inside a data URI has no cascade to inherit from anyway —
     the visible colour comes from `background:currentColor` above, so
     this still follows the button's own text colour in both states. */
  header.site-head .nav-end #nav-auth .btn-ghost{
    --icon-sign-in:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><g fill="none" stroke="%23000" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"><line x1="24" y1="128" x2="136" y2="128"/><polyline points="96 88 136 128 96 168"/><polyline points="136 40 200 40 200 216 136 216"/></g></svg>');
  }
}

/* Hairline separators between the links, as on the reference. Drawn on
   the <li> rather than as a border on the <a> so the gap stays even and
   the first item never gets a leading rule. */
header.site-head #navlinks li{ position:relative; }
header.site-head #navlinks li + li::before{
  content:""; position:absolute; left:calc(var(--nav-gap, 22px) / -2);
  top:50%; transform:translateY(-50%);
  width:1px; height:14px; background:rgba(255,255,255,0.10);
}
/* gap:0 is deliberate and load-bearing. This is a flex container, and
   "Scout" and the gold <span class="dot">line</span> are TWO flex items —
   so any `gap` here lands in the middle of the wordmark and splits it into
   "Scout line". The spacing between the logo and the word comes from
   .brand-mark's margin instead, which only ever applies to the image. */
.brand{ display:inline-flex; align-items:center; gap:0; font-weight:600; font-size:1.16rem; text-decoration:none; letter-spacing:-0.03em; }
/* THE WORDMARK IS PLAIN WHITE, 2026-08-01, and this span is why the rule
   still exists. `<span class="dot">line</span>` used to colour the second
   half of "Scoutline" the brand accent. The mark is now plain white and the
   wordmark matches it — a two-tone wordmark next to a single-tone mark
   reads as two logos. The span is left in the markup (it is in every
   page's header and footer) and simply inherits, so nothing had to be
   edited across 14 files to make this change. */
.brand .dot{ color:inherit; }
/* 32px, not 24, BECAUSE THE HEADER USES THE FULL MARK. The compact cut
   read fine at 24 — it's five big dots — but the full mark's outer ring is
   r 2.8 in a 256 viewBox, which is 0.26px at 24px and simply doesn't
   render. At 32px it's 0.35px, so it resolves on a 2x display (~1.4px
   across) and antialiases to a faint but real dot on 1x. That's the honest
   floor for showing all four rings next to the wordmark; going back to 24
   means going back to logo-mark-sm.svg.
   No border-radius any more: the old PNG was a square tile that needed
   corners rounding, the SVG is transparent artwork that doesn't. */
.brand-mark{ width:32px; height:32px; display:block; margin-right:10px; }
/* Mono, uppercase, tightly sized. This is WIDER per character than the
   old 0.9rem body-font nav, so the size came down to 0.7rem and the gap
   from 28px to 22px to buy it back — measured against the widest label
   set ("Sample output" / "How it works"), not guessed. The header
   breakpoints further down were re-checked against this. */
nav ul{ list-style:none; display:flex; align-items:center; gap:var(--nav-gap,22px); margin:0; padding:0; }
/* The links sit inside a pill now, so each gets its own hit area and its
   own hover pill rather than relying on the bar around them. */
header.site-head #navlinks{ --nav-gap:6px; }
/* Padding only — NO hover or current background here. `.nav-rail` is the
   one highlight, and it slides between links; a second static background
   underneath it would leave two lit items whenever you point at anything
   other than the current page. */
header.site-head #navlinks a{ padding:7px 14px; border-radius:999px; }
nav ul a{
  text-decoration:none; font-family:var(--font-mono);
  font-size:0.7rem; font-weight:400;
  text-transform:uppercase; letter-spacing:0.09em;
  /* --text-on-wash, not --dim. The header floats over whatever the first
     section is, which on most pages is the spectral wash — so these are
     the definition of "text over the wash", which is what that token
     exists for. Measured: at --dim they fell to 3.9-4.4:1 against a 4.5
     requirement once the CSS blur came off and the pools concentrated.
     The aurora's own top fade cannot reach them, because the element
     starts ABOVE the viewport and a mask percentage is relative to the
     element, not the screen. */
  color:var(--text-on-wash); transition:color .2s ease;
}
nav ul a[aria-current="page"]{ color:var(--text); }
nav ul a:hover{ color:var(--text); }
nav ul a.btn{ color:var(--text); letter-spacing:0.10em; }
nav ul a.btn-white{ color:var(--canvas); }
nav ul a.btn-white:hover{ color:var(--canvas); }
.nav-toggle{ display:none; background:none; border:none; color:var(--text); font-size:1.4rem; cursor:pointer; padding:4px 8px; }
@media (max-width:820px){
  .nav-toggle{ display:block; }
  /* THE ISLAND UNDO USED TO LIVE HERE AND IT BROKE THE DRAWER OUTRIGHT.
     Removed 2026-08-05. The rules were written as
     `header.site-head #navlinks{ background:none; padding:20px 24px; … }`,
     which is (1,1,1) — an ID — while the drawer's own look, in the
     "marketing nav, small screens" block far below, is written
     `nav.wrap ul` at (0,1,2). An ID beats two elements and a class no
     matter which comes later, so the undo won and the drawer rendered
     with NO background, NO border, NO shadow and 20px of top padding
     instead of 88px: the links floated transparently over the hero,
     unreadable, with the first one tucked under the floating dock.
     Measured, not inferred — `getComputedStyle(#navlinks).backgroundColor`
     was `rgba(0,0,0,0)` with the menu open.

     The drawer is now styled at matching specificity in that later block,
     which both wins this tie on source order and restates every island
     property it needs to undo. There is one place again, and it is the
     one the note down there has always pointed at.

     THE OLD SHELF RULES WENT WITH IT. `nav.wrap ul{ position:absolute;
     top:100%; … display:none }` was the pre-drawer dropdown, dead since
     the drawer landed and overridden property-for-property by it — but it
     reads as authoritative, and `display:none` in particular is the one
     declaration that would silently make an animated drawer un-animatable
     if the later block ever lost. Dead CSS that looks load-bearing is how
     this file keeps getting bitten. */
}

/* ---- pin-grid texture (Scoutline's own motif) --------------------- */
.pin-field{ position:relative; }
.pin-field::before{
  content:""; position:absolute; inset:0; pointer-events:none;
  background-image:radial-gradient(rgba(139,144,150,0.16) 1.2px, transparent 1.8px);
  background-size:28px 28px;
  -webkit-mask-image:radial-gradient(ellipse 90% 80% at 50% 30%, #000 0%, transparent 75%);
          mask-image:radial-gradient(ellipse 90% 80% at 50% 30%, #000 0%, transparent 75%);
  animation:pin-drift 60s linear infinite;
}
@keyframes pin-drift{
  from{ background-position:0 0; }
  to  { background-position:56px 28px; }
}
@media (prefers-reduced-motion:reduce){
  .pin-field::before{ animation:none; }
}

/* ---- hero ---------------------------------------------------------
   Near-black, and mostly empty. The whole premise of the new identity is
   that the blackspace is the luxury signal, so the hero's job is to hold
   one headline at real size and get out of the way. Colour arrives only
   as out-of-focus light (`.aurora`, mounted by js/main.js) — there is no
   flat tint anywhere in here. */
.hero{
  /* DOCK CLEARANCE — the top value is not free-choice spacing. The dock
     floats over this section (header.site-head carries a negative bottom
     margin of --header-h), so the hero starts at y=0 and its own padding
     is the ONLY thing keeping the eyebrow off the wordmark island. The
     dock's box is 78px, so the floor has to clear that with room to read
     as deliberate rather than as a near-miss. */
  padding:clamp(118px,15vh,168px) 0 clamp(72px,10vh,120px);
  position:relative; overflow:hidden; isolation:isolate;
  background:var(--canvas);
}
/* A single soft lift under the header so the sticky bar has something to
   separate from, fading to flat --canvas well before the bottom edge —
   at full strength at the boundary it cut off square against the section
   below and left a visible horizontal seam. */
.hero::before{
  content:""; position:absolute; inset:0; z-index:-2; pointer-events:none;
  background:linear-gradient(180deg, rgba(244,245,247,0.045) 0%, transparent 62%);
}
.hero .spotlight{
  position:absolute; inset:0; pointer-events:none; z-index:-1;
  background:radial-gradient(620px circle at var(--mx,70%) var(--my,30%),
             rgba(215,255,62,0.07), transparent 62%);
  transition:background .12s linear;
}
/* The copy column. Capped well short of the .wrap width so it never runs
   under `.hero-mark` on the right — the mark is 58vw and starts around
   the 60% mark, so the text stops before it. `position:relative` +
   z-index:1 keeps the headline above the mark wherever they do meet. */
.hero .wrap{
  display:grid; gap:22px; position:relative; z-index:1;
  max-width:var(--well);
}
/* 48%/600, not 56%. At 56% the headline's box measured 25px INTO the
   mark's box at 1440px. The mark's own geometry keeps ~17% of clear space
   inside its square (the logo's documented safe area), so nothing actually
   collided — but relying on the artwork's internal padding to save a
   layout overlap is how a later change to that padding breaks the hero.

   RE-TUNED 2026-08-04 when the well went 1180 -> 1340. The mark is sized
   off the VIEWPORT (58vw), so widening the well moves the text toward a
   mark that did not move. The old 52%/620 still cleared at 1440 (45px) but
   fell to 15px at 1280 — measured, not guessed. 48%/600 restores ~60-70px
   of clearance across 1280-1680 and leaves the headline's own width within
   a few px of what it was, so nothing reflows.

   The hero deliberately does NOT take the extra width the wider well gives
   every other section: here the mark is the constraint, not the margin. */
.hero .wrap > *{ max-width:min(48%,600px); }
@media (max-width:1100px){
  .hero .wrap > *{ max-width:none; }
}

/* ==== EYEBROW =======================================================
   Was a bordered pill with a accent dot. It is now the bare micro-label
   from the token block — mono, uppercase, widely tracked — with the dot
   kept as the one piece of accent. The pill is gone because a page with
   four section-heads had four pills on it, and four outlined capsules
   read as four buttons rather than as four labels. Class name unchanged:
   `.eyebrow` is in the markup of all 14 pages. */
.eyebrow{
  /* align-items is flex-start, NOT center, so that an eyebrow long enough
     to wrap keeps its dot beside the FIRST line rather than floating down
     the middle of a two- or three-line block. The dot's margin-top below
     re-centres it on that first line, so single-line eyebrows — which is
     nearly all of them — look exactly as they did. */
  display:inline-flex; align-items:flex-start; gap:10px;
  font-family:var(--font-mono); font-size:0.66rem; font-weight:500;
  text-transform:uppercase; letter-spacing:0.15em;
  /* Was --dim. Every hero eyebrow sits directly on the wash. */
  color:var(--text-on-wash); width:fit-content;
  background:none; border:0; padding:0; border-radius:0;
}
.eyebrow .p{
  width:5px; height:5px; border-radius:50%; background:var(--acc);
  box-shadow:0 0 8px rgba(215,255,62,0.7); flex-shrink:0;
  /* Centres the dot on the FIRST line rather than on the whole block.
     The em value is the fallback; the calc is exact — `1lh` is the
     computed line box, so (line − dot) / 2 needs no magic number and
     stays right if the eyebrow's font-size or line-height ever move. */
  margin-top:0.57em;
  margin-top:calc((1lh - 5px) / 2);
}
/* The sentence as ONE flex item. Without min-width:0 a flex item refuses
   to shrink below its longest unbreakable run, which on a narrow phone is
   what pushes the line past the viewport. */
.eyebrow .eyebrow-t{ min-width:0; }
.hero .eyebrow{ font-size:0.7rem; }
.hero .eyebrow .p{ animation:pulse 2.4s ease-in-out infinite; }
@keyframes pulse{
  0%,100%{ box-shadow:0 0 8px rgba(215,255,62,0.7), 0 0 0 0 rgba(215,255,62,0.45); }
  50%    { box-shadow:0 0 8px rgba(215,255,62,0.7), 0 0 0 6px rgba(215,255,62,0); }
}

/* Bigger and tighter than the global h1: this is the one headline on the
   site that gets the full display treatment.

   THE SCALE IS SET AGAINST THE COLUMN, NOT THE VIEWPORT, and that is why
   it is 4.4vw rather than the 7vw it was until 2026-08-01. The copy
   column is capped at 52% of the wrap (see above, so it clears the 3D
   mark), so a viewport-relative size grows the type twice as fast as the
   box holding it: at 1440 the headline set at 89.6px into a 587px column
   and broke this sentence over SIX lines, 602px tall, which pushed the
   lead paragraph and both CTAs entirely below the fold and left a bay of
   dead black between the hero and the section under it. That reads as
   forgotten whitespace rather than as composure, which is the opposite of
   what the blackspace is for. At 4.4vw the same line sets over four and
   the whole hero — eyebrow through buttons — fits one screen.

   If you change the 52% cap, re-check this number: the two are one
   decision, and only their RATIO controls how the headline breaks. */
.hero h1{
  font-size:clamp(2.5rem,4.4vw,4rem);
  line-height:1.0; font-weight:600;
  letter-spacing:-0.038em; margin:2px 0 6px;
  max-width:18ch;
}
/* The sheen highlight is WHITE now, not the old gold #F2D48C. A warm
   highlight over lime turned the sweep muddy green-brown at the
   midpoint; white just reads as light passing over it. */
.hero h1 .accent{
  color:var(--acc-text);
  background:linear-gradient(100deg, var(--acc) 40%, #FFFFFF 50%, var(--acc) 60%);
  background-size:250% 100%;
  -webkit-background-clip:text; background-clip:text;
  -webkit-text-fill-color:transparent;
  animation:sheen 7s ease-in-out infinite;
}
@keyframes sheen{
  0%, 55%, 100%{ background-position:120% 0; }
  75%{ background-position:-30% 0; }
}
@media (prefers-reduced-motion:reduce){
  .hero h1 .accent{ animation:none; background:none; -webkit-text-fill-color:var(--acc-text); color:var(--acc-text); }
  .hero .eyebrow .p{ animation:none; }
  .hero .spotlight{ display:none; }
}
.hero p.lead{ color:var(--dim); font-size:1.06rem; line-height:1.65; max-width:58ch; margin:0 0 10px; }
.hero-cta{ display:flex; gap:12px; flex-wrap:wrap; margin-top:10px; }

/* ==== THE NUMBERS ===================================================
   Cirform's treatment: a full-screen panel that PINS while the page
   keeps scrolling, with the figures swiping sideways through it.

   THE PIN IS PURE CSS AND THAT IS THE WHOLE TRICK. `.numstage` is a tall
   wrapper; `.numpin` inside it is `position:sticky; top:0; height:100vh`.
   The browser holds the panel still for exactly the wrapper's surplus
   height, natively — no scroll handler writing position:fixed, no
   placeholder element to stop the page jumping when the pin engages, and
   it still works with JS switched off (you get a static full-screen
   panel showing the first figure). js/numbers.js only reads the
   progress and translates the track.

   These three stats used to be `.hero-stats`. They were moved rather
   than duplicated — the hero needed the room back for the 3D mark.

   HONESTY NOTE, because the reference invites the opposite: cirform's
   version reads "3M users worldwide". Scoutline has no traction to
   claim, and this is the same page that promises 0 fabricated facts. The
   SHAPE is borrowed; every figure here is true and stated elsewhere on
   the site. The true value is always the element's own text content and
   data-count-* only drives the animation, so a script failure shows the
   real number rather than a partial one. */
.numstage{ position:relative; height:265vh; }
.numpin{
  position:sticky; top:0; height:100vh; min-height:540px;
  display:flex; flex-direction:column;
  align-items:center; justify-content:center; gap:clamp(24px,4vh,54px);
  overflow:hidden; isolation:isolate;
}
.numpin-head{ display:flex; flex-direction:column; align-items:center; width:100%; }

.numbers-tag{
  display:inline-flex; align-items:center; gap:9px;
  font-family:var(--font-mono); font-size:0.62rem; font-weight:500;
  text-transform:uppercase; letter-spacing:0.16em; color:var(--dim);
  border:1px solid var(--line-strong); border-radius:999px;
  padding:8px 18px; background:var(--surface);
  position:relative; z-index:2;
}
.numbers-tag .p{
  width:5px; height:5px; border-radius:50%; background:var(--acc);
  box-shadow:0 0 8px rgba(215,255,62,0.7); flex-shrink:0;
}

/* The drop line and the arc are one continuous gesture: the line falls
   from the pill, the arc springs from where it lands. Same 2px dotted
   stroke on both so they read as one drawn element rather than two. */
.numbers-arc{
  position:relative; width:100%;
  height:clamp(70px,9vh,132px); margin-top:-1px; pointer-events:none;
}
.numbers-arc .drop{
  position:absolute; top:0; left:50%; width:0; height:36%;
  border-left:2px dotted var(--line-strong); transform:translateX(-1px);
}
.numbers-arc svg{ position:absolute; inset:auto 0 0; width:100%; height:76%; display:block; }
.numbers-arc path{
  fill:none; stroke:var(--line-strong); stroke-width:2;
  stroke-dasharray:0.1 9; stroke-linecap:round;
  /* preserveAspectRatio="none" stretches the stroke horizontally, which
     turns round dots into stretched dashes on a wide viewport.
     vector-effect keeps the stroke in screen space so they stay round. */
  vector-effect:non-scaling-stroke;
}

/* The padding is what centres the FIRST and LAST slide: half the
   viewport minus half a slide. Without it the track would start with
   slide one flush left and end with slide three flush right, and the
   swipe would begin and end off-centre. */
/* `align-self:stretch` is load-bearing, not tidying. `.numpin` is a
   centring flex column, so by default it shrink-wraps this track to its
   max-content width (2566px) and then CENTRES that — which offsets the
   whole row left by half the overflow and exactly cancels the centring
   padding below. Measured before the fix: at scroll progress 0 the
   SECOND slide sat dead centre and the first was 563px off to the left.
   Stretching the track to the pin's width makes its left edge the pin's
   left edge, so the padding does what it says. */
.numtrack{
  --sw:min(520px,72vw);
  align-self:stretch; width:100%;
  display:flex; align-items:stretch; gap:clamp(20px,3vw,44px);
  padding-inline:calc(50vw - var(--sw) / 2);
  will-change:transform;
}
.numslide{
  flex:0 0 var(--sw); min-width:0;
  border-radius:var(--r-lg); padding:clamp(30px,4vh,52px) clamp(24px,3vw,38px);
  display:flex; flex-direction:column; justify-content:center;
  text-align:center;
}
.num-n{
  font-family:var(--font-mono); font-weight:400;
  font-size:clamp(2.8rem,7vw,6rem);
  line-height:0.95; letter-spacing:-0.06em;
  margin-bottom:18px; white-space:nowrap;
  /* Top-weighted fade. The reference fades its figures almost away,
     which is fine for decoration; these are real claims, so full
     strength is held through the top half and only drops at the
     baseline — vast, but still legible. */
  background:linear-gradient(180deg,
    var(--text) 0%, var(--text) 48%, rgba(244,245,247,0.22) 100%);
  -webkit-background-clip:text; background-clip:text;
  -webkit-text-fill-color:transparent;
}
.num-l{
  font-family:var(--font-mono); font-size:0.64rem;
  text-transform:uppercase; letter-spacing:0.11em; line-height:1.65;
  color:var(--dim); max-width:26ch; margin:0 auto;
}
.num-foot{
  display:block; margin:10px auto 0; font-size:0.68rem;
  color:var(--faint); font-family:var(--font-body);
  letter-spacing:0; text-transform:none; max-width:28ch;
}

.numdots{ display:flex; gap:8px; align-items:center; }
.numdots i{
  width:6px; height:6px; border-radius:50%;
  background:var(--line-strong);
  transition:width .35s cubic-bezier(.16,1,.3,1), background .35s ease, border-radius .35s ease;
}
.numdots i.on{ width:22px; border-radius:3px; background:var(--acc); }

/* ---- the opt-out ---------------------------------------------------
   A pinned, scroll-hijacked panel is hostile if you did not ask for it,
   and it fights a phone's own gestures. In BOTH cases the stage becomes
   an ordinary stacked section — every figure simply on the page, in
   order. js/numbers.js checks the same 760px breakpoint and returns
   early; if these two ever disagree, the script translates a track that
   is no longer a row and the slides pile up on each other. */
@media (prefers-reduced-motion:reduce), (max-width:759px){
  .numstage{ height:auto; }
  .numpin{
    /* `relative`, NOT `static` — AND THAT ONE WORD IS THE GLOW FIX.
       (2026-08-05. The `overflow:clip` below was added on 2026-08-04 to
       stop this same leak and DID NOTHING, because of this line.)

       `overflow` on an element only clips descendants whose CONTAINING
       BLOCK CHAIN passes through it. `.aurora` is `position:absolute`,
       so its containing block is the nearest POSITIONED ancestor — and
       with `.numpin` set to `static` here, that ancestor was `.numstage`
       (`position:relative`, `overflow:visible`). So the aurora was
       laid out against `.numstage`, clipped by `.numstage`, and
       `.numpin`'s clip was never in the chain at all. Desktop was fine
       purely by accident: up there `.numpin` is `position:sticky`, which
       IS positioned, so the clip applied and the bug was mobile-only.

       Measured at 393px: the bloom painted ~200px above `.numpin`, over
       the bottom third of the light `.scan.invert` panel above it,
       washing lavender and blue across its own caption.

       `relative` with no offsets lays out identically to `static` — it
       only restores `.numpin` as the containing block, which is what
       both the clip and the aurora's own `inset:-20% -10% auto` were
       always written to assume ("a fifth of its own height above ITS
       container"). Don't put `static` back to "undo" the sticky; the
       sticky is already undone by `relative`. */
    position:relative; height:auto; min-height:0;
    /* `clip` rather than `hidden`: same clipping, but it does not make
       this a scroll container, so nothing here can start scrolling or
       break a sticky descendant later. */
    padding:clamp(72px,9vh,110px) 0; overflow:clip;
  }
  .numtrack{
    flex-direction:column; padding-inline:26px; gap:44px;
    transform:none !important;
  }
  .numslide{ flex:none; width:100%; }
  .numdots{ display:none; }
  .numbers-arc{ height:64px; }
  .num-n{ font-size:clamp(2.6rem,14vw,4rem); }
}

/* ---- scan strip ---------------------------------------------------
   Four result cards, presented as instrument readouts: mono status line,
   grotesk business name, hairline rule, mono outcome. The card is a
   large-radius panel (--r-lg) because it is a piece of media rather than
   a control. */
/* Only applies when .scan is NOT an inverted panel — section.invert sets
   its own symmetric padding, and it is later in the file so it wins. */
.scan{ padding:clamp(72px,9vh,120px) 0; }
/* `perspective` is gone: it only ever existed to give the cursor-tracked
   rotateX/rotateY a vanishing point, and there is no rotation any more.
   Left in place it would still force a 3D rendering context on every
   card for nothing. */
.scan-row{ display:grid; grid-template-columns:repeat(4,1fr); gap:12px; }
/* THE HOVER IS PURE CSS AND NO JAVASCRIPT TOUCHES IT.
   Rewritten 2026-08-01 after three cursor-tracking tilt implementations
   were each reported as laggy or choppy — the full post-mortem is in
   js/main.js where that code used to live.

   The card now simply comes toward you: a lift, a small scale, and a
   shadow that grows to match. Because nothing runs on pointermove, this
   cannot stutter — the browser hands one transform transition to the
   compositor and interpolates it there, off the main thread, at the
   display's own refresh rate. It is smooth by construction rather than
   by tuning, which is the point.

   The shadow moves WITH the lift, and that is what sells it as depth
   rather than as a size change: a card 6px off the page casts a longer,
   softer, more offset shadow than one lying flat on it.

   `translateZ(0)` promotes the card to its own compositor layer up
   front, so the first hover doesn't pay for a layer promotion mid
   transition — the usual cause of a single dropped frame on the way in.

   Timings are asymmetric on purpose: .22s coming forward reads as
   responsive, .4s going back reads as settling. Equal timings in both
   directions feel mechanical. */
.pin-card{
  background:var(--raised); border:1px solid var(--line);
  border-radius:var(--r-lg); padding:22px 20px;
  opacity:0.35; filter:saturate(0.4);
  transform:translateZ(0);
  transition:opacity .6s ease var(--d,0s), filter .6s ease var(--d,0s),
             transform .4s cubic-bezier(.16,1,.3,1),
             box-shadow .4s ease,
             border-color .28s ease, background .28s ease;
  will-change:transform;
}
.scan-row.lit .pin-card{ opacity:1; filter:none; }
.pin-card:hover{
  transform:translateY(-6px) scale(1.022);
  border-color:var(--acc-line); background:var(--raised-2);
  box-shadow:0 26px 54px -20px rgba(0,0,0,0.9),
             0 4px 14px -8px rgba(0,0,0,0.6);
  transition:transform .22s cubic-bezier(.22,1,.36,1),
             box-shadow .22s ease,
             border-color .16s ease, background .16s ease;
}
/* One block, not two. The second was left behind when the hover was
   rewritten; `opacity:1; filter:none` is the load-bearing half — without
   it a reduced-motion visitor keeps the un-lit 0.35 opacity forever,
   because the reveal that normally clears it is itself a transition. */
@media (prefers-reduced-motion:reduce){
  .pin-card{ opacity:1; filter:none; transition:none; }
  .pin-card:hover{ transform:none; transition:none; }
}
.pin-card .top{ display:flex; align-items:center; gap:9px; margin-bottom:18px; }
.pin-dot{
  width:6px; height:6px; border-radius:50%; background:var(--acc);
  box-shadow:0 0 9px rgba(215,255,62,0.7); flex-shrink:0;
}
.scan-row.lit .pin-dot{ animation:pin-blink 3.2s ease-in-out infinite; }
@keyframes pin-blink{
  0%,100%{ box-shadow:0 0 9px rgba(215,255,62,0.7); }
  50%    { box-shadow:0 0 16px rgba(215,255,62,1); }
}
@media (prefers-reduced-motion:reduce){ .scan-row.lit .pin-dot{ animation:none; } }
.pin-card .status{
  font-family:var(--font-mono); font-size:0.6rem; font-weight:500;
  text-transform:uppercase; letter-spacing:0.13em; color:var(--acc-text);
}
.pin-card .name{
  font-family:var(--font-display); font-size:1.02rem; font-weight:600;
  letter-spacing:-0.02em; margin-bottom:5px; line-height:1.25;
}
.pin-card .meta{ font-family:var(--font-mono); font-size:0.68rem; color:var(--dim); letter-spacing:-0.01em; }
.pin-card .after{
  margin-top:16px; padding-top:14px; border-top:1px solid var(--line);
  font-family:var(--font-mono); font-size:0.64rem; color:var(--dim);
  text-transform:uppercase; letter-spacing:0.1em;
}
.scan-caption{
  margin-top:18px; font-size:0.68rem; color:var(--faint);
  font-family:var(--font-mono); letter-spacing:0.02em;
}
@media (max-width:900px){ .scan-row{ grid-template-columns:repeat(2,1fr); } }
@media (max-width:520px){ .scan-row{ grid-template-columns:1fr; } }

/* ---- globe -------------------------------------------------------- */
.globe-section{ overflow:hidden; }
/* minmax(0,1fr), NOT 1fr. A bare `1fr` track is `minmax(auto,1fr)`, and
   that `auto` minimum is the track's MIN-CONTENT — which, with a <canvas>
   in the column, is the canvas's intrinsic width. Measured at 320px: the
   grid box was 268px and its single track resolved to 312px, pushing the
   globe 18px past the viewport. `min-width:0` on the ITEM is not enough
   on its own, because the track is sized before the item is laid out
   into it — the floor has to come off the TRACK. */
.globe-grid{
  display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  gap:56px; align-items:center;
}
/* `min-width:0` is load-bearing, not tidying. This is a GRID ITEM, and
   grid (and flex) items default to `min-width:auto` — they refuse to
   shrink below their content's min-content width. The content is a
   <canvas>, which has an intrinsic size, so at 320px the stage measured
   312px wide inside a 268px column and pushed 18px past the viewport.
   Found by the width sweep, not by eye; it is the same class of bug as
   the `.app-main` / leads-table overflow fixed on 2026-07-29. */
.globe-stage{
  position:relative; aspect-ratio:1/1; width:100%; max-width:520px;
  min-width:0;
  margin:0 auto; display:flex; align-items:center; justify-content:center;
}
.globe-stage::after{ /* soft ambient halo behind the wireframe */
  content:""; position:absolute; inset:12%;
  border-radius:50%; pointer-events:none;
  background:radial-gradient(circle at 42% 34%, rgba(139,144,150,0.14), transparent 62%);
}
.globe-stage canvas{ position:relative; z-index:1; width:100%; height:100%; display:block; }
.globe-legend{
  position:absolute; bottom:2%; left:50%; transform:translateX(-50%);
  font-family:var(--font-mono); font-size:0.68rem;
  color:var(--dim); opacity:0.6; white-space:nowrap; z-index:2;
}
@media (max-width:900px){
  .globe-grid{ grid-template-columns:minmax(0,1fr); gap:32px; }
  .globe-stage{ max-width:380px; order:-1; }
}

/* ---- section shell ------------------------------------------------
   THREE section weights, and no more than three. Before 2026-07-29 the
   rhythm was carried by HUE (green / maroon / cream), which is exactly
   what stopped working once the site went single-palette. It's carried by
   the ladder instead, so the two classes were renamed to say what they do
   rather than what colour they used to be:

     (default)   --canvas    the base. Most sections.
     .band       --surface   one rung up. Was `.deep` (maroon).
     .lit        --canvas    back down to the base, but framed by a gold
                             hairline and washed with a little gold —
                             reserved for the one section on a page that
                             is making the argument. Was `.paper` (cream,
                             dark text). It is NOT a light section any
                             more; everything inside it that assumed
                             dark-on-light has been redone.

   Two adjacent sections must never share a weight — that's what turns a
   scroll into one flat sheet. `.lit` is --canvas rather than --surface
   for exactly this reason, and it was caught in a render, not in
   review: every page that uses `.lit` puts it immediately next to a
   `.band` (index, how-it-works, sample-output) or immediately above the
   footer (why-us, whose gradient starts at --surface). At --surface it
   shared an edge with its neighbour on all five, and the hairline
   between them was the only thing saying a new section had started. */
/* Padding grew from a flat 80px to a fluid 88–150px. The single biggest
   lever on "does this look expensive" is how much empty space sits
   between one idea and the next, and 80px was doing the job of a
   divider rather than of a pause. */
section{ padding:clamp(88px,12vh,150px) 0; position:relative; }
section.band{ background:var(--surface); }
section.lit{
  background:var(--canvas);
  border-top:1px solid var(--acc-line);
  border-bottom:1px solid var(--acc-line);
}
/* The wash is on a pseudo so it sits under the content and can't be
   inherited by anything; `overflow:hidden` is deliberately NOT set, so a
   card that lifts on hover still lifts over the section edge. */
section.lit::before{
  content:""; position:absolute; inset:0; pointer-events:none; z-index:0;
  background:
    radial-gradient(900px 300px at 18% 0%, rgba(215,255,62,0.06), transparent 72%),
    linear-gradient(180deg, rgba(215,255,62,0.03), transparent 38%);
}
section.lit > *{ position:relative; z-index:1; }
/* An inverted section is a ROUNDED PANEL INSET INTO THE PAGE, not a
   full-bleed band. Straight to the edges it cut the scroll in half with
   two hard horizontal lines and read as a different website; as a panel
   with the site's own --r-lg radius and a margin of dark left visible
   down both sides, it reads as a card laid ON the page — the AeroSense
   treatment. That is what buys the continuity: the black never actually
   stops, so there is no seam to notice.

   Padding is SYMMETRIC here, deliberately. `.scan` used to carry
   `padding:0 0 150px` from when it sat directly under the hero and wanted
   to hug it; inside a panel that put the cards flush against the top edge
   with 155px of empty white underneath. Measured, not eyeballed. */
section.invert{
  border-top:0; border-bottom:0;
  border-radius:clamp(20px,2.2vw,34px);
  margin:0 clamp(10px,1.6vw,26px);
  padding:clamp(64px,8vh,104px) 0;
  overflow:hidden;
}
section.invert::before{ content:none; }

/* ---- section head --------------------------------------------------
   Two-column on wide screens: the label and headline sit left, the
   supporting paragraph right, the way AeroSense and Pulsar set a
   section. It stops a 58ch headline and a 58ch paragraph from stacking
   into one undifferentiated block of text. `.section-head.centred` (used
   by why-us's closing argument) opts back out. */
.section-head{
  display:grid; grid-template-columns:minmax(0,1.1fr) minmax(0,0.9fr);
  gap:16px 56px; align-items:end;
  max-width:none; margin-bottom:clamp(40px,6vh,64px);
}
.section-head .eyebrow{ grid-column:1/-1; margin-bottom:6px; }
.section-head h2{
  grid-column:1; margin:0;
  font-size:clamp(1.85rem,3.6vw,3rem); font-weight:600;
  letter-spacing:-0.032em; line-height:1.06; max-width:16ch;
}
.section-head p{
  grid-column:2; margin:0; align-self:end;
  color:var(--dim); font-size:1rem; line-height:1.65; max-width:46ch;
}
.section-head p + p{ grid-column:2; margin-top:12px; }
/* minmax(0,1fr), not 1fr — same reason as .globe-grid above. A bare
   `1fr` floors the track at its content's min-content width, and this
   grid contains `.section-cta` buttons which are `white-space:nowrap`;
   "Find businesses without a website" is ~312px unbreakable, so at 320px
   the track resolved to 312 inside a 268px box and pushed the whole
   section head past the viewport. Both halves of that are fixed: the
   track can shrink here, and the buttons can wrap (see --- responsive). */
@media (max-width:860px){
  .section-head{ grid-template-columns:minmax(0,1fr); gap:14px; }
  .section-head h2, .section-head p, .section-head p + p{ grid-column:1; max-width:none; }
}
/* ==== A NESTED .section-head MUST NOT SPLIT AGAIN =====================
   `.section-head` is a two-column grid — heading left, supporting copy
   right — and that is correct for a section head spanning the whole well.
   Inside `.globe-grid` it is ALREADY in a half-width column, so it split
   that half in two and gave the paragraph a 227px measure (about 30
   characters), starting 334px to the right of the heading and 163px above
   it. Measured at 1280px, reported as the section looking "skewed
   rightwards", and it was.

   The CTA row inside it was fixed for this same reason on 2026-08-01
   (`grid-column:1/-1`) — that patched the symptom one child at a time.
   This fixes the cause: a section head in a narrow column is one column. */
.globe-grid .section-head{
  grid-template-columns:minmax(0,1fr);
  gap:14px; align-items:start;
}
.globe-grid .section-head h2,
.globe-grid .section-head p,
.globe-grid .section-head p + p{ grid-column:1; max-width:none; }
.globe-grid .section-head h2{ max-width:18ch; }

.section-head.centred{
  grid-template-columns:1fr; justify-items:center; text-align:center;
}
.section-head.centred h2, .section-head.centred p{ grid-column:1; max-width:52ch; }
/* flex + gap, not margin-left on the second button. why-us.html carried an
   inline `margin-left:12px` (alongside two colour overrides that existed
   only to make a ghost button legible on the old cream section) — which
   meant a wrapped second button on a phone kept a stray left indent. */
/* `grid-column:1/-1` and `flex-shrink:0` are both fixing the same real
   bug, found 2026-08-01. When `.section-head` became a two-column grid,
   the CTA row auto-placed into the NARROW left column — and inside the
   globe section that column is only half the page. Flex items shrink
   below their content by default, and `.btn` is `white-space:nowrap`, so
   "Find businesses without a website" was laid out at 264px while needing
   286px and the last characters were simply cut off. Measured
   (scrollWidth 286 vs clientWidth 262), not noticed by eye.

   So: the row spans the full width of the section head, and a button
   refuses to be compressed below its own text. It wraps to a second line
   instead, which `flex-wrap` already allowed for. */
.section-cta{
  grid-column:1/-1;
  margin-top:36px; display:flex; gap:12px; flex-wrap:wrap;
}
.section-cta .btn{ flex-shrink:0; max-width:100%; }

/* Opt-in centring for a section that closes a page rather than opening
   one. `.eyebrow` is `width:fit-content` so it needs the auto margins;
   text-align alone would leave it stuck to the left. */
.section-head.centred{ margin-left:auto; margin-right:auto; text-align:center; }
.section-head.centred .eyebrow{ margin-left:auto; margin-right:auto; }
.section-cta.centred{ justify-content:center; }

/* ---- pipeline -----------------------------------------------------
   Was a four-up grid of boxed tiles. It is now a RAIL: four columns
   divided by hairlines, each headed by its index, with no box around
   anything. The four stages are a sequence, and boxing them made them
   read as four unrelated options — the Optikka "01. Pipeline" treatment
   says "these happen in order" without adding a single arrow.

   The top rule spans the whole rail; each column carries its own left
   hairline except the first, so the divisions can't double up. */
.pipeline{
  display:grid; grid-template-columns:repeat(4,1fr);
  gap:0; border-top:1px solid var(--line-strong);
}
.stage{
  padding:26px 26px 0 24px; position:relative;
  border-left:1px solid var(--line);
  transition:background .3s ease;
}
.stage:first-child{ border-left:0; padding-left:0; }
/* A short accent tick sitting ON the top rule, above each stage. Only
   the hovered one lights, which is what makes the rail feel alive
   without animating anything. */
.stage::before{
  content:""; position:absolute; top:-1px; left:0; width:32px; height:1px;
  background:var(--acc); opacity:0; transition:opacity .3s ease;
}
.stage:first-child::before{ left:0; }
.stage:hover::before{ opacity:1; }
.stage .k{
  font-family:var(--font-mono); color:var(--acc-text); font-size:0.64rem;
  text-transform:uppercase; letter-spacing:0.13em; margin-bottom:18px;
}
.stage h3{ font-size:1.15rem; margin:0 0 10px; letter-spacing:-0.024em; }
.stage p{ color:var(--dim); font-size:0.9rem; line-height:1.6; margin:0; max-width:30ch; }
@media (max-width:880px){
  .pipeline{ grid-template-columns:repeat(2,1fr); }
  .stage{ padding:24px 20px 24px 22px; }
  .stage:nth-child(3){ border-left:0; padding-left:0; }
  .stage:nth-child(n+3){ border-top:1px solid var(--line); margin-top:8px; padding-top:26px; }
}
@media (max-width:560px){
  .pipeline{ grid-template-columns:1fr; }
  .stage, .stage:first-child{ border-left:0; padding:24px 0; border-top:1px solid var(--line); margin-top:0; }
  .stage:first-child{ border-top:0; padding-top:26px; }
  .stage p{ max-width:none; }
}

/* ---- flowchart ---------------------------------------------------- */
.flowchart{
  display:flex; align-items:stretch; justify-content:space-between;
  gap:0; margin-top:16px; flex-wrap:nowrap;
}
.flow-node{
  background:var(--raised); border:1px solid var(--line);
  border-radius:var(--radius); padding:22px; text-align:center;
  flex:1; min-width:0; position:relative;
  transition:border-color .45s ease, box-shadow .45s ease, transform .45s ease;
}
.flow-node .icon{ font-family:var(--font-mono); font-size:1.1rem; color:var(--dim); margin-bottom:8px; transition:color .45s ease; }
.flow-node h4{ margin:0 0 4px; font-size:0.98rem; color:var(--text); }
.flow-node p{ margin:0; font-size:0.78rem; color:var(--dim); }
.flow-node.lit{
  border-color:var(--acc);
  box-shadow:0 10px 30px -14px var(--acc-glow);
  transform:translateY(-3px);
}
.flow-node.lit .icon{ color:var(--acc-text); }
/* ==== THE CONNECTOR IS A RAIL THAT FILLS, NOT AN ARROW (2026-08-06) ===
   WHAT THIS REPLACES AND WHY. Each gap used to hold a 1.4rem "→" glyph
   with a hairline drawn straight across it and a dot flying over the top.
   Three marks stacked in one 52px gap saying the same single thing, and
   the line ran through the middle of the arrowhead — asked to be removed
   for exactly that reason.

   THE IDEA: ONE CONTINUOUS RAIL, LIT BY THE LEAD AS IT PASSES. The three
   cards are stations on a line. The rail is a dim hairline at rest and
   fills with accent, in order, as each stage completes — so the flow is
   shown by something actually moving ALONG the pipeline rather than by a
   symbol that means "next". A leading dot rides the end of the fill,
   which is the whole of the old travelling dot's job, now attached to the
   thing it is drawing instead of floating above it.

   WHY NO ARROWHEAD AT ALL. The numbers already say the order — 01, 02, 03
   are in the cards and are the loudest thing in them. An arrow next to a
   numbered sequence is a second instruction for a reader who has already
   followed the first. Removing it leaves the gap to do one job.

   THE RAIL NEVER CROSSES A CARD. `.flow-node` is opaque and sits at
   z-index 1, so the rail is only ever visible in the gaps — no clipping
   to maintain, no length to keep in step with the layout, and nothing
   that can reach a heading. That is a property of the stacking order, not
   of a measurement, which is what makes it hold at every width.

   AND IT IS NEVER ROTATED. The stacked layout builds a vertical rail
   directly (see the media query below). The previous connector was a
   horizontal element turned 90deg, and rotating an element sized by
   percentages is what drew a 269px line through all three cards on a
   phone. There is no transform in this component now, so that class of
   fault cannot return. */
.flow-link{
  position:relative; flex:0 0 clamp(30px,4.5vw,64px); align-self:stretch;
}
/* The rail at rest: a dim hairline across the gap, vertically centred. */
.flow-link::before{
  content:""; position:absolute; left:0; right:0; top:50%;
  height:1px; transform:translateY(-50%);
  background:var(--line-strong);
}
/* The fill. `scaleX` off the left edge, so it grows in the direction the
   pipeline runs. The bright head is painted into the gradient's own end
   rather than being a separate element — one box, one animation. */
.flow-link::after{
  content:""; position:absolute; left:0; right:0; top:50%;
  height:1px; transform:translateY(-50%) scaleX(0); transform-origin:left center;
  background:linear-gradient(90deg,
    rgba(215,255,62,0.30) 0%, rgba(215,255,62,0.75) 72%, var(--acc) 100%);
  box-shadow:0 0 10px var(--acc-glow);
  transition:transform .85s cubic-bezier(.4,0,.2,1);
}
.flow-link.go::after{ transform:translateY(-50%) scaleX(1); }
@media (prefers-reduced-motion:reduce){
  .flow-node, .flow-node .icon{ transition:none; }
  .flow-node.lit{ transform:none; }
  /* The rail still shows its completed state — it is information, not
     decoration — it simply arrives rather than travels. */
  .flow-link::after{ transition:none; }
}
/* STACKED: THE RAIL IS BUILT VERTICAL, NOT ROTATED.
   The connector this replaced was a horizontal element turned 90deg, and
   that is precisely what put a 269px line through all three cards on a
   phone: `.flowchart` is `align-items:stretch`, so in column mode the
   element stretched to the full ~354px column width while keeping its
   52px height, and rotating a box does not change the box its own
   percentages were resolved against — a `left:12%; right:12%` hairline
   became 76% of 354px standing on end inside a 52px gap.

   Swapping the axis rather than rotating removes the trap instead of
   tuning around it: every value below is stated in the direction it is
   actually used, so there is nothing left for a rotation to reinterpret.
   Height replaces flex-basis, the hairline runs top-to-bottom, and the
   fill scales on Y from the top — the direction the stacked pipeline
   reads. */
@media (max-width:760px){
  .flowchart{ flex-direction:column; align-items:stretch; }
  .flow-link{ flex:0 0 clamp(26px,7vw,42px); align-self:stretch; }
  .flow-link::before,
  .flow-link::after{
    left:50%; right:auto; top:0; bottom:0;
    width:1px; height:auto;
  }
  .flow-link::before{ transform:translateX(-50%); }
  .flow-link::after{
    transform:translateX(-50%) scaleY(0); transform-origin:center top;
    background:linear-gradient(180deg,
      rgba(215,255,62,0.30) 0%, rgba(215,255,62,0.75) 72%, var(--acc) 100%);
  }
  .flow-link.go::after{ transform:translateX(-50%) scaleY(1); }
}

/* ---- differentiators (why-us) ------------------------------------- */
.diff-grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:14px; margin-top:16px; }
.diff-card{
  background:var(--raised); border:1px solid var(--line);
  border-radius:var(--r-lg); padding:30px 26px;
  position:relative; overflow:hidden;
  transition:transform .3s cubic-bezier(.16,1,.3,1), border-color .25s ease,
             box-shadow .25s ease, background .25s ease;
}
/* A wash that rises from the bottom edge on hover. Sits under the
   content on its own pseudo, so it can tint the card without touching
   the legibility of anything written on it. */
.diff-card::after{
  content:""; position:absolute; inset:auto 0 0; height:60%;
  background:linear-gradient(0deg, var(--acc-soft), transparent);
  opacity:0; transition:opacity .3s ease; pointer-events:none;
}
.diff-card:hover{
  transform:translateY(-5px);
  border-color:var(--acc-line);
  background:var(--raised-2);
  box-shadow:0 22px 50px -26px rgba(0,0,0,0.9);
}
.diff-card:hover::after{ opacity:1; }
.diff-card > *{ position:relative; z-index:1; }
.diff-card .num{
  font-family:var(--font-mono); color:var(--faint);
  font-size:0.64rem; text-transform:uppercase; letter-spacing:0.13em;
  margin-bottom:16px; transition:color .25s ease;
}
.diff-card:hover .num{ color:var(--acc-text); }
.diff-card h3{ font-size:1.12rem; margin:0 0 10px; letter-spacing:-0.024em; }
.diff-card p{ font-size:0.9rem; line-height:1.6; color:var(--dim); margin:0; }
@media (prefers-reduced-motion:reduce){
  .diff-card{ transition:none; }
  .diff-card:hover{ transform:none; }
}
@media (max-width:900px){ .diff-grid{ grid-template-columns:1fr; } }

/* ---- honesty band -------------------------------------------------
   THE LIT CARD, adapted from the supplied `illumination.css` snippet.
   That component is a dark card with a horizontal slit that switches on
   and throws a cone of light down the face. Reproduced here with two
   pseudo-elements instead of the snippet's eight nested divs, because
   the markup is fixed (`.honesty-card` is written in index.html) and
   cannot gain children:

     ::before  the slit — a 1px bar inset from both edges, sitting on the
               card's top rule. Dark at rest, accent and glowing on hover.
     ::after   the cone — a wide, heavily-blurred ellipse hanging below
               the slit at low opacity, which is what sells the slit as a
               light source rather than as a coloured line.

   The snippet drove it from a real toggle; here the trigger is :hover
   plus :focus-within, so it is reachable from the keyboard too. */
.honesty-grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:14px; margin-top:8px; }
.honesty-card{
  position:relative; isolation:isolate; overflow:hidden;
  background:var(--raised); border:1px solid var(--line);
  border-radius:var(--r-lg); padding:34px 24px 26px;
  transition:border-color .4s ease, transform .4s cubic-bezier(.16,1,.3,1),
             box-shadow .4s ease, background .4s ease;
}
.honesty-card::before{
  content:""; position:absolute; top:0; left:22%; right:22%; height:1px;
  background:var(--line-strong); z-index:1;
  transition:background .4s ease, box-shadow .4s ease;
}
.honesty-card::after{
  content:""; position:absolute; top:-14px; left:50%; z-index:-1;
  width:78%; height:150px; transform:translateX(-50%);
  border-radius:50%; filter:blur(34px);
  background:radial-gradient(ellipse at 50% 0%, var(--acc), transparent 68%);
  opacity:0; transition:opacity .45s ease; pointer-events:none;
}
.honesty-card:hover,
.honesty-card:focus-within{
  border-color:var(--acc-line); transform:translateY(-4px);
  background:var(--raised-2);
  box-shadow:0 22px 50px -26px rgba(0,0,0,0.9);
}
.honesty-card:hover::before,
.honesty-card:focus-within::before{
  background:var(--acc);
  box-shadow:0 0 12px 1px var(--acc-glow);
}
.honesty-card:hover::after,
.honesty-card:focus-within::after{ opacity:0.17; }
@media (prefers-reduced-motion:reduce){
  .honesty-card, .honesty-card::before, .honesty-card::after{ transition:none; }
  .honesty-card:hover{ transform:none; }
}
/* "01 · GENERATED SITES" — the indexed function label from the
   "ai-powered editing" reference ("01 Function", "02 Function"). Done
   with a CSS counter rather than by typing numbers into the markup, so
   reordering or inserting a card renumbers the set automatically and no
   hand-typed number can fall out of step with its position.
   decimal-leading-zero is what gives "01" rather than "1". */
.honesty-grid{ counter-reset:honesty; }
.honesty-card .k{
  font-family:var(--font-mono); font-size:0.62rem; color:var(--acc-text);
  letter-spacing:0.14em; text-transform:uppercase; margin-bottom:14px;
}
.honesty-card .k::before{
  counter-increment:honesty;
  content:counter(honesty,decimal-leading-zero) " \00B7 ";
  color:var(--faint);
}
.honesty-card h3{ font-size:1.08rem; margin:0 0 10px; color:var(--text); letter-spacing:-0.024em; }
.honesty-card p{ font-size:0.89rem; line-height:1.6; color:var(--dim); margin:0; }
@media (max-width:900px){ .honesty-grid{ grid-template-columns:1fr; } }

/* ---- click-to-load video facade (guides) ---------------------------
   NOT a live iframe. A YouTube embed sets cookies and runs their scripts
   on every page view whether or not anyone watches, so nothing is
   requested until a real click — same decision as the post-build screen
   in js/handoff.js. The 16:9 box is reserved up front so swapping the
   button for the iframe does not shift the page under the reader. */
.vid-facade{
  margin:20px auto 0;
  max-width:760px;
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:10px;
  padding:26px 18px;
  border:1px dashed var(--line-strong);
  border-radius:10px;
  background:var(--surface);
  text-align:center;
}
.vid-facade .out-sub{ font-size:.86rem; color:var(--dim); }
.vid-facade iframe{
  width:100%;
  aspect-ratio:16 / 9;
  height:auto;
  border:0;
  border-radius:8px;
  display:block;
}
/* Once the iframe is in, the dashed placeholder chrome is just noise. */
.vid-facade:has(iframe){ padding:0; border:0; background:none; }

/* ---- proof (sample-output) ---------------------------------------- */
.proof-grid{ display:grid; grid-template-columns:1.1fr 1fr; gap:48px; align-items:center; }
/* THE ONE LIGHT THING LEFT ON THE SITE, and deliberately so. This is a
   mock browser showing a GENERATED CUSTOMER SITE — a real one is a light
   page, and darkening it to match Scoutline's own chrome would be showing
   the visitor something the product doesn't actually produce. Its own
   chrome was warm cream (#EFEDE5/#D2CFC5) to sit on the old paper
   section; that's now neutral grey so the frame doesn't read warm against
   a cool graphite page. Everything OUTSIDE the frame is dark. */
.browser-frame{
  background:#fff; border-radius:12px; overflow:hidden;
  border:1px solid rgba(244,245,247,0.14);
  box-shadow:0 30px 70px -28px rgba(0,0,0,0.85);
}
.browser-bar{ display:flex; align-items:center; gap:6px; padding:10px 12px; background:#E7E9EC; }
.browser-bar .d{ width:8px; height:8px; border-radius:50%; background:#C3C8CE; }
/* #5C646E, not #7A828C. Pre-existing AA failure, unrelated to the 2026-08-04
   glow work but found by the same measurement: grey-on-light-grey inside the
   browser mock measured 3.12:1 against a 4.5 requirement. This is the one
   deliberately-light element on the site, so it is the one place a LIGHTER
   grey makes things worse — it needed darkening. Now 5.1:1. */
.browser-bar .url{ margin-left:8px; font-size:0.72rem; color:#5C646E; font-family:var(--font-mono); }
.mini-hero{ background:linear-gradient(180deg, #2B3545 0%, #171E28 100%); padding:26px 22px; }
.mini-hero h4{ color:#fff; font-size:1.3rem; margin:0 0 8px; }
.mini-hero .badge{
  display:inline-block; background:rgba(255,255,255,0.92); color:#22262A;
  font-size:0.72rem; font-weight:600; padding:5px 10px; border-radius:4px;
}
.mini-body{ padding:20px 22px; display:grid; grid-template-columns:repeat(2,1fr); gap:1px; background:#C9CED4; }
.proof-cap{
  margin:10px 2px 0; font-family:var(--font-mono);
  font-size:0.72rem; color:var(--faint);
}
.mini-tile{ background:#F1F3F5; padding:14px; font-size:0.72rem; font-weight:600; color:#22262A; }
.proof-copy ul{ list-style:none; padding:0; margin:22px 0 0; display:grid; gap:14px; }
.proof-copy li{ display:flex; gap:10px; font-size:0.95rem; color:var(--dim); }
.proof-copy li .tick{ color:var(--acc-text); font-weight:700; flex-shrink:0; }

/* brief.md panel */
.brief-panel{
  background:var(--raised); border:1px solid var(--line);
  border-radius:var(--radius); padding:26px 24px;
  font-family:var(--font-mono); font-size:0.8rem; line-height:1.8;
  color:var(--dim);
}
.brief-panel .fname{ color:var(--acc-text); font-size:0.75rem; margin-bottom:14px; display:block; }
.brief-panel .flag{ color:var(--acc-text); }
.brief-panel .dim{ opacity:0.6; }

/* ---- pricing ------------------------------------------------------ */
.discount-banner{
  display:inline-flex; align-items:center; gap:8px;
  background:rgba(215,255,62,0.12); border:1px solid rgba(215,255,62,0.35);
  color:var(--acc-text); font-size:0.82rem; font-weight:600;
  padding:8px 16px; border-radius:999px; margin-bottom:24px;
}
.pricing-controls{ display:flex; align-items:center; gap:18px; flex-wrap:wrap; margin-bottom:8px; }
.billing-toggle{
  display:inline-flex; align-items:center;
  background:var(--raised); border:1px solid var(--line);
  border-radius:999px; padding:5px; position:relative;
}
.billing-toggle button{
  position:relative; z-index:1; background:none; border:none; cursor:pointer;
  padding:9px 18px; border-radius:999px; font-size:0.85rem; font-weight:600;
  color:var(--dim); font-family:var(--font-body);
  transition:color .25s ease;
}
.billing-toggle button[aria-pressed="true"]{ color:#14100B; }
.billing-toggle .thumb{
  position:absolute; top:5px; bottom:5px; border-radius:999px;
  background:var(--acc); z-index:0;
  transition:left .3s cubic-bezier(.4,1.4,.4,1), width .3s cubic-bezier(.4,1.4,.4,1);
}
@media (prefers-reduced-motion:reduce){ .billing-toggle .thumb{ transition:none; } }
.currency-badge{
  font-size:0.75rem; color:var(--dim); font-family:var(--font-mono);
  border:1px solid var(--line); border-radius:999px; padding:6px 12px;
  opacity:0; transition:opacity .4s ease;
}
.currency-badge.show{ opacity:0.85; }

.price-grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:18px; margin-top:24px; }
.price-card{
  background:var(--raised); border:1px solid var(--line);
  border-radius:12px; padding:28px 24px; display:flex; flex-direction:column;
  transition:transform .25s ease, box-shadow .25s ease;
}
.price-card:hover{ transform:translateY(-4px); box-shadow:0 18px 44px -24px rgba(0,0,0,0.8); }
@media (prefers-reduced-motion:reduce){ .price-card:hover{ transform:none; } }
.price-card.feat{ border-color:var(--acc); box-shadow:0 0 0 1px rgba(215,255,62,0.25); }
.price-card .tag{
  display:inline-block; width:fit-content; background:rgba(215,255,62,0.15);
  color:var(--acc-text); font-size:0.72rem; font-weight:600;
  padding:4px 10px; border-radius:6px; margin-bottom:14px;
}
.price-card .tag.quiet{ background:rgba(139,144,150,0.12); color:var(--dim); }
.price-card h3{ font-size:1.05rem; margin:0 0 4px; }
.price-card .amt{
  font-family:var(--font-mono); font-size:1.9rem; font-weight:600;
  margin:10px 0 4px; display:flex; align-items:baseline; gap:10px; flex-wrap:wrap;
}
.price-card .amt .struck{
  text-decoration:line-through; color:var(--dim);
  font-size:1.1rem; font-weight:500;
}
.price-card .amt .per{ font-size:0.9rem; color:var(--dim); font-weight:500; }
.price-card .cred{ color:var(--dim); font-size:0.85rem; margin-bottom:10px; font-family:var(--font-mono); }
/* MISALIGNMENT FIX, 2026-07-29. The three cards' feature lists didn't
   line up, and it was two faults compounding:

   1. `.payback` only has copy on the Solo card, and the 2.4em reserved
      for it was less than the ~3.1em two wrapped lines actually take —
      so the slot silently failed at its one job.
   2. `margin:auto 0 0` on the list pinned it to the BOTTOM of the card,
      so a card with three bullets started its list higher than a card
      with two. Bottom-aligning the button is what was wanted; bottom-
      aligning the list is what was written.

   Now: the slot reserves enough for two lines, and `margin-bottom:auto`
   pushes the BUTTON down while leaving the list where it falls. Every
   row above the list is fixed-height, so the lists start level. */
.price-card .payback{
  font-size:0.78rem; color:var(--acc-text); margin-bottom:18px;
  min-height:3.2em;
}
.price-card ul{ list-style:none; padding:0; margin:0 0 auto; display:grid; gap:10px; }
.price-card .plan-cta{ margin-top:22px; }
.price-card li{ font-size:0.85rem; color:var(--dim); padding-left:20px; position:relative; }
.price-card li::before{ content:"✓"; position:absolute; left:0; color:var(--dim); opacity:0.7; }
.price-note{ margin-top:22px; font-size:0.8rem; color:var(--dim); max-width:78ch; }
.price-note + .price-note{ margin-top:10px; opacity:0.8; }

/* ==== PLAN COMPARISON, added 2026-07-31, restructured 2026-08-06 ====
   Used to be one table: seventeen rows, four columns, most of them
   ticked "yes" in all three. On a phone that meant scrolling sideways
   to confirm something that was true of every plan. The ten rows that
   never vary are now `.cmp-included` — a single list, no comparison
   needed — and `.cmp` holds only the eight that actually differ.

   Short enough now that mobile doesn't need horizontal scroll at all:
   below 640px each row becomes its own block (feature name, then three
   `data-label`-prefixed values stacked underneath) instead of a wide
   row. Ticks are still `aria-hidden` with real "Yes"/"No" text in
   .visually-hidden — a screen reader announcing "check mark" forty
   times is noise; "Yes" is the actual answer. */
.cmp-wrap{ margin-top:44px; }
.cmp-title{
  font-size:1.05rem; font-weight:400; margin:0 0 14px; color:var(--text);
}
.cmp-title-differs{ margin-top:32px; }
.cmp-included{
  list-style:none; margin:0; padding:0; display:grid; gap:10px;
  border:1px solid var(--line-strong); border-radius:12px;
  background:var(--surface); padding:18px 20px;
}
.cmp-included li{
  font-size:0.85rem; color:var(--text); padding-left:24px; position:relative;
}
.cmp-included li::before{
  content:"✓"; position:absolute; left:0; color:var(--acc-text);
}
.cmp-scroll{
  overflow-x:auto; border:1px solid var(--line-strong); border-radius:12px;
  background:var(--surface);
}
.cmp{
  width:100%; min-width:600px; border-collapse:collapse;
  font-size:0.85rem;
}
.cmp thead th{
  position:sticky; top:0; z-index:1;
  background:var(--raised); color:var(--text);
  font-family:var(--font-display); font-weight:400;
  text-align:center; padding:14px 12px; white-space:nowrap;
  border-bottom:1px solid var(--line-strong);
}
.cmp thead th:first-child{ text-align:left; }
.cmp tbody th[scope="row"]{
  text-align:left; font-weight:400; color:var(--dim);
  padding:11px 14px; position:sticky; left:0;
  background:var(--surface); min-width:230px;
}
.cmp td{
  text-align:center; padding:11px 12px; color:var(--text);
  border-left:1px solid rgba(244,245,247,0.05);
}
.cmp tbody tr + tr{ border-top:1px solid rgba(244,245,247,0.05); }
.cmp .y{ color:var(--acc-text); font-size:1rem; }
/* An absence is quiet, not alarming — it is a plan boundary, not a fault,
   so it never gets the red that .ai-fail.ours uses. */
.cmp .n{ color:var(--faint); }
.cmp td.sm{ font-size:0.78rem; color:var(--dim); }
.cmp td.mono{ font-family:var(--font-mono); letter-spacing:0.01em; }
.cmp .cmp-group th{
  background:rgba(215,255,62,0.06); color:var(--acc-text);
  font-family:var(--font-mono); font-size:0.7rem;
  letter-spacing:0.12em; text-transform:uppercase;
  text-align:left; padding:10px 14px; position:sticky; left:0;
}
.cmp-note{
  margin-top:14px; font-size:0.8rem; color:var(--dim); max-width:78ch;
}
/* Below 640px the table STOPS BEING A TABLE VISUALLY and becomes a stack
   of small cards, one per row — no sideways scroll, ever, at any table
   length. `.cmp-scroll`'s overflow-x is harmless dead weight here since
   nothing inside it is wider than the viewport once this fires. */
@media (max-width:640px){
  .cmp{ min-width:0; }
  .cmp thead{
    position:absolute; width:1px; height:1px; overflow:hidden;
    clip:rect(0,0,0,0); white-space:nowrap; border:0;
  }
  .cmp, .cmp tbody, .cmp tr{ display:block; width:100%; }
  .cmp tbody tr:not(.cmp-group){
    display:block; padding:14px 16px; border-top:1px solid rgba(244,245,247,0.05);
  }
  .cmp tbody tr:first-child{ border-top:0; }
  .cmp tbody th[scope="row"]{
    display:block; position:static; background:none; min-width:0;
    padding:0 0 8px; font-size:0.82rem; color:var(--text); font-weight:500;
  }
  .cmp td{
    display:flex; align-items:center; justify-content:space-between;
    text-align:right; padding:5px 0; border-left:0;
  }
  .cmp td::before{
    content:attr(data-label); text-align:left; color:var(--dim);
    font-size:0.7rem; font-family:var(--font-mono); text-transform:uppercase;
    letter-spacing:0.06em;
  }
  .cmp .cmp-group th{ position:static; padding:10px 16px; }
}

/* ==== OFFER_MODAL (pricing page only) ==============================
   Popup offering an extra 20% off Solo Operator, then a rubber-stamp
   animation onto the Solo card and a 500ms count from £30 to £24.
   Built by js/pricing-offer.js — remove that one <script> line from
   pricing.html to switch the whole thing off. */
.offer-backdrop{
  position:fixed; inset:0; z-index:120;
  background:rgba(12,16,21,0.72);
  backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px);
  display:flex; align-items:center; justify-content:center; padding:24px;
  opacity:0; transition:opacity .28s ease;
}
.offer-backdrop.in{ opacity:1; }
.offer-modal{
  position:relative; width:min(460px,100%);
  background:var(--surface); border:1px solid rgba(215,255,62,0.45);
  border-radius:16px; padding:34px 30px 28px; text-align:center;
  box-shadow:0 40px 90px -30px rgba(0,0,0,0.75);
  transform:translateY(14px) scale(0.96); opacity:0;
  transition:transform .34s cubic-bezier(.2,1.1,.35,1), opacity .28s ease;
}
.offer-backdrop.in .offer-modal{ transform:none; opacity:1; }
.offer-modal .offer-eyebrow{
  font-family:var(--font-mono); font-size:0.7rem;
  letter-spacing:0.14em; text-transform:uppercase;
  color:var(--acc-text); margin-bottom:12px;
}
.offer-modal h2{
  font-size:1.45rem; line-height:1.25; margin:0 0 10px; letter-spacing:-0.015em;
}
.offer-modal h2 .hl{ color:var(--acc-text); }
.offer-modal p{ color:var(--dim); font-size:0.9rem; margin:0 0 24px; }
.offer-actions{ display:flex; flex-direction:column; gap:10px; }
.offer-actions .btn{ justify-content:center; width:100%; }
.offer-decline{
  background:none; border:none; cursor:pointer;
  color:var(--dim); font-family:var(--font-body);
  font-size:0.82rem; text-decoration:underline; opacity:0.75;
  padding:6px; transition:opacity .2s ease;
}
.offer-decline:hover{ opacity:1; color:var(--text); }
@media (prefers-reduced-motion:reduce){
  .offer-backdrop, .offer-modal{ transition:none; }
  .offer-modal{ transform:none; opacity:1; }
}

/* ---- LAUNCH_STAMP — the circular stamp on the Solo card ------------
   SHARED BY TWO SCRIPTS, and only one of them should ever be mounted:
     js/pricing-stamp.js  (mounted) — a standing badge for the launch
                          discount already shown on the card. Fires on
                          scroll-into-view. Changes no price.
     js/pricing-offer.js  (unmounted) — the OFFER_MODAL, which stamped
                          the card only on accept AND cut the price.
   Both add .offer-stamp/.offer-shock and toggle .hit/.go/.stamped, so
   running both puts two stamps on one card. The styles stay here, in the
   OFFER_MODAL block, because that is where they were written; the
   scripts' own headers carry the difference between them. */
.price-card{ position:relative; }
.offer-stamp{
  position:absolute; top:-16px; right:-14px; z-index:3;
  width:88px; height:88px; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  /* --acc-ink, never --canvas and never white: lime is a light colour.
     Same rule as .btn-acc — see the palette note at the top of the
     file. The dark rgba()s below are --acc-ink at low alpha. */
  background:var(--acc); color:var(--acc-ink);
  font-family:var(--font-mono); font-weight:700; font-size:1.15rem;
  box-shadow:0 0 0 3px rgba(11,15,2,0.35) inset,
             0 14px 34px -10px rgba(215,255,62,0.7);
  opacity:0; pointer-events:none;
  transform:scale(2.6) rotate(-38deg);
}
.offer-stamp .ring{
  position:absolute; inset:7px; border-radius:50%;
  border:1.5px dashed rgba(11,15,2,0.5);
}
.offer-stamp.hit{ animation:stamp-slam .42s cubic-bezier(.3,1.5,.4,1) forwards; }
@keyframes stamp-slam{
  0%  { opacity:0; transform:scale(2.6) rotate(-38deg); }
  55% { opacity:1; transform:scale(0.88) rotate(-9deg); }
  75% { transform:scale(1.06) rotate(-13deg); }
  100%{ opacity:1; transform:scale(1) rotate(-12deg); }
}
/* shockwave ring thrown off on impact */
.offer-shock{
  position:absolute; top:-16px; right:-14px; z-index:2;
  width:88px; height:88px; border-radius:50%;
  border:2px solid var(--acc); opacity:0; pointer-events:none;
}
.offer-shock.go{ animation:stamp-shock .55s ease-out .2s forwards; }
@keyframes stamp-shock{
  0%  { opacity:0.85; transform:scale(0.9); }
  100%{ opacity:0; transform:scale(2.1); }
}
/* the card takes the hit */
.price-card.stamped{ animation:card-recoil .42s cubic-bezier(.3,1.4,.4,1); }
@keyframes card-recoil{
  0%  { transform:none; }
  30% { transform:scale(0.975) translateY(4px); }
  60% { transform:scale(1.012) translateY(-3px); }
  100%{ transform:none; }
}
.price-card.locked{ border-color:var(--acc); box-shadow:0 0 0 1px rgba(215,255,62,0.45), 0 22px 50px -26px rgba(215,255,62,0.5); }
.price-card .amt .was{
  text-decoration:line-through; color:var(--dim);
  font-size:1.1rem; font-weight:500;
}
@media (prefers-reduced-motion:reduce){
  .offer-stamp.hit{ animation:none; opacity:1; transform:scale(1) rotate(-12deg); }
  .offer-shock.go{ animation:none; }
  .price-card.stamped{ animation:none; }
}
/* ==== end OFFER_MODAL ============================================== */

/* ==== TOASTS ========================================================
   Top-right stack. Green = success, red = failure. Used by the sign-in
   page; generic enough to reuse anywhere. */
.toast-stack{
  position:fixed; top:18px; right:18px; z-index:150;
  display:flex; flex-direction:column; gap:10px;
  width:min(340px, calc(100vw - 36px)); pointer-events:none;
}
.toast{
  display:flex; align-items:center; gap:11px;
  background:var(--raised); border:1px solid var(--line);
  border-left:3px solid var(--dim);
  border-radius:var(--radius); padding:12px 16px;
  box-shadow:0 18px 40px -18px rgba(0,0,0,0.8);
  transform:translateX(120%); opacity:0;
  transition:transform .38s cubic-bezier(.2,1.1,.35,1), opacity .3s ease;
  pointer-events:auto;
  line-height:1.35;
}
.toast.in{ transform:none; opacity:1; }
.toast.out{ transform:translateX(120%); opacity:0; }
.toast .ic{
  flex-shrink:0; width:20px; height:20px; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  font-size:0.72rem; font-weight:700; color:var(--canvas);
  background:var(--dim);
}
.toast .msg{ display:flex; flex-direction:column; gap:2px; min-width:0; }
.toast .tx{ display:block; font-size:0.86rem; font-weight:600; color:var(--text); }
.toast .sub{ display:block; font-size:0.76rem; color:var(--dim); font-weight:400; }
.toast.ok   { border-left-color:#7FD1A8; }
.toast.ok .ic{ background:#7FD1A8; }
.toast.err  { border-left-color:#F08A78; }
.toast.err .ic{ background:#F08A78; color:#0C1015; }
/* "It worked, but part of it needs your attention" — see the note above
   toast() in js/auth.js. Gold rather than a third hue: the site has exactly
   one accent, and this is the state that wants noticing without alarming. */
.toast.warn { border-left-color:var(--acc); }
.toast.warn .ic{ background:var(--acc); color:#0C1015; }
@media (prefers-reduced-motion:reduce){
  .toast{ transition:opacity .2s ease; transform:none; }
  .toast.out{ transform:none; }
}

/* ==== AUTH (sign-in page) =========================================== */
/* DOCK CLEARANCE — same rule as `.hero` and `.page-head`, and this page
   was the one that never got it. Both numbers here were written when the
   header still OCCUPIED its ~78px of flow: subtracting `--header-h` from
   the viewport was then correct, and 56px of top padding was then measured
   from the bottom of the bar. Since the dock started floating
   (`header.site-head{ margin-bottom:calc(-1 * var(--header-h)) }`) the
   wrap begins at y=0, so 56px put the card's top edge — and the whole
   upper half of the Sign in / Create account switcher — UNDER the
   wordmark and link islands. Measured: card top at y=56 against a 78px
   dock, so 22px of a segmented control sat behind glass.

   `min-height` loses the subtraction for the same reason: nothing is
   above this element any more, so the wrap is the full viewport. The
   clamp matches `.page-head` exactly rather than inventing a third
   clearance figure — search "DOCK CLEARANCE" for the others. */
.auth-wrap{
  min-height:100vh;
  display:flex; align-items:center; justify-content:center;
  padding:clamp(108px,12vh,132px) 24px 72px;
}
.auth-card{
  width:min(430px,100%);
  background:var(--raised); border:1px solid var(--line);
  border-radius:14px; padding:36px 32px 30px;
  box-shadow:0 30px 70px -34px rgba(0,0,0,0.8);
}
/* Sign in / Create account switcher — reuses .billing-toggle's pill+thumb
   component (js/pricing.js has its own instance of this same pattern; this
   one is driven separately by js/sign-in.js so the two never share state).
   display:flex + each button flex:1 so it fills the card's width evenly,
   unlike the toggle's original inline-flex, content-width usage. */
.auth-tabs{ display:flex; width:100%; margin-bottom:24px; }
/* NEITHER TAB MAY WRAP, or the two segments stop matching. At 375px the
   track is 261px, so each tab gets ~125px and "Create account" broke onto
   two lines while "Sign in" stayed on one — measured 53px against 36px.
   In a segmented control that reads as a broken component: the filled
   pill no longer fills its own track and the two labels sit on different
   baselines. `nowrap` plus a slightly tighter horizontal pad keeps the
   longer label on one line at 375px with room to spare; the padding is
   only narrowed here, so the shared `.billing-toggle` on the pricing page
   is untouched. */
.auth-tabs button{
  flex:1; text-align:center;
  white-space:nowrap; padding-left:10px; padding-right:10px;
}
.auth-card h1{ font-size:1.5rem; margin:10px 0 6px; letter-spacing:-0.015em; }
.auth-card .sub{ color:var(--dim); font-size:0.88rem; margin:0 0 26px; }
.field{ display:block; margin-bottom:16px; }
.field span{
  display:block; font-size:0.76rem; font-weight:600; letter-spacing:0.04em;
  text-transform:uppercase; color:var(--dim); margin-bottom:7px;
}
.field input{
  width:100%; padding:12px 14px; border-radius:var(--radius);
  background:rgba(244,245,247,0.04); border:1px solid var(--line);
  color:var(--text); font-family:var(--font-body); font-size:0.92rem;
  transition:border-color .2s ease, background .2s ease;
}
.field input:focus{
  outline:none; border-color:var(--acc);
  background:rgba(244,245,247,0.07);
}
.field input::placeholder{ color:rgba(139,144,150,0.45); }
.auth-card .btn-acc{ width:100%; justify-content:center; margin-top:8px; }
.auth-err{
  font-size:0.8rem; color:#E2917F; margin:0 0 14px; min-height:1.1em;
}
.auth-foot{
  margin-top:22px; padding-top:18px; border-top:1px solid var(--line);
  font-size:0.78rem; color:var(--dim); text-align:center;
}
.auth-foot a{ color:var(--acc-text); text-decoration:none; }
/* `.demo-note` USED TO BE HERE and was deleted 2026-08-04. It styled the
   demo-credentials callout on the old click-through sign-in page, which
   was itself deleted when real accounts landed on 2026-07-28 — so the
   rule had been dead for months, referenced by no HTML, JS or template.
   It surfaced by colliding with a NEW `.demo-note` on the homepage
   territory demo, which inherited a gold-bordered callout box nobody
   asked for. Dead CSS is not harmless; it is a name nobody knows is
   taken. */

/* ==== INTERFACE (placeholder app shell) ============================= */
.app-bar{
  display:flex; align-items:center; justify-content:space-between;
  gap:16px; flex-wrap:wrap; padding:26px 0 0;
}
.app-who{ font-size:0.84rem; color:var(--dim); }
.app-who b{ color:var(--text); font-weight:600; }
.tool-grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:18px; margin-top:26px; }
.tool-card{
  background:var(--raised); border:1px solid var(--line);
  border-radius:12px; padding:26px 24px; display:flex; flex-direction:column;
}
.tool-card .k{
  font-family:var(--font-mono); font-size:0.72rem;
  color:var(--acc-text); margin-bottom:12px;
}
.tool-card h3{ font-size:1.05rem; margin:0 0 8px; }
.tool-card p{ font-size:0.87rem; color:var(--dim); margin:0 0 18px; }
.tool-card .btn{ margin-top:auto; justify-content:center; }
.tool-card .btn[disabled]{ opacity:0.4; cursor:not-allowed; }
.status-pill{
  display:inline-flex; align-items:center; gap:7px; width:fit-content;
  font-family:var(--font-mono); font-size:0.7rem;
  color:var(--dim); border:1px solid var(--line);
  border-radius:999px; padding:5px 12px; margin-bottom:18px;
}
.status-pill .d{ width:6px; height:6px; border-radius:50%; background:var(--acc); }
@media (max-width:820px){ .tool-grid{ grid-template-columns:1fr; } }

.role-pill{
  display:inline-flex; align-items:center; margin-left:9px;
  font-family:var(--font-mono); font-size:0.66rem; font-weight:600;
  padding:3px 9px; border-radius:999px; border:1px solid var(--line);
  color:var(--acc-text); text-transform:uppercase; letter-spacing:.03em;
  vertical-align:middle;
}
.accounts-panel{
  background:var(--raised); border:1px solid var(--line);
  border-radius:12px; padding:24px 26px; margin-top:34px;
}
.accounts-panel h3{ font-size:1rem; margin:0 0 4px; }
.accounts-panel .cap{ font-size:0.82rem; color:var(--dim); margin:0 0 16px; }
.accounts-table{ width:100%; border-collapse:collapse; }
.accounts-table th, .accounts-table td{
  text-align:left; padding:10px 6px; font-size:0.85rem;
  border-bottom:1px solid var(--line);
}
.accounts-table th{
  font-family:var(--font-mono); font-size:0.66rem; text-transform:uppercase;
  letter-spacing:.03em; color:var(--dim); font-weight:600;
}
.accounts-table td{ color:var(--text); }
.accounts-table td.role{ text-transform:capitalize; color:var(--dim); }
.accounts-table tr:last-child td{ border-bottom:none; }

/* ==== APP SHELL (signed-in pages) ==================================
   Rebuilt 2026-07-28 (third pass). The interface used to be one page with
   three open/close panels; it is now five real pages behind a sidebar
   (dashboard / find / generate / draft / accounts), each with its own URL
   and its own back button. The marketing header and footer are gone from
   these pages on purpose — this is an app, and it should stop pretending
   to be another marketing page.

   Only .app-body gets any of this, so nothing here can leak into the
   seven public pages. */

.app-body{ background:var(--canvas); }
.app-shell{ display:grid; grid-template-columns:236px 1fr; min-height:100vh; }

.sidebar{
  position:sticky; top:0; align-self:start;
  height:100vh; display:flex; flex-direction:column;
  background:var(--surface); border-right:1px solid var(--line);
  padding:22px 16px 18px;
}
/* The row that used to be just `.side-brand` now also holds the settings
   gear — billing + sign out moved here from a permanent block at the
   bottom of the sidebar, 2026-07-29. `.side-brand-row` carries the
   padding/spacing `.side-brand` used to carry on its own, so the logo
   position doesn't shift now that it shares the row with the toggle. */
.side-brand-row{
  display:flex; align-items:center; justify-content:space-between;
  gap:8px; padding:0 8px 22px;
}
/* Same rule as .brand above: gap:0, because the gap would otherwise fall
   between "Scout" and "line". Spacing lives on the image. */
.side-brand{
  display:flex; align-items:center; gap:0;
  font-weight:700; font-size:1.1rem; text-decoration:none;
  letter-spacing:-0.01em;
}
/* Sized here rather than trusting the width/height attributes, so the
   sidebar and the marketing header stay in step. Same 32px reasoning as
   .brand-mark above — the full mark needs it. */
.side-brand img{ width:32px; height:32px; margin-right:10px; }

.settings-wrap{ position:relative; flex-shrink:0; }
.settings-toggle{
  display:inline-flex; align-items:center; justify-content:center;
  width:32px; height:32px; padding:0; border-radius:8px;
  border:1px solid transparent; background:transparent; color:var(--dim);
  cursor:pointer;
  transition:background .18s ease, color .18s ease, border-color .18s ease;
}
.settings-toggle:hover,
.settings-toggle[aria-expanded="true"]{
  background:rgba(244,245,247,0.07); color:var(--text); border-color:var(--line);
}
.settings-menu{
  position:absolute; top:calc(100% + 8px); right:0; z-index:5;
  min-width:210px; background:var(--surface); border:1px solid var(--line);
  border-radius:10px; padding:14px; box-shadow:0 18px 40px -20px rgba(0,0,0,0.5);
  display:flex; flex-direction:column; gap:10px;
  /* Closed state: kept in the DOM and laid out (so it CAN be transitioned,
     unlike `display:none`/`hidden`, which snap with nothing to animate
     between), but invisible, non-interactive, and out of the tab order.
     `visibility:hidden` does that last part for free — same effective
     a11y result as the `hidden` attribute this replaced, just animatable.
     js/app.js's `setSettings()` toggles the `.open` class below; it
     removes the markup's `hidden` attribute on setup so this becomes the
     only thing controlling visibility once JS has run. */
  opacity:0; visibility:hidden; pointer-events:none;
  transform:translateY(-6px) scale(.96); transform-origin:top right;
  transition:opacity .16s ease, transform .16s cubic-bezier(.3,1,.4,1),
             visibility 0s linear .16s;
}
.settings-menu.open{
  opacity:1; visibility:visible; pointer-events:auto; transform:none;
  transition:opacity .16s ease, transform .16s cubic-bezier(.3,1,.4,1);
}
.settings-menu .side-period{ margin:0; padding:0 2px; }
/* The Accounts entry in this popover (it lives here rather than in the
   workspace nav — see the note in tools-build-pages.py) wears
   `.btn.btn-ghost.btn-sm`, the same classes as the two controls beside
   it, rather than a bespoke `.settings-link` class. A new class was
   written first and it drifted immediately: body font against their mono,
   sentence case against their uppercase. There was never a second
   component here, only a third instance of the existing one.

   It stays an <a> and not a <button> because it navigates, which is what
   a keyboard and a screen reader need to be told; .btn styles any element
   and carries no opinion about the tag. */
.settings-menu .btn{ width:100%; justify-content:center; }
@media (prefers-reduced-motion:reduce){
  .settings-toggle{ transition:none; }
  .settings-menu{ transition:opacity .01s linear, visibility 0s linear .01s; transform:none; }
  .settings-menu.open{ transition:opacity .01s linear; }
}

.side-nav{ display:flex; flex-direction:column; gap:2px; }
.side-nav a{
  display:flex; align-items:center; gap:11px;
  padding:10px 12px; border-radius:8px; text-decoration:none;
  font-size:0.88rem; color:var(--dim);
  border-left:2px solid transparent;
  transition:background .18s ease, color .18s ease, border-color .18s ease;
}
.side-nav a:hover{ background:rgba(244,245,247,0.05); color:var(--text); }
.side-nav a.active{
  background:rgba(215,255,62,0.10); color:var(--acc-text);
  border-left-color:var(--acc);
}
.side-k{
  font-family:var(--font-mono); font-size:0.66rem;
  opacity:0.55; min-width:16px;
}

/* `.side-foot` (a permanent block pinned to the bottom of the sidebar,
   `margin-top:auto`) is gone from the markup as of 2026-07-29 — billing,
   sign out, and the identity/period info it held all moved into
   `.settings-menu` above. `.side-user`/`.side-period` are reused there
   unchanged; only the "pin to the bottom of the sidebar" wrapper is gone. */
.side-user{ display:flex; align-items:center; flex-wrap:wrap; gap:6px; font-size:0.8rem; }
.side-user b{ color:var(--text); font-weight:600; word-break:break-all; }
.side-period{
  font-family:var(--font-mono); font-size:0.68rem;
  color:var(--dim); margin:8px 0 12px; line-height:1.5;
}

.side-toggle{
  display:none; position:fixed; top:14px; left:14px; z-index:60;
  background:var(--canvas); border:1px solid var(--line); border-radius:8px;
  color:var(--text); font-size:1.1rem; padding:7px 12px; cursor:pointer;
}

/* min-width:0 is load-bearing, not decoration. `.app-main` is a CSS Grid
   item (`.app-shell{ display:grid }`); grid (and flex) items default to
   `min-width:auto`, meaning they refuse to shrink below their content's
   intrinsic min-content size. Found 2026-07-29 tracing the "have to zoom
   out on mobile" report: a leads `.table-scroll table{ min-width:640px }`
   deep inside was contributing ~640px of unshrinkable intrinsic width,
   and with no min-width:0 to override the grid default, that dragged
   `.app-main` — and therefore the whole `.app-shell` row, `.app-topbar`,
   `.app-head`, every panel — out to ~698px even inside a 375px viewport.
   `.table-scroll`'s own `overflow-x:auto` (see max-width:560px below) was
   already built to contain exactly this by scrolling the table alone;
   it just never got the chance, because the page around it grew to fit
   the table instead of letting the table scroll inside a normal-width
   page. This one property is what lets that existing design work. */
.app-main{ padding:36px 40px 72px; max-width:1180px; min-width:0; }
.app-head{
  display:flex; align-items:flex-start; justify-content:space-between;
  gap:32px; flex-wrap:wrap; margin-bottom:30px;
}
.app-head h1{ font-size:clamp(1.6rem,3vw,2.1rem); font-weight:700; margin:10px 0 6px; }
.app-sub{ color:var(--dim); font-size:0.92rem; max-width:56ch; margin:0; }

@media (max-width:900px){
  .app-shell{ grid-template-columns:1fr; }
  .side-toggle{ display:block; }
  .sidebar{
    position:fixed; top:0; bottom:0; left:0; width:236px; z-index:55;
    transform:translateX(-100%); transition:transform .26s ease;
  }
  .app-shell.side-open .sidebar{ transform:none; }
  .app-main{ padding:64px 20px 60px; }
}
@media (prefers-reduced-motion:reduce){
  .sidebar{ transition:none; }
  .side-nav a{ transition:none; }
}

/* ---- allowance gauges ----------------------------------------------
   REBUILT 2026-08-01: three rings became three figures. A ring answers
   "what fraction is gone" precisely and "how many have I got left" only
   approximately, and the second is the only question anyone asks a
   credit meter. So the NUMBER is the object now, the bar under it is the
   decoration, and the digits are the odometer roll from
   js/sliding-number.js — spending one coin rolls one digit.

   The bar FILLS as the allowance is spent, so full means gone. Same
   direction the arc filled in; changing it would silently invert what a
   glance at the header means. */
.app-gauges{ display:flex; gap:18px; flex-wrap:wrap; }
.gauge{ position:relative; min-width:118px; }
.gauge-fig{ display:flex; align-items:baseline; gap:6px; min-height:34px; }
.gauge-num{
  font-family:var(--font-mono); font-size:1.75rem; font-weight:600;
  color:var(--text); line-height:1; letter-spacing:-0.02em;
}
.gauge-of{ font-family:var(--font-mono); font-size:0.72rem; color:var(--dim); }
.gauge.spent .gauge-num{ color:#E2917F; }
.gauge-label{
  font-family:var(--font-mono); font-size:0.62rem; text-transform:uppercase;
  letter-spacing:.1em; color:var(--dim); margin-top:5px;
}
/* The track is 3px and the fill sits on it. Deliberately not a border or
   an outline — a 3px bar whose width transitions is one composited
   property, and the gauges re-render on every quota refresh. */
.gauge-bar{
  position:relative; height:3px; border-radius:3px; margin-top:9px;
  background:rgba(244,245,247,0.10); overflow:hidden;
}
.gauge-fill{
  display:block; height:100%; width:0; border-radius:3px;
  background:var(--acc);
  transition:width .5s cubic-bezier(.3,1,.4,1), background-color .2s ease;
}
.gauge.spent .gauge-fill{ background:#E2917F; }

/* ---- locked gauge (no plan) ----------------------------------------
   A padlock where the number goes, and a dashed track instead of a solid
   one. The dashes matter: a solid empty bar reads as "a gauge at zero",
   which is the very thing this must NOT say to someone who has simply
   never bought anything. Deliberately NOT --app-danger — nothing has gone
   wrong here, and colouring it like a failure would make a signup feel
   like a problem to be fixed rather than a purchase to be made. */
.gauge.locked .gauge-lock{ width:22px; height:22px; color:var(--dim); transition:color .2s ease; }
.gauge.locked:hover .gauge-lock{ color:var(--acc-text); }
.gauge.locked .gauge-label{ color:var(--faint); }
.gauge.locked .gauge-bar{
  background:none;
  background-image:repeating-linear-gradient(90deg,
    var(--line-strong) 0 5px, transparent 5px 11px);
}
.gauge.locked .gauge-fill{ display:none; }
@media (prefers-reduced-motion:reduce){ .gauge-fill{ transition:none; } }

/* ==== SLIDING NUMBER =================================================
   js/sliding-number.js. One column per digit; the column holds 0-9
   stacked and is translated so the wanted digit sits in the 1/10 window.

   THE WINDOW IS `overflow:hidden` ON THE COLUMN, and the strip is exactly
   ten cells tall, so translateY(-N * 10%) puts digit N in view. That
   percentage is of the STRIP's own height, which is why the cells must
   all be the same height — they are, they are all one line of the same
   monospace face, and that is a large part of why this is set in mono.

   `.visually-hidden` carries the whole number for a screen reader; the digit
   stack is aria-hidden. Ten cells per column read aloud in sequence is
   not a number. */
.sliding-number{ display:inline-flex; align-items:baseline; }
.sn-digits{ display:inline-flex; }
.sn-col{
  display:block; overflow:hidden;
  height:1em; line-height:1;
}
.sn-strip{
  display:block;
  transition:transform .55s cubic-bezier(.3,1,.4,1);
  will-change:transform;
}
.sn-d{ display:block; height:1em; line-height:1; font-variant-numeric:tabular-nums; }
@media (prefers-reduced-motion:reduce){ .sn-strip{ transition:none; } }

/* ---- panels + dashboard --------------------------------------------- */
.panel{
  background:var(--surface); border:1px solid var(--line);
  border-radius:12px; padding:24px 26px; margin-bottom:22px;
}
.panel h2{ font-size:1.02rem; margin:0 0 6px; }
.panel .cap{ font-size:0.84rem; color:var(--dim); margin:0 0 18px; }
.panel-head{
  display:flex; align-items:center; justify-content:space-between;
  gap:14px; flex-wrap:wrap;
}
.table-scroll{ overflow-x:auto; }

.stat-row{ display:grid; grid-template-columns:repeat(4,1fr); gap:14px; margin-bottom:22px; }
@media (max-width:820px){ .stat-row{ grid-template-columns:repeat(2,1fr); } }
.stat-card{
  display:block; text-decoration:none;
  background:var(--surface); border:1px solid var(--line);
  border-radius:12px; padding:20px 22px;
  transition:border-color .2s ease, transform .2s ease;
}
.stat-card:hover{ border-color:rgba(215,255,62,0.42); transform:translateY(-2px); }
.stat-n{ font-family:var(--font-mono); font-size:1.7rem; font-weight:700; color:var(--text); }
.stat-l{ font-size:0.78rem; color:var(--dim); margin-top:2px; }
@media (prefers-reduced-motion:reduce){
  .stat-card{ transition:none; }
  .stat-card:hover{ transform:none; }
}

.pill-done, .pill-todo{
  display:inline-flex; align-items:center; font-family:var(--font-mono);
  font-size:0.64rem; font-weight:600; text-transform:uppercase; letter-spacing:.03em;
  padding:3px 9px; border-radius:999px;
}
.pill-done{ background:rgba(139,144,150,0.16); color:var(--dim); }
.pill-todo{ background:rgba(244,245,247,0.07); color:rgba(139,144,150,0.75); }

.select{
  width:100%; padding:12px 14px; border-radius:var(--radius);
  background:rgba(244,245,247,0.04); border:1px solid var(--line);
  color:var(--text); font-family:var(--font-body); font-size:0.92rem;
}
.select:focus{ outline:none; border-color:var(--acc); }

/* ==== SUBSCRIBER TOOLS (scan / build / draft panels) ================
   Rebuilt 2026-07-28 when generate + draft went live and the scan form
   was reworked: the old five-column .scan-grid (name + lat + lon +
   radius + cell radius) and the seventeen-checkbox .trade-list are gone,
   replaced by a place search, an Advanced disclosure, a chip picker and
   sliders. Everything here is shared by all three panels. */

.scan-result{
  margin-top:18px; padding:14px 16px; border-radius:var(--radius);
  background:rgba(244,245,247,0.04); border:1px solid var(--line);
  font-family:var(--font-mono); font-size:0.78rem;
  color:var(--dim); line-height:1.7;
}
.scan-result b{ color:var(--text); }
.scan-result .err-line, .err-line{ color:#E2917F; }
.leads-head{
  display:flex; align-items:center; justify-content:space-between;
  margin-top:30px; margin-bottom:14px;
}
.grade-badge{
  display:inline-flex; align-items:center; justify-content:center;
  width:22px; height:22px; border-radius:6px; font-weight:700; font-size:0.72rem;
  font-family:var(--font-mono);
}
.grade-badge.A{ background:rgba(215,255,62,0.18); color:var(--acc-text); }
.grade-badge.B{ background:rgba(139,144,150,0.18); color:var(--dim); }
.grade-badge.C, .grade-badge.D{ background:rgba(226,145,127,0.14); color:#E2917F; }

/* ---- typeahead (territory search + profession search) --------------
   One component, two uses: the territory box hits the Places API through
   /api/places/territory, the profession box filters a local array. Same
   markup and same keyboard behaviour either way. */
.ac-wrap{ position:relative; }
.ac-wrap input{
  width:100%; padding:12px 14px; border-radius:var(--radius);
  background:rgba(244,245,247,0.04); border:1px solid var(--line);
  color:var(--text); font-family:var(--font-body); font-size:0.92rem;
  transition:border-color .2s ease, background .2s ease;
}
.ac-wrap input:focus{ outline:none; border-color:var(--acc); background:rgba(244,245,247,0.07); }
.ac-wrap input::placeholder{ color:rgba(139,144,150,0.45); }
.ac-list{
  position:absolute; z-index:20; top:calc(100% + 6px); left:0; right:0;
  background:var(--canvas); border:1px solid var(--line);
  border-radius:var(--radius); overflow:hidden;
  box-shadow:0 22px 50px -24px rgba(0,0,0,0.85);
  max-height:280px; overflow-y:auto;
}
.ac-item{
  display:flex; flex-direction:column; gap:2px; width:100%; text-align:left;
  background:none; border:none; cursor:pointer; padding:10px 14px;
  font-family:var(--font-body); border-bottom:1px solid var(--line);
}
.ac-item:last-child{ border-bottom:none; }
.ac-item:hover, .ac-item:focus{ background:rgba(215,255,62,0.10); outline:none; }
.ac-item:focus{ box-shadow:inset 2px 0 0 var(--acc); }
.ac-main{ color:var(--text); font-size:0.9rem; }
.ac-sec{ color:var(--dim); font-size:0.76rem; }
.ac-empty{ padding:10px 14px; color:var(--dim); font-size:0.82rem; }

/* The selected territory, restated in full. The whole point of the search
   is that what gets scanned is unambiguous, so it says which place and
   which coordinates rather than just echoing the typed text. */
.terr-chosen{
  display:flex; align-items:flex-start; justify-content:space-between; gap:14px;
  margin-top:10px; padding:12px 14px; border-radius:var(--radius);
  background:rgba(215,255,62,0.08); border:1px solid rgba(215,255,62,0.3);
}
.terr-chosen b{ color:var(--text); font-size:0.92rem; }

/* ---- advanced disclosure ------------------------------------------- */
.adv{ margin:6px 0 20px; }
.adv-toggle{
  background:none; border:none; cursor:pointer; padding:6px 0;
  color:var(--dim); font-family:var(--font-body); font-size:0.82rem;
  display:inline-flex; align-items:center; gap:8px;
}
.adv-toggle:hover{ color:var(--text); }
.adv-caret{ display:inline-block; transition:transform .22s ease; color:var(--acc-text); }
.adv-toggle.open .adv-caret{ transform:rotate(90deg); }
.adv-body{
  margin-top:12px; padding:18px 18px 6px; border-radius:var(--radius);
  background:rgba(244,245,247,0.03); border:1px solid var(--line);
}
.adv-grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:14px 16px; }
@media (max-width:760px){ .adv-grid{ grid-template-columns:1fr; } }
/* `.field span{ display:block }` (the label itself) is a bare descendant
   selector, so it also matches these three — which are meant to sit INSIDE
   a label, on the same line. They were silently inheriting display:block
   and wrapping onto their own line: "Temporary password" then "(min 8)"
   underneath it. Pre-existing since .opt was introduced (visible on
   accounts.html's create-tester form); found 2026-07-30 by rendering the
   new contact form, where the effect repeats on every field.

   The colour rules below are MORE specific than `.field span` but never set
   `display`, so they never overrode it — a reminder that specificity only
   settles the properties a rule actually declares. */
.field .req,
.field .opt,
.field .field-count{ display:inline; }
.field .req{ font-style:normal; color:var(--acc-text); text-transform:none; letter-spacing:0; }
.field .opt{ font-style:normal; color:var(--dim); opacity:0.7; text-transform:none; letter-spacing:0; }
@media (prefers-reduced-motion:reduce){ .adv-caret{ transition:none; } }

/* ---- sliders --------------------------------------------------------
   Used for minimum score and claim-up-to. The live value is an <output>
   next to the label rather than a tooltip on the thumb, so the number is
   readable without hovering and is announced to screen readers. */
.slider-field{ margin-bottom:22px; }
.slider-out{
  font-family:var(--font-mono); color:var(--acc-text); font-weight:600;
  text-transform:none; letter-spacing:0; margin-left:8px;
}
.slider-field input[type="range"]{
  -webkit-appearance:none; appearance:none;
  width:100%; height:4px; border-radius:999px; padding:0; margin:12px 0 6px;
  background:rgba(244,245,247,0.14); cursor:pointer;
}
.slider-field input[type="range"]:focus{ outline:none; }
.slider-field input[type="range"]::-webkit-slider-thumb{
  -webkit-appearance:none; appearance:none;
  width:18px; height:18px; border-radius:50%;
  background:var(--acc); border:3px solid var(--canvas);
  box-shadow:0 0 0 1px rgba(215,255,62,0.6); cursor:pointer;
}
.slider-field input[type="range"]::-moz-range-thumb{
  width:14px; height:14px; border-radius:50%;
  background:var(--acc); border:3px solid var(--canvas);
  box-shadow:0 0 0 1px rgba(215,255,62,0.6); cursor:pointer;
}
.slider-field input[type="range"]:focus-visible::-webkit-slider-thumb{
  box-shadow:0 0 0 3px rgba(215,255,62,0.45);
}
.slider-field input[type="range"]:disabled{ opacity:0.4; cursor:not-allowed; }
.slider-scale{
  display:flex; justify-content:space-between;
  font-family:var(--font-mono); font-size:0.68rem;
  color:var(--dim); opacity:0.7; text-transform:none; letter-spacing:0;
}
.slider-note{
  display:block; margin-top:8px; font-size:0.76rem; color:var(--acc-text);
  text-transform:none; letter-spacing:0; font-weight:400; min-height:1.1em;
}

/* ---- profession picker ---------------------------------------------- */
.prof-modes{ display:flex; gap:22px; flex-wrap:wrap; padding:4px 0 12px; }
.radio{
  display:inline-flex; align-items:center; gap:8px; cursor:pointer;
  font-size:0.86rem; color:var(--dim); font-weight:400;
  text-transform:none; letter-spacing:0;
}
.radio input{ accent-color:var(--acc); }
.prof-picker{ margin-bottom:6px; }
.chips{ display:flex; flex-wrap:wrap; gap:8px; margin-top:12px; min-height:30px; }
.chip{
  display:inline-flex; align-items:center; gap:8px;
  background:rgba(139,144,150,0.12); border:1px solid rgba(139,144,150,0.32);
  color:var(--text); border-radius:999px; padding:5px 8px 5px 13px;
  font-size:0.8rem;
}
.chip-x{
  background:none; border:none; cursor:pointer; line-height:1;
  color:var(--dim); font-size:1.05rem; padding:0 3px;
}
.chip-x:hover{ color:#E2917F; }
.chip-empty{ font-size:0.8rem; color:var(--dim); opacity:0.65; }

/* ---- profession groups + browse (2026-07-31, 289 professions) --------
   EVERY SELECTOR HERE IS PREFIXED `.field`, AND THAT IS LOAD-BEARING.
   `.field span` (specificity 0,1,1) sets display:block, uppercase and
   letter-spacing on EVERY span inside a field, meaning the whole picker
   rendered as uppercase blocks — the group blurbs, the counts, the caret.
   A bare `.prof-group-blurb` is 0,1,0 and loses. This is the same trap
   CLAUDE.md already records for `.req` / `.opt` / `.field-count`, hit again
   because the rule catches descendants rather than direct children.
   Verified in a browser, not reasoned about: computed textTransform was
   "uppercase" before and is "none" now.

   Every colour is a rung of the :root ladder or the one gold accent;
   nothing invents a shade. Motion stays inside prefers-reduced-motion. */

/* `.radio` is a flex container and its label text is an ANONYMOUS flex
   item, which wraps to min-content — "A whole group" rendered over three
   lines at 122x64 with the full width of the panel free beside it. Measured,
   not guessed: nowrap takes it to 122x21. Scoped to .prof-modes so a longer
   radio label elsewhere can still wrap. */
.prof-modes .radio{ white-space:nowrap; }

/* The count next to the "Professions" label. Written by js/find.js from
   the generated data, never typed into the markup. */
.field .prof-total{
  display:inline; color:var(--dim); font-weight:400; text-transform:none;
  letter-spacing:0; font-size:0.78rem; margin-left:8px;
}

/* AND SCOPING ALONE IS NOT ENOUGH — the reset below is the other half.
   `.field span` also sets font-size, font-weight, letter-spacing,
   text-transform and margin-bottom. Winning on specificity only settles the
   properties a rule actually DECLARES, so the first version of this block
   scoped everything under `.field`, still rendered fully uppercase, and
   looked correct in the file. Undeclared properties are inherited from the
   rule you beat, not reset by beating it. Verified in a browser both times.
   Same lesson CLAUDE.md records for `.req` / `.opt` / `.field-count`. */
/* .chip-empty is pre-existing and was caught by the same rule long before
   this picker — it is a quiet sentence ("Nothing added yet…") that has been
   rendering as an uppercase 600-weight block. Included here because it sits
   inside the picker and would now be the only shouting thing in it. */
.field .chip-empty,
.field .prof-total,
.field .prof-group-label, .field .prof-group-n, .field .prof-group-blurb,
.field .prof-cat-label, .field .prof-cat-n, .field .prof-caret{
  text-transform:none; letter-spacing:0; font-weight:400; margin-bottom:0;
}

/* Group mode: one tile per group. auto-fill rather than a fixed column
   count, so this needs no breakpoint of its own between 320px and 2000px. */
.field .prof-groups{
  display:grid; gap:10px; margin-bottom:6px;
  grid-template-columns:repeat(auto-fill, minmax(190px, 1fr));
}
/* AUTHOR CSS BEATS THE UA'S `[hidden]{display:none}` REGARDLESS OF
   SPECIFICITY, so any element that sets its own `display` AND is toggled
   with the hidden attribute needs this explicitly or `hidden` does nothing
   at all. js/find.js was correctly setting .hidden = true and both panels
   stayed on screen — caught in a render, invisible in review. CLAUDE.md
   records the same trap on `.settings-menu`; this is it again, twice. */
.field .prof-groups[hidden]{ display:none; }
.field .prof-group-card{
  display:grid; grid-template-columns:1fr auto; gap:2px 10px;
  text-align:left; cursor:pointer; padding:12px 13px;
  background:var(--raised); border:1px solid var(--line-strong);
  border-radius:10px; color:var(--text); font:inherit;
}
.field .prof-group-label{ font-family:var(--font-display); font-weight:400; font-size:0.95rem; }
.field .prof-group-n{
  font-family:var(--font-mono); font-size:0.76rem; color:var(--dim);
  align-self:center; padding:1px 7px; border-radius:999px;
  background:rgba(139,144,150,0.14);
}
.field .prof-group-blurb{
  grid-column:1 / -1; font-size:0.78rem; color:var(--dim); line-height:1.35;
}
.field .prof-group-card:hover{ border-color:rgba(215,255,62,0.45); background:var(--raised-2); }
.field .prof-group-card:focus-visible{ outline:2px solid var(--acc); outline-offset:2px; }
/* Selected: gold edge and wash. Never colour alone — aria-pressed carries
   the same fact for anyone not seeing it. */
.field .prof-group-card.on{
  border-color:var(--acc); background:rgba(215,255,62,0.10);
}
.field .prof-group-card.on .prof-group-n{ background:rgba(215,255,62,0.22); color:var(--acc-text); }

/* Pick mode: the collapsed browse list under the search box. */
.field .prof-browse{ margin-top:14px; border-top:1px solid var(--line); }
.field .prof-cat{ border-bottom:1px solid var(--line); }
.field .prof-cat-head{
  display:flex; align-items:center; gap:10px; width:100%;
  background:none; border:none; cursor:pointer; padding:10px 2px;
  color:var(--text); font:inherit; text-align:left;
}
.field .prof-cat-label{ font-family:var(--font-display); font-weight:400; font-size:0.9rem; flex:1; }
.field .prof-cat-n{
  font-family:var(--font-mono); font-size:0.74rem; color:var(--dim);
  padding:1px 7px; border-radius:999px; background:rgba(139,144,150,0.14);
}
.field .prof-cat-head.has-picked .prof-cat-n{ background:rgba(215,255,62,0.20); color:var(--acc-text); }
.field .prof-caret{ display:inline-block; color:var(--acc-text); }
.field .prof-cat-head.open .prof-caret{ transform:rotate(90deg); }
.field .prof-cat-head:focus-visible{ outline:2px solid var(--acc); outline-offset:-2px; }

.field .prof-cat-body{ display:flex; flex-wrap:wrap; gap:6px; padding:2px 0 14px; }
.field .prof-cat-body[hidden]{ display:none; }   /* same trap - see above */
.field .prof-opt{
  cursor:pointer; font:inherit; font-size:0.78rem; padding:5px 11px;
  border-radius:999px; color:var(--dim);
  background:rgba(139,144,150,0.08); border:1px solid var(--line-strong);
}
.field .prof-opt:hover{ color:var(--text); border-color:rgba(215,255,62,0.45); }
.field .prof-opt:focus-visible{ outline:2px solid var(--acc); outline-offset:2px; }
.field .prof-opt.on{
  background:rgba(215,255,62,0.14); border-color:var(--acc); color:var(--text);
}
/* A tick, so "added" is not carried by colour alone. */
.field .prof-opt.on::before{ content:"\2713\00a0"; color:var(--acc-text); }

.field .prof-addall{
  cursor:pointer; font:inherit; font-size:0.74rem; padding:5px 11px;
  border-radius:999px; background:none; border:1px dashed var(--line-strong);
  color:var(--acc-text);
}
.field .prof-addall:hover{ border-color:var(--acc); background:rgba(215,255,62,0.08); }
.field .prof-addall:focus-visible{ outline:2px solid var(--acc); outline-offset:2px; }

@media (prefers-reduced-motion: no-preference){
  .prof-group-card{ transition:border-color .18s ease, background .18s ease; }
  .prof-opt{ transition:border-color .16s ease, background .16s ease, color .16s ease; }
  .prof-caret{ transition:transform .22s ease; }
}

/* ---- claim block ----------------------------------------------------- */
.claim-block{
  margin-top:26px; padding:22px 22px 8px; border-radius:var(--radius);
  background:rgba(244,245,247,0.03); border:1px solid var(--line);
}
.claim-block h4{ margin:0 0 6px; font-size:0.98rem; }
.quota-line{
  font-family:var(--font-mono); font-size:0.78rem;
  color:var(--dim); margin:0 0 20px;
}
.quota-line.spent{ color:#E2917F; }

/* ---- generate / draft output panels ---------------------------------- */
.out-panel{
  margin-top:26px; padding:22px 24px; border-radius:var(--radius);
  background:rgba(12,16,21,0.55); border:1px solid var(--line);
}
.out-head{
  display:flex; align-items:center; justify-content:space-between;
  gap:14px; flex-wrap:wrap; margin-bottom:12px;
}
.out-head h4{ margin:0; font-size:1rem; }
.out-actions{ display:flex; gap:8px; flex-wrap:wrap; }
.out-panel h5{
  font-family:var(--font-display); font-weight:400; font-size:0.92rem;
  margin:22px 0 10px; color:var(--text);
}
.out-line{ font-size:0.9rem; margin:0 0 6px; color:var(--text); }
.out-sub{
  font-family:var(--font-mono); font-size:0.74rem;
  color:var(--dim); margin:0 0 8px; line-height:1.6;
}
/* The brief's "don't forward this" warning. Deliberately the loudest thing
   in the panel — it's the one rule that keeps a cold-sent finished-looking
   website from being a lie. */
.out-warn{
  font-size:0.82rem; color:var(--acc-text); margin:0 0 14px;
  padding:10px 13px; border-radius:var(--radius);
  background:rgba(215,255,62,0.10); border:1px solid rgba(215,255,62,0.32);
}
/* ---- generation progress circle -------------------------------------
   Shown in the output panel for as long as one generation is in flight.
   Deliberately its OWN classes rather than a reuse of `.gauge`: a gauge
   arc fills as an allowance is spent and turns red at full, which is the
   opposite meaning of a progress arc filling towards done.

   The percentage is time-based (see the long comment in js/generate.js —
   the server can't report real progress) and never reaches 100 until the
   response has actually arrived, so nothing here should imply more
   precision than that. Small, calm, one accent. */
.genprog{ display:flex; align-items:center; gap:18px; flex-wrap:wrap; }
.genprog-ringwrap{ position:relative; width:64px; height:64px; flex:none; }
.genprog-ring{ display:block; width:64px; height:64px; transform:rotate(-90deg); }
.genprog-track{ fill:none; stroke:rgba(244,245,247,0.10); stroke-width:5; }
.genprog-arc{
  fill:none; stroke:var(--acc); stroke-width:5; stroke-linecap:round;
  /* Matches the 250ms tick in js/generate.js closely enough to read as one
     continuous sweep rather than 4 steps a second. */
  transition:stroke-dashoffset .28s linear;
}
.genprog.failed .genprog-arc{ stroke:#E2917F; }
.genprog-centre{
  position:absolute; inset:0;
  display:flex; align-items:center; justify-content:center;
  pointer-events:none;
}
.genprog-pct{
  font-family:var(--font-mono); font-weight:700; font-size:0.86rem;
  color:var(--text); line-height:1; font-variant-numeric:tabular-nums;
}
.genprog.failed .genprog-pct{ color:#E2917F; }
/* 180px, not 240: the app panel on a 375px phone leaves ~233px next to the
   64px ring, and a larger basis wrapped the ring onto a line of its own with
   the text underneath — a small circle marooned above a paragraph. */
.genprog-text{ flex:1 1 180px; min-width:0; }
.genprog-text h3{
  font-family:var(--font-display); font-weight:400; font-size:1rem;
  line-height:1.3; margin:0 0 4px; color:var(--text);
}
.genprog-eta{
  font-family:var(--font-mono); font-size:0.78rem; color:var(--acc-text);
  margin:0 0 6px;
}
.genprog.failed .genprog-eta{ color:#E2917F; }
.genprog-text .out-sub{ margin:0; }
@media (prefers-reduced-motion:reduce){
  /* The number and the arc still update — this is information, not
     decoration. Only the tween between ticks goes. */
  .genprog-arc{ transition:none; }
}

.ph-list{ list-style:none; padding:0; margin:0; display:grid; gap:14px; }
.ph-list li{
  font-size:0.86rem; color:var(--text);
  padding-left:14px; border-left:2px solid rgba(139,144,150,0.3);
}
.ph-list code{
  font-family:var(--font-mono); font-size:0.82rem; color:var(--acc-text);
}
.ph-shown{ font-size:0.79rem; color:var(--dim); opacity:0.8; margin-top:3px; }
.ph-ask{ font-size:0.79rem; color:var(--dim); margin-top:3px; }

/* Static compliance text, mirrored from lib/email-render.js's
   COMPLIANCE_BANNER. Shown on every draft regardless of what the model
   wrote — if you edit the wording, edit it in both places. */
.pecr-banner{
  font-size:0.8rem; line-height:1.65; color:var(--dim);
  padding:14px 16px; border-radius:var(--radius); margin-bottom:18px;
  background:rgba(226,145,127,0.08); border:1px solid rgba(226,145,127,0.3);
}
.pecr-banner b{ color:#E2917F; }

.draft-body{
  white-space:pre-wrap; word-break:break-word;
  font-family:var(--font-body); font-size:0.88rem; line-height:1.65;
  color:var(--text); background:rgba(244,245,247,0.04);
  border:1px solid var(--line); border-radius:var(--radius);
  padding:16px 18px; margin:0 0 6px;
}
.preview-link{ color:var(--acc-text); word-break:break-all; }
.cost-note{ font-family:var(--font-mono); font-size:0.7rem; color:var(--dim); }
.pill-done{
  display:inline-flex; align-items:center; font-family:var(--font-mono);
  font-size:0.66rem; font-weight:600; text-transform:uppercase; letter-spacing:.03em;
  padding:3px 9px; border-radius:999px;
  background:rgba(139,144,150,0.16); color:var(--dim);
}
.btn-sm{ padding:7px 13px; font-size:0.79rem; }
/* Kept as a real table-cell (not flex) so the row's baseline and bottom
   border still behave; spacing comes from margins on the buttons instead. */
.accounts-table td.act{ white-space:nowrap; text-align:right; }
.accounts-table td.act .btn{ margin:2px 0 2px 8px; }
.tool-panel-scroll{ overflow-x:auto; }

/* ---- footer ------------------------------------------------------- */
/* The footer can't rely on a background change to separate itself: the
   page above it is --canvas on three of the five marketing pages, so a
   flat --canvas footer just ran on. It gets an accent hairline and a
   short lift-to-canvas fade instead, which reads as an end on every page
   regardless of what the last section happened to be. */
footer.pin-field{
  padding:clamp(80px,10vh,120px) 0 34px; position:relative; overflow:hidden;
  background:linear-gradient(180deg, var(--surface) 0%, var(--canvas) 260px);
  border-top:1px solid var(--acc-line);
}
/* The footer's drifting dot-grid was REMOVED 2026-08-01 when the
   starfield went in below it. Both were small pale dots drifting
   upward at a slow constant rate, in the same band, and running the two
   together read as noise rather than as depth — the grid's regular
   spacing actively fought the stars' randomness. The starfield does the
   job better, so this is deleted rather than dimmed. */
footer.pin-field .wrap{ position:relative; z-index:1; }

/* The full mark, used at the one place on the site with room for it.
   104px is not arbitrary — it's about the smallest size at which the mark
   is fully itself. The outer diamond's dots have r 2.8 in a 256 viewBox,
   so they render at 1.14px radius at 104px, 1.05px at 96px and 0.61px at
   56px: not small, GONE, and the anti-aliasing of the attempt is what
   makes a fine mark look dirty. The floor is ~92px; below that use
   images/logo-mark-sm.svg instead.

   And it does NOT shrink on mobile. The obvious responsive instinct here
   is a smaller mark on a phone, but 104px is already the floor — dropping
   it to 88 would put ring 4 back under a pixel on exactly the devices with
   the least forgiving pixel grid. 104px sits comfortably in a 375px
   viewport, so there is nothing to gain.

   THIS COMMENT USED TO CLOSE EARLY, and it cost the rule below it. There
   was a stray comment terminator after "logo-mark-sm.svg instead." and a
   second one at the end, so the four lines between them were parsed as
   CSS. An unknown selector makes the parser skip to the next `{...}`
   block and discard it — and the next block was `.foot-mark` itself.
   Measured before the fix: the mark computed to `display:inline;
   margin:0px; opacity:1`, i.e. none of its own rule applied, and it only
   looked roughly right because the <img> carries width/height attributes
   of 104. It was never centred and never had its bottom margin.
   Pre-existing, found 2026-08-01 while retheming. Worth remembering as a
   class of bug: a broken comment doesn't fail loudly, it eats the next
   rule.

   AND THEN THIS PARAGRAPH RECREATED IT. Describing the bug meant writing
   the terminator out, which closed the comment at that word and ate
   `.foot-mark` a second time — reported by VS Code as five bogus
   "identifier expected" errors on the rule below, which is what a eaten
   rule looks like from the outside. Never write the two-character
   terminator inside a CSS comment; say "comment terminator" instead. */
.foot-mark{
  display:block; width:104px; height:104px;
  margin:0 auto 28px; opacity:0.92;
}

.foot-cta{ text-align:center; max-width:620px; margin:0 auto clamp(56px,8vh,84px); }
.foot-cta h2{
  font-size:clamp(1.7rem,3.4vw,2.6rem); margin:0 0 22px;
  letter-spacing:-0.032em; line-height:1.06;
}
.foot-contact{
  display:flex; flex-direction:column; align-items:center; gap:6px;
  margin-bottom:44px;
  font-family:var(--font-mono); font-size:0.68rem;
  text-transform:uppercase; letter-spacing:0.12em; color:var(--dim);
}
.foot-contact a{ color:var(--acc-text); text-decoration:none; }
.foot-contact a:hover{ text-decoration:underline; }
.foot-locale{ font-size:0.62rem; color:var(--faint); letter-spacing:0.12em; }

/* ---- the social row ------------------------------------------------
   Four small islands, matching the dock's language rather than four bare
   glyphs. The icons are Phosphor regular (js/icons.js), so they inherit
   the same 16-weight stroke as every other icon on the site instead of
   looking like pasted-in brand logos.

   `.foot-contact a` above sets the accent colour and an underline on
   hover — both wrong for an icon in a circle, so both are overridden. */
.foot-social{ display:flex; align-items:center; gap:10px; margin-top:12px; }
.foot-social a{
  display:inline-flex; align-items:center; justify-content:center;
  width:38px; height:38px; border-radius:50%;
  color:var(--dim);
  background:rgba(255,255,255,0.035);
  border:1px solid rgba(255,255,255,0.08);
  box-shadow:inset 0 1px 0 rgba(255,255,255,0.06);
  transition:color .2s ease, background .2s ease, border-color .2s ease, transform .2s ease;
}
.foot-social a:hover{
  color:var(--text); background:rgba(255,255,255,0.08);
  border-color:rgba(255,255,255,0.16);
  transform:translateY(-2px);
  text-decoration:none;
}
.foot-social a:focus-visible{ outline:2px solid var(--acc); outline-offset:3px; }
.foot-social svg{ display:block; }
@media (prefers-reduced-motion:reduce){
  .foot-social a:hover{ transform:none; }
}

/* ---- the giant wordmark -------------------------------------------
   The C8VN / AeroSense sign-off: the brand name set enormous across the
   full width of the footer, as the last thing on the page. It is REAL
   TEXT, not generated content — this is the `<span>Scoutline</span>`
   already in .foot-bottom, just given the whole width — so it stays
   selectable and readable to a screen reader.

   `clamp` with a vw term does the fitting rather than a fixed size,
   because the word has to reach both edges at 320px and at 1600px.
   Tracking goes NEGATIVE hard at this scale: at 9rem the default spacing
   reads as gaps between letters rather than as a word. */
/* Three children now: the giant wordmark, the tagline, and the copyright
   line added 2026-08-01. Two columns so the tagline sits left and the
   copyright right on one baseline — stacked they read as two unrelated
   footnotes. The wordmark spans both. */
.foot-bottom{
  display:grid; grid-template-columns:minmax(0,1fr) auto;
  gap:22px 26px; align-items:end;
  border-top:1px solid var(--line); padding-top:34px;
  color:var(--dim);
}
.foot-bottom > span:first-child{
  font-family:var(--font-display);
  font-size:clamp(3.2rem,15.5vw,14rem);
  font-weight:600; letter-spacing:-0.055em; line-height:0.82;
  color:var(--text); opacity:0.09;
  user-select:none;
  /* Optical, not structural: the last line of a page should sit flush
     with the column edge, and a grotesk's left sidebearing at 14rem is
     several pixels of apparent indent. */
  margin-left:-0.014em;
}
/* nth-child(2), NOT last-child — the copyright is the last child now,
   and leaving this as :last-child silently moved the tagline's styling
   onto it and left the tagline unstyled. */
.foot-bottom > span:nth-child(2){
  grid-column:1;
  font-family:var(--font-mono); font-size:0.64rem;
  text-transform:uppercase; letter-spacing:0.11em;
  color:var(--faint); max-width:60ch;
}
.foot-copy{
  grid-column:2; justify-self:end; text-align:right; white-space:nowrap;
  font-family:var(--font-mono); font-size:0.64rem;
  text-transform:uppercase; letter-spacing:0.11em;
  color:var(--faint);
}
@media (max-width:720px){
  .foot-bottom{ grid-template-columns:minmax(0,1fr); }
  .foot-copy{ grid-column:1; justify-self:start; text-align:left; white-space:normal; }
}

/* ---- back to top -------------------------------------------------- */
.to-top{
  position:fixed; right:22px; bottom:22px; z-index:50;
  width:42px; height:42px; border-radius:50%;
  background:var(--raised); color:var(--acc-text);
  border:1px solid rgba(215,255,62,0.4); cursor:pointer; font-size:1rem;
  opacity:0; pointer-events:none; transform:translateY(8px);
  transition:opacity .3s ease, transform .3s ease, box-shadow .3s ease;
}
.to-top.show{ opacity:1; pointer-events:auto; transform:none; }
.to-top:hover{ box-shadow:0 6px 20px -6px var(--acc-glow); }
@media (prefers-reduced-motion:reduce){ .to-top{ transition:opacity .3s ease; transform:none; } }

/* ---- page head ---------------------------------------------------- */
/* IT HOSTS AN `.aurora`, SO IT MUST CLIP — the fourth host to need this
   and the last one to get it (2026-08-05).

   `.aurora` is deliberately `inset:-20% -10% auto`: it starts a fifth of
   its own height ABOVE its container and a tenth of its width outside on
   each side, so the bloom has somewhere to fall off instead of ending on
   a hard edge. That only works while the container clips it. `.hero` is
   `overflow:hidden` and `.numpin` / `.promo-band` were both given
   `overflow:clip` on 2026-08-04 for exactly this — `.page-head` was
   missed, and it is the first section on every inner page.

   Measured at 375px on about.html before the fix: 76px of glow above the
   section and 38px past each side edge, so the wash bled up behind the
   dock and out over the page margins. Worst on a phone, where 38px is a
   tenth of the screen.

   `clip`, not `hidden`, and the distinction is load-bearing: `hidden`
   makes this a scroll container, which would break `position:sticky` for
   anything inside it and give the section its own scrollport. `clip` just
   clips. `isolation:isolate` matches the other three hosts so the layer
   composites against this section and not the page behind it. */
.page-head{
  /* DOCK CLEARANCE — same rule as `.hero`, and this is the section that
     needed it most: 72px minimum against a 78px dock meant the eyebrow
     could sit level with the wordmark island on a short screen. */
  padding:clamp(108px,12vh,132px) 0 28px; position:relative;
  overflow:clip; isolation:isolate;
}
.page-head h1{
  font-size:clamp(2.1rem,4.6vw,3.6rem); font-weight:600;
  margin:14px 0 10px; letter-spacing:-0.036em; line-height:1.02;
}
.page-head p{ color:var(--dim); max-width:58ch; font-size:1.02rem; }

/* ---- the scroll-driven ribbon (how-it-works) ----------------------
   Two columns: copy left, the 3D object right. The object's column is
   `aspect-ratio` sized rather than given a fixed height so it scales
   with the layout, and js/ribbon.js reads that box on resize.

   Below 900px it is REMOVED, not shrunk. It is a decorative canvas
   running an rAF loop; on the screens most likely to be on a metered
   connection and a battery, the honest choice is not to run it at all.
   display:none also guarantees the IntersectionObserver never fires, so
   the loop is never even started. */
.ribbon-grid{
  display:grid; grid-template-columns:minmax(0,1fr) minmax(0,0.85fr);
  gap:40px; align-items:center;
}
.ribbon-copy{ min-width:0; }
.ribbon-stage{
  position:relative; width:100%; aspect-ratio:1/1;
  max-width:460px; margin-left:auto; min-width:0;
}
#ribbon{ position:absolute; inset:0; }
#ribbon canvas{ display:block; width:100%; height:100%; }
/* The object is a wire figure with no fill, so it needs something behind
   it to sit on or it reads as scratches on the page. */
.ribbon-stage::before{
  content:""; position:absolute; inset:8%; border-radius:50%;
  background:radial-gradient(circle at 46% 38%, rgba(244,245,247,0.055), transparent 64%);
  pointer-events:none;
}
.ribbon-stage .meta-corners span{ font-size:0.58rem; }
.ribbon-stage .meta-corners .tl,
.ribbon-stage .meta-corners .bl{ left:0; }
.ribbon-stage .meta-corners .tr,
.ribbon-stage .meta-corners .br{ right:0; }
@media (max-width:900px){
  .ribbon-grid{ grid-template-columns:1fr; gap:0; }
  .ribbon-stage{ display:none; }
}
/* A page head and the section under it are one unit; 24px + a full 80px
   section pad put ~104px of nothing between a page's title and its first
   real content. Only fires where a section directly follows — on
   pricing/sample-output an inline padding-top already wins. */
.page-head + section{ padding-top:40px; }

/* ---- responsive --------------------------------------------------- */
@media (max-width:900px){
  .proof-grid{ grid-template-columns:1fr; gap:36px; }
}
@media (max-width:820px){
  .scan-row, .pipeline, .price-grid, .diff-grid, .honesty-grid{ grid-template-columns:1fr; }
  /* DOCK CLEARANCE, phone. 72px was written when the header was a solid
     bar sitting IN the flow — it pushed the hero down and 72px was space
     below it. The dock floats now, so that 72px became the entire gap
     between the top of the screen and the eyebrow, against a 78px dock:
     measured 16px of daylight below the wordmark island at 375x620.
     104px keeps the phone tighter than desktop (where the floor is 118)
     while leaving the eyebrow clearly below the islands. */
  .hero{ padding:104px 0 48px; }
}

/* ====================================================================
   APP THEME — signed-in interface redesign, 2026-07-28 (fourth pass)
   ====================================================================

   WHY THIS IS AN APP-ONLY PALETTE, AND WHY IT IS A LATE OVERRIDE BLOCK.

   The interface inherited the marketing palette wholesale: ebony green
   panels on a coffee-bean maroon canvas, with olive green as the colour of
   every piece of secondary text. That reads as a brand on a landing page —
   a warm, earthy, slightly editorial thing you look at for thirty seconds.
   It reads badly as a tool you sit inside for an hour: green-on-maroon has
   no clear luminance ladder, so panels, page and table rows all sat at a
   similar visual weight and nothing looked structural. Olive body text on
   maroon is also the weakest contrast pair in the whole palette.

   So the app gets its own palette, deliberately: a cool neutral graphite
   ladder (canvas darker than surface, surface darker than raised) which
   gives real depth for free, with the brand GOLD carried over untouched as
   the single accent. Gold is what keeps this recognisably Scoutline; the
   green and the maroon are what's dropped. The seven public marketing
   pages are completely untouched — nothing in this block applies outside
   .app-body.

   The mechanism is deliberate too. Rather than rewriting the ~450 lines of
   app CSS above, the four brand variables are REDEFINED on .app-body:

       --surface / --canvas / --dim / --text / --line

   Every existing app rule already reads those variables, so the whole
   interface re-themes in one place, and any app CSS written later inherits
   the app palette automatically without having to know this block exists.
   That is the whole reason this is a variable override and not a pile of
   !important overrides. Structural changes (spacing, depth, the topbar,
   the gauge cluster, the mobile drawer) then sit on top.

   Two things to keep true if you touch this:
     - Nothing in here may live outside a `.app-body` selector.
     - --acc stays the only accent hue. A second accent is how an
       interface starts looking like a dashboard template.
   ==================================================================== */

.app-body{
  /* ---- the app-only ladder ---- */
  --app-canvas:#0C1015;      /* the page itself, the darkest thing        */
  --app-surface:#141A22;     /* panels and the sidebar                    */
  --app-raised:#1B232E;      /* inputs, rows, anything sitting on a panel */
  --app-text:#E9EEF5;
  --app-dim:#94A2B6;         /* secondary text — replaces olive entirely  */
  --app-faint:#6C7A8D;
  --app-line:rgba(244,245,247,0.09);
  --app-line-strong:rgba(244,245,247,0.17);
  --app-danger:#F08A78;
  --app-ok:#7FD1A8;

  /* ---- brand variables, re-pointed for the app only ---- */
  --canvas:var(--app-canvas);
  --surface:var(--app-surface);
  --dim:var(--app-dim);
  --text:var(--app-text);
  --line:var(--app-line);

  background:var(--app-canvas);
  color:var(--app-text);
}

/* A single very soft accent wash behind the top of the workspace. Fixed, so
   it doesn't travel with scroll and turn into a gradient band halfway down
   a long table. Purely atmospheric — sits under everything, ignores input. */
.app-body::before{
  content:""; position:fixed; inset:0; pointer-events:none; z-index:0;
  background:
    radial-gradient(900px 420px at 22% -8%, rgba(215,255,62,0.10), transparent 70%),
    radial-gradient(700px 380px at 90% 0%, rgba(127,161,209,0.07), transparent 70%);
}
/* z-index:1 is back to its original value — see js/app.js's `wireSidebar`
   for why. SUPERSEDED APPROACH (2026-07-29, briefly): this was bumped to
   48 to get `.sidebar` (a `position:fixed` descendant, LOCAL z-index:47)
   to paint above `.scrim` (appended to `<body>`, z-index:44) — raising
   the whole `.app-shell` stacking context did clear the scrim, but it
   just as easily lifted `.app-main` — the real page content behind the
   drawer — above the scrim right along with the sidebar. A tap on what
   looked like the dimmed backdrop landed on a real link underneath and
   navigated away instead of closing the drawer. Fixed properly by
   appending `.scrim` INSIDE `.app-shell` instead (a sibling of `.sidebar`
   and `.app-main`), so its z-index only has to beat `.app-main`'s locally
   — `.app-shell`'s own z-index relative to the rest of the page is back
   to just "above `.app-body::before`'s decorative wash" and has nothing
   to do with the scrim any more. */
.app-shell{ position:relative; z-index:1; grid-template-columns:252px 1fr; }

/* ---- sidebar ------------------------------------------------------- */
.app-body .sidebar{
  background:linear-gradient(180deg, #161D26 0%, var(--app-surface) 34%);
  border-right:1px solid var(--app-line);
  padding:20px 14px 16px;
}
/* The header-row divider (border-bottom/margin) moved from `.side-brand`
   to `.side-brand-row` when the settings gear joined it, 2026-07-29 — the
   row is now what needs the padding/border, not the logo link alone. */
.app-body .side-brand-row{
  padding:6px 10px 20px; border-bottom:1px solid var(--app-line); margin-bottom:16px;
}
.app-body .side-brand{ font-size:1.12rem; }
.app-body .settings-toggle{ color:var(--app-dim); }
.app-body .settings-toggle:hover,
.app-body .settings-toggle[aria-expanded="true"]{
  background:rgba(244,245,247,0.06); color:var(--app-text); border-color:var(--app-line);
}
.app-body .settings-menu{
  background:var(--app-raised); border-color:var(--app-line);
  box-shadow:0 18px 40px -20px #000;
}
.app-body .side-user b{ color:var(--app-text); }
.app-body .side-period{ color:var(--app-faint); }

/* The nav gets a section label so the sidebar reads as a structured list
   rather than five links floating in a dark column. */
.side-label{
  font-family:var(--font-mono); font-size:0.6rem; letter-spacing:.16em;
  text-transform:uppercase; color:var(--app-faint);
  padding:0 12px; margin:0 0 8px;
}
.app-body .side-nav{ gap:4px; }
.app-body .side-nav a{
  position:relative; padding:11px 13px; border-radius:9px;
  border-left:none;                    /* replaced by the ::before rail */
  color:var(--app-dim); font-size:0.89rem;
  transition:background .18s ease, color .18s ease, transform .18s ease;
}
/* The active rail is a pseudo-element rather than a left border so it can
   animate in from nothing — a border-color transition on an always-present
   2px border leaves a permanent notch in the padding. */
.app-body .side-nav a::before{
  content:""; position:absolute; left:0; top:50%; width:3px; height:0;
  border-radius:0 3px 3px 0; background:var(--acc);
  transform:translateY(-50%);
  transition:height .2s cubic-bezier(.3,1,.4,1);
}
.app-body .side-nav a:hover{
  background:rgba(244,245,247,0.055); color:var(--app-text); transform:translateX(2px);
}
.app-body .side-nav a:hover::before{ height:16px; }
.app-body .side-nav a.active{
  background:linear-gradient(90deg, rgba(215,255,62,0.16), rgba(215,255,62,0.03));
  color:var(--acc-text);
}
.app-body .side-nav a.active::before{ height:60%; }
.app-body .side-k{
  font-family:var(--font-mono); font-size:0.62rem; opacity:1;
  color:var(--app-faint); min-width:20px; letter-spacing:.05em;
}
.app-body .side-nav a.active .side-k{ color:var(--acc-text); opacity:0.8; }

.app-body .side-user{
  background:var(--app-raised); border:1px solid var(--app-line);
  border-radius:10px; padding:10px 12px; font-size:0.78rem;
}
.app-body .side-user b{ font-size:0.8rem; }
.app-body .role-pill{
  margin-left:0; border-color:rgba(215,255,62,0.35);
  background:rgba(215,255,62,0.10);
}
.app-body .side-period{ padding:0 2px; font-size:0.66rem; color:var(--app-faint); }

/* ==== THE DOCK ======================================================
   Added 2026-08-01. The workspace nav collapses to an icon rail and
   expands while the pointer or keyboard focus is inside it — the
   vertical reading of PRODUCTION/cool effects/dock.jsx. js/app.js's
   wireDock() adds `.dock-open` to `.app-shell` and writes `--mag` per
   item; everything below is what those two mean.

   THE EXPANDED PANEL FLOATS; THE GRID COLUMN DOES NOT MOVE. `.sidebar`
   goes `position:fixed` and the column stays at `--dock-rail`, so
   opening the dock never reflows the page beside it. An animated grid
   column would re-lay-out every table and panel on the page each time
   the cursor crossed the left edge — on the dashboard it visibly
   re-wraps the stat cards. This is the whole reason it is fixed rather
   than simply wider.

   NOTHING IS `display:none` WHEN COLLAPSED. The labels are clipped and
   faded, so every link keeps its full accessible name in both states and
   a screen reader is never handed five nameless icons. That is also why
   `focusin` opens the rail: tabbing into it must show what you are on.

   BELOW 901px NONE OF THIS APPLIES — the same element is the slide-out
   drawer there, already full width, with no hovering pointer to open it.
   DOCK_MIN_WIDTH in js/app.js must match the query below. */
@media (min-width:901px){
  .app-body{ --dock-rail:76px; --dock-open:252px; }
  .app-shell{ grid-template-columns:var(--dock-rail) 1fr; }

  .app-body .sidebar{
    position:fixed; top:0; left:0; height:100vh; width:var(--dock-rail);
    z-index:30; overflow:hidden; padding:20px 14px 16px;
    transition:width .26s cubic-bezier(.3,1,.4,1),
               box-shadow .26s ease, border-color .26s ease;
  }
  .app-shell.dock-open .sidebar{
    width:var(--dock-open);
    box-shadow:30px 0 70px -40px #000, 1px 0 0 0 var(--app-line);
  }
  /* EXPLICIT, AND LOAD-BEARING. `position:fixed` takes the sidebar OUT of
     the grid, so `.app-main` becomes the first in-flow item and auto-
     placement drops it into column 1 — the 76px rail. Measured: the whole
     dashboard rendered 76px wide underneath the dock. The column is a
     reserved gutter now, not something an element flows into, so the one
     remaining item has to be told where it goes. */
  .app-body .app-main{ grid-column:2; }

  /* ---- the brand row -----------------------------------------------
     The mark stays put and the wordmark fades. Fading the COLOUR rather
     than clipping the text is what avoids the rail showing "Scou" at
     rest — the letters are still there for anything reading the DOM,
     they are simply invisible. */
  .app-body .side-brand{ transition:color .2s ease; }
  .app-body .side-brand .dot{ transition:color .2s ease; }
  .app-shell:not(.dock-open) .side-brand,
  .app-shell:not(.dock-open) .side-brand .dot{ color:transparent; }
  /* THE SETTINGS GEAR IS NO LONGER IN THE DOCK (2026-08-02). It moved out
     to its own floating circle — see "GLASS DOCK" at the end of this file.
     The rule that used to fade it with the rail is gone rather than
     retargeted: `.settings-wrap` is still a descendant of `.app-shell`, so
     `:not(.dock-open) .settings-wrap{ opacity:0 }` would STILL match it in
     its new home and hide the gear whenever the pointer was anywhere but
     the dock. A selector that keeps matching after the thing it describes
     has moved is worse than no selector. */

  /* Section labels ("Workspace", "Help") are the one thing that really
     has nothing to say at 76px: they name a group whose members are
     already visible as icons. */
  .app-body .side-label{ transition:opacity .18s ease; }
  .app-shell:not(.dock-open) .side-label{ opacity:0; }

  /* ---- items -------------------------------------------------------
     The icon box is a fixed 26px at a fixed offset, so it does not move
     when the rail widens: only the label arrives. An icon that slides
     sideways on hover reads as the whole nav shifting. */
  .app-body .side-nav.dock a,
  .app-body .side-act{ gap:12px; }
  .side-ico{
    flex:none; width:26px; height:26px;
    display:grid; place-items:center;
    opacity:calc(0.62 + var(--mag,0) * 0.38);
    transform:scale(calc(1 + var(--mag,0) * 0.26));
    transition:opacity .18s ease, transform .18s cubic-bezier(.3,1,.4,1);
  }
  .side-t{
    display:flex; align-items:center; gap:11px;
    white-space:nowrap; min-width:0;
    opacity:0; transform:translateX(-4px);
    transition:opacity .18s ease, transform .22s cubic-bezier(.3,1,.4,1);
  }
  .app-shell.dock-open .side-t{ opacity:1; transform:none; }

  /* The magnified row lifts its own background as the cursor nears it.
     A calc() inside rgba() so it is one interpolated value rather than a
     class the script would have to add and remove per item. */
  .app-shell.dock-open .side-nav.dock a{
    background:rgba(244,245,247, calc(var(--mag,0) * 0.05));
  }
  /* :hover still wins, and deliberately: the magnification is proximity,
     the hover state is "this is the one you would click". They are
     different claims and the second must be unambiguous. */
  .app-body .side-nav.dock a:hover{ background:rgba(244,245,247,0.075); }
  .app-body .side-nav.dock a:hover .side-ico,
  .app-body .side-nav.dock a.active .side-ico{ opacity:1; }
}

/* Reduced motion keeps the dock (it is navigation, not decoration) and
   drops the travel: it opens, it just doesn't glide, and nothing
   magnifies — js/app.js never binds the pointer handler in that case, so
   --mag stays 0 and the two calc()s above resolve to their rest values
   on their own. */
@media (min-width:901px) and (prefers-reduced-motion:reduce){
  .app-body .sidebar,
  .side-ico, .side-t,
  .app-body .side-label, .app-body .settings-wrap,
  .app-body .side-brand, .app-body .side-brand .dot{ transition:none; }
}

/* On the mobile drawer the labels are always shown — the rules above are
   scoped to min-width:901px, so this is only stating the base state for
   the two new wrapper elements. */
.side-ico{ flex:none; display:inline-grid; place-items:center; }
.side-t{ display:inline-flex; align-items:center; gap:11px; min-width:0; }

/* ---- workspace topbar ----------------------------------------------
   New. The page title, its caption and the three credit rings used to sit
   loose at the top of the scroll area with nothing containing them, which
   is the specific thing that read as unfinished. They now sit in one
   bordered header block with a hairline under it. */
/* ONE GUTTER FOR THE WHOLE WORKSPACE, redefined per breakpoint at the
   bottom of this file and nowhere else. The header, the panel column and
   the no-plan banner are three separate elements that must line up on
   their left and right edges; before this they each typed their own
   40/24/18/14 at four breakpoints, which is twelve numbers that have to
   agree and no way to tell when they stop agreeing. */
.app-body{ --app-gutter:28px; }
.app-body .app-main{ padding:0 0 80px; max-width:none; }
/* NOT CAPPED AND NOT CENTRED (2026-08-03). It was `max-width:1200px;
   margin:0 auto`, which on a 1512 laptop left ~98px of centring gutter on
   top of the 40px padding — so the first panel began 156px right of the
   dock's edge and read as the page having come adrift from its own nav.
   Centring only makes sense against a page that starts at the viewport
   edge; this one already starts after a reserved dock column, so the
   centring was measuring from the wrong left edge. A workspace fills the
   width it is given. */
/* The fallback is not decoration: this is the one selector in the group
   not scoped to `.app-body`, and an unresolved var() invalidates the whole
   declaration — the column would lose its gutter entirely rather than
   degrade. Every other rule below states `.app-body` and cannot miss. */
.app-inner{ max-width:none; padding:0 var(--app-gutter, 28px); margin:0; }
/* FULL BLEED, EDGE TO EDGE — see the negative margin in the dock block
   below for how it escapes its own column. The horizontal padding is the
   shared gutter, so the title still lines up with the panels beneath it. */
.app-body .app-head{
  max-width:none; margin:0 0 28px; padding:34px var(--app-gutter) 26px;
  border-bottom:1px solid var(--app-line);
  align-items:center;
  /* This element is a real `<header>` tag (`interface.html` etc. use
     `<header class="app-head">`), so it also picks up the bare `header{}`
     rule from the ---- nav ---- block — including that rule's
     position:sticky and z-index. z-index is pinned explicitly here so a
     change made for the MARKETING header (a completely different element
     that happens to share a tag name) can't silently change this one too.
     Found 2026-07-29: bumping the shared `header{}` z-index from 40 to 48
     to fix the marketing mobile-nav-vs-scrim bug also pushed THIS header
     to 48 — one point above `.app-body .sidebar`'s own z-index:47 — so the
     app's sticky content header started painting over the open mobile
     sidebar drawer, exactly like the bug that prompted that fix in the
     first place. Keep this below .sidebar's z-index (see the mobile
     max-width:900px block below) whatever either of those change to. */
  position:sticky; top:0; z-index:2;
}
/* Background + blur live on a pseudo-element, not on `.app-head` itself —
   same reasoning as the marketing `header::before` above: backdrop-filter
   on an element makes it a containing block for its own position:fixed
   descendants, so keeping it off `.app-head` directly means nothing put
   inside this header later (a tooltip, a popover) can be silently broken
   by it. Added 2026-07-29: the gradient alone was only ~75% opaque at its
   own strongest point and fully transparent by its bottom edge, so text
   scrolling underneath the sticky header stayed legible enough to read
   through it — readability complaint, not a rendering bug. The blur now
   does the actual separating; the gradient just keeps the same soft
   fade-to-transparent look at the header's lower edge. */
/* ==== THE HEADER'S TINT IS A SCROLL AFFORDANCE, NOT A SURFACE ========
   This gradient exists so text scrolling UNDER the sticky header stays
   unreadable rather than showing through it (2026-07-29). But it was
   painted unconditionally, including at scroll-top with nothing behind
   it — where it has nothing to obscure and simply draws a darker band
   across the top of the page, ending in a hard horizontal seam at the
   header's bottom edge. That is the second half of the "black box
   around the text" report, and it was the same underlying mistake as
   the eyebrow: a fill that matches no surface it sits on.

   It now fades in only once the page has actually scrolled, so at rest
   the header IS the page and there is no edge to explain. `js/app.js`
   toggles `.scrolled` on `.app-body`. If the class never arrives — no
   JS, a script error — the tint simply stays off, which is the correct
   fail-open: the worst case is text showing through a header on an
   already-broken page, not a permanent band on a working one. */
.app-body .app-head::before{
  content:""; position:absolute; inset:0; z-index:-1;
  background:linear-gradient(180deg, rgba(20,26,34,0.86), rgba(20,26,34,0.55));
  backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px);
  opacity:0; transition:opacity .18s ease;
}
.app-body.scrolled .app-head::before{ opacity:1; }
@media (prefers-reduced-motion: reduce){
  .app-body .app-head::before{ transition:none; }
}
.app-body .app-head > div:first-child{ max-width:60ch; }
.app-body .app-head h1{ font-size:clamp(1.55rem,2.6vw,2rem); margin:12px 0 8px; }
.app-body .app-sub{ font-size:0.9rem; color:var(--app-dim); }
/* ==== AN EYEBROW IS A LABEL, NOT A CHIP =============================
   This used to add `padding:5px 12px` and a translucent
   `rgba(244,245,247,0.04)` fill, left over from when app eyebrows were
   styled as pills. Two things made it read as a fault rather than a
   style:

     1. It set `border-color` and nothing else, while the base .eyebrow
        rule sets `border:0`. A border-color with no width or style
        draws nothing, so the fill had no edge — a soft grey rectangle
        floating behind the text with no outline to explain it.
     2. That one fill sat on THREE different surfaces across the app —
        the page canvas, a panel, and a card header — and matched none
        of them, so it read as a box around the text on every page.

   The typography and colour were always doing the work. Keeping those
   and dropping the fill is what makes an eyebrow sit on whatever
   surface it is placed on, which is the whole point of a label. */
.app-body .eyebrow{
  font-family:var(--font-mono); font-size:0.66rem; letter-spacing:.1em;
  text-transform:uppercase;
  color:var(--app-dim);
  background:none; padding:0; border:0;
}
.app-body .eyebrow .p{ background:var(--acc); }

/* ---- credit gauges, as one cluster --------------------------------- */
.app-body .app-gauges{
  gap:6px; padding:12px 14px; border-radius:14px;
  background:rgba(20,26,34,0.72); border:1px solid var(--app-line);
  box-shadow:0 18px 40px -30px #000;
}
.app-body .gauge{
  min-width:104px; padding:8px 12px 9px; border-radius:10px;
  transition:background .18s ease;
}
/* A hairline between the figures, so the cluster reads as one block of
   three related numbers rather than three loose stacks. Not on the first
   one, and not a border on `.gauge` itself, which would double up. */
.app-body .gauge + .gauge{ border-left:1px solid var(--app-line); }
.app-body .gauge:hover{ background:rgba(244,245,247,0.04); }
.app-body .gauge-num{ font-size:1.6rem; color:var(--app-text); }
.app-body .gauge-of{ color:var(--app-faint); }
.app-body .gauge-label{ color:var(--app-dim); font-size:0.58rem; letter-spacing:.1em; }
.app-body .gauge-bar{ background:rgba(244,245,247,0.09); }
.app-body .gauge.spent .gauge-fill{ background:var(--app-danger); }
.app-body .gauge.spent .gauge-num{ color:var(--app-danger); }
.app-body .genprog-track{ stroke:rgba(244,245,247,0.09); }
.app-body .genprog.failed .genprog-arc{ stroke:var(--app-danger); }
.app-body .genprog.failed .genprog-pct,
.app-body .genprog.failed .genprog-eta{ color:var(--app-danger); }
.app-body .gauge.locked .gauge-lock{ color:var(--app-faint); }
.app-body .gauge.locked:hover .gauge-lock{ color:var(--acc-text); }

/* ---- the no-plan prompt (js/app.js renderPlanState) ------------------
   Sits directly under the app header on every page an unpaid account can
   reach. Gold-edged rather than red: this is an invitation, not an error. */
.app-body .plan-prompt{
  display:flex; align-items:center; gap:20px; flex-wrap:wrap;
  /* js/app.js inserts this as a SIBLING of .app-head, not inside
     .app-inner, so it has to reproduce the column's gutter itself. */
  margin:0 var(--app-gutter) 22px; max-width:none;
  padding:18px 22px; border-radius:12px;
  background:linear-gradient(180deg, rgba(215,255,62,0.10), rgba(215,255,62,0.04));
  border:1px solid var(--acc-line);
}
.app-body .plan-prompt-copy{ display:grid; gap:3px; min-width:0; flex:1 1 320px; }
.app-body .plan-prompt-copy b{ font-size:0.96rem; color:var(--app-text); }
.app-body .plan-prompt-copy span{ font-size:0.85rem; color:var(--app-dim); }
.app-body .plan-prompt .btn{ flex:0 0 auto; }
/* The three per-breakpoint margin overrides that used to sit here are gone:
   --app-gutter already carries the same values, in one place. */
@media (max-width:900px){
  .app-body .plan-prompt{ padding:16px 18px; }
  .app-body .plan-prompt .btn{ width:100%; justify-content:center; }
}

/* ---- panels -------------------------------------------------------- */
.app-body .panel{
  background:var(--app-surface); border:1px solid var(--app-line);
  border-radius:14px; padding:26px 28px; margin-bottom:20px;
  box-shadow:0 20px 44px -36px #000;
}
/* A one-pixel highlight along the top edge. Cheap, and it's what stops a
   flat dark panel on a flat dark page from looking like a coloured
   rectangle rather than a raised surface.
   NOT overflow:hidden — the territory and profession typeahead lists are
   absolutely positioned inside these panels and would be clipped to
   nothing. The highlight is inset by the corner radius instead, which is
   what overflow:hidden would have been doing for it. */
.app-body .panel{ position:relative; }
.app-body .panel::before{
  content:""; position:absolute; top:0; left:14px; right:14px; height:1px;
  background:linear-gradient(90deg, transparent, rgba(244,245,247,0.16) 18%,
             rgba(215,255,62,0.30) 50%, rgba(244,245,247,0.16) 82%, transparent);
}
.app-body .panel h2{
  font-size:1.05rem; color:var(--app-text);
  display:flex; align-items:center; gap:10px;
}
.app-body .panel h2::before{
  content:""; width:3px; height:15px; border-radius:2px; background:var(--acc);
}
.app-body .panel .cap{ color:var(--app-dim); font-size:0.85rem; }
.app-body .panel-head{ margin-bottom:2px; }

/* `padding:0` IS THE FIX FOR A LEAK, NOT A TIDY-UP. The stat row is a
   `<section>` (interface.html), and the marketing site's bare
   `section{ padding:clamp(88px,12vh,150px) 0 }` matches it — so the four
   dashboard cards were sitting inside 108px of empty space top AND
   bottom, 216px of nothing between the header and the leads panel.
   `.panel` and `.out-panel` both happen to set a padding shorthand of
   their own, so this row was the only app element the marketing rule
   could still reach. Same family as `header{}` reaching `.app-head` and
   `.field span{ display:block }` reaching `.req`/`.opt`: a bare element
   selector written for one half of the site, matching in the other. */
.app-body .stat-row{ gap:16px; margin-bottom:20px; padding:0; }
.app-body .stat-card{
  background:var(--app-surface); border:1px solid var(--app-line);
  border-radius:14px; padding:20px 22px; position:relative; overflow:hidden;
}
.app-body .stat-card::after{
  content:""; position:absolute; inset:auto -30% -60% auto;
  width:120px; height:120px; border-radius:50%;
  background:radial-gradient(circle, rgba(215,255,62,0.16), transparent 65%);
  opacity:0; transition:opacity .25s ease;
}
.app-body .stat-card:hover{ border-color:var(--app-line-strong); background:var(--app-raised); }
.app-body .stat-card:hover::after{ opacity:1; }
.app-body .stat-n{ font-size:1.8rem; color:var(--app-text); }
.app-body .stat-l{ color:var(--app-dim); font-size:0.76rem; }

/* ---- tables -------------------------------------------------------- */
.app-body .accounts-table th{
  color:var(--app-faint); font-size:0.62rem; letter-spacing:.1em;
  border-bottom:1px solid var(--app-line-strong);
  background:var(--app-surface);
}
.app-body .accounts-table td{
  padding:13px 8px; border-bottom:1px solid var(--app-line); font-size:0.86rem;
}
.app-body .accounts-table tbody tr{ transition:background .15s ease; }
.app-body .accounts-table tbody tr:hover{ background:rgba(244,245,247,0.035); }
.app-body .accounts-table td.role{ color:var(--app-dim); }
.app-body .table-scroll{ margin:0 -4px; padding:0 4px; }

/* ---- controls ------------------------------------------------------ */
.app-body .btn-ghost{
  background:var(--app-raised); border-color:var(--app-line-strong);
  color:var(--app-text);
}
.app-body .btn-ghost:hover{
  background:#222C39; border-color:rgba(215,255,62,0.45); color:var(--app-text);
}
.app-body .btn-acc{ box-shadow:0 10px 30px -16px var(--acc-glow); }
.app-body .btn-acc:hover{ box-shadow:0 14px 34px -12px var(--acc-glow); }
.app-body .btn[disabled]{ opacity:0.45; cursor:not-allowed; transform:none; }

.app-body .ac-wrap input,
.app-body .select,
.app-body .field input{
  background:var(--app-raised); border-color:var(--app-line-strong);
  color:var(--app-text);
}
.app-body .ac-wrap input:focus,
.app-body .select:focus,
.app-body .field input:focus{
  background:#202A36; border-color:var(--acc);
  box-shadow:0 0 0 3px rgba(215,255,62,0.16);
}
.app-body .ac-wrap input::placeholder{ color:var(--app-faint); }
.app-body .ac-list{
  background:#161D26; border-color:var(--app-line-strong);
  box-shadow:0 26px 60px -26px #000;
}
.app-body .ac-item:hover, .app-body .ac-item:focus{ background:rgba(215,255,62,0.12); }
.app-body .ac-sec{ color:var(--app-faint); }

.app-body .field > span{ color:var(--app-dim); }
.app-body .adv-body,
.app-body .claim-block,
.app-body .scan-result{
  background:rgba(12,16,21,0.5); border-color:var(--app-line);
}
.app-body .out-panel{
  background:rgba(12,16,21,0.62); border-color:var(--app-line-strong);
}
.app-body .draft-body{
  background:rgba(12,16,21,0.7); border-color:var(--app-line);
}
.app-body .chip{
  background:var(--app-raised); border-color:var(--app-line-strong);
}
.app-body .chip-x{ color:var(--app-faint); }
.app-body .pill-done{ background:rgba(127,209,168,0.14); color:var(--app-ok); }
.app-body .pill-todo{ background:rgba(244,245,247,0.07); color:var(--app-faint); }
.app-body .grade-badge.B{ background:rgba(127,161,209,0.18); color:#9DBEE6; }
.app-body .grade-badge.C, .app-body .grade-badge.D{
  background:rgba(240,138,120,0.14); color:var(--app-danger);
}
.app-body .err-line, .app-body .scan-result .err-line{ color:var(--app-danger); }
.app-body .slider-field input[type="range"]{ background:rgba(244,245,247,0.13); }
.app-body .slider-field input[type="range"]::-webkit-slider-thumb{ border-color:var(--app-surface); }
.app-body .slider-field input[type="range"]::-moz-range-thumb{ border-color:var(--app-surface); }
.app-body .terr-chosen{ background:rgba(215,255,62,0.09); }
.app-body .auth-err{ color:var(--app-danger); }
.app-body .field span{ color:var(--app-dim); font-size:0.72rem; }
.app-body .field input::placeholder{ color:var(--app-faint); }
.app-body .field .opt{ color:var(--app-faint); }
.app-body .out-sub{ color:var(--app-dim); }
.app-body .quota-line{ color:var(--app-dim); }
.app-body .quota-line.spent{ color:var(--app-danger); }
.app-body .ph-list li{ border-left-color:rgba(215,255,62,0.35); }
.app-body .ph-ask, .app-body .ph-shown{ color:var(--app-dim); }
.app-body .radio{ color:var(--app-dim); }
.app-body .adv-toggle{ color:var(--app-dim); }
.app-body .adv-toggle:hover{ color:var(--app-text); }
.app-body .slider-scale{ color:var(--app-faint); }
.app-body .scan-result{ color:var(--app-dim); }
.app-body .scan-result b{ color:var(--app-text); }

@media (prefers-reduced-motion:reduce){
  .app-body .side-nav a, .app-body .side-nav a::before,
  .app-body .stat-card, .app-body .stat-card::after,
  .app-body .accounts-table tbody tr, .app-body .gauge{ transition:none; }
  .app-body .side-nav a:hover{ transform:none; }
}

/* ====================================================================
   MOBILE — marketing nav + app interface, 2026-07-28 (fourth pass)
   ====================================================================

   Two separate problems, fixed in one pass because they share a pattern
   (an off-canvas panel with a scrim, a scroll lock and an Escape key).

   1. THE MARKETING NAV was a plain block of links that dropped down under
      the header and pushed nothing aside — no backdrop, so the page behind
      stayed fully lit and the menu read as part of the page rather than
      over it; no scroll lock, so the page scrolled behind the open menu;
      and a static ☰ glyph that never acknowledged being pressed. It is now
      a full-height panel sliding in from the right, over a dimmed scrim,
      with a hamburger that morphs into an X.

   2. THE APP had a floating hamburger pinned over the top-left of the
      content, which overlapped the page heading on narrow screens, and a
      drawer with nothing behind it — tapping "past" the drawer did
      nothing, which is the gesture everyone tries first. It now has a real
      top bar (so nothing is ever floating over content), the same scrim
      treatment, and layout that stops assuming there's room for a 4-across
      stat row and three 92px gauges side by side.

   The scrim and the animated bars are BUILT IN JS (js/main.js for the
   marketing pages, js/app.js for the app) rather than added to markup,
   deliberately: the marketing nav appears in seven hand-maintained HTML
   files and the app shell in five, and adding two elements to twelve files
   is twelve chances to leave one behind.
   ==================================================================== */

/* ---- shared scrim --------------------------------------------------- */
.scrim{
  position:fixed; inset:0; z-index:44;
  background:rgba(6,8,11,0.62);
  -webkit-backdrop-filter:blur(2px); backdrop-filter:blur(2px);
  opacity:0; pointer-events:none; transition:opacity .26s ease;
}
.scrim.show{ opacity:1; pointer-events:auto; }
body.nav-locked{ overflow:hidden; }

/* ---- animated hamburger (both navs) --------------------------------- */
.burger{
  display:inline-flex; flex-direction:column; justify-content:center;
  gap:5px; width:22px; height:16px;
}
.burger i{
  display:block; height:2px; width:100%; border-radius:2px;
  background:currentColor; transition:transform .26s cubic-bezier(.3,1,.4,1),
             opacity .18s ease;
}
[aria-expanded="true"] .burger i:nth-child(1){ transform:translateY(7px) rotate(45deg); }
[aria-expanded="true"] .burger i:nth-child(2){ opacity:0; }
[aria-expanded="true"] .burger i:nth-child(3){ transform:translateY(-7px) rotate(-45deg); }
@media (prefers-reduced-motion:reduce){
  .scrim{ transition:none; }
  .burger i{ transition:none; }
}

/* ---- marketing nav, small screens ----------------------------------- */
@media (max-width:820px){
  /* A CIRCLE, NOT A ROUNDED SQUARE, AND THAT IS GEOMETRY RATHER THAN
     TASTE. This button sits at the right-hand END of the CTA island — a
     50px-tall stadium, so its cap is a true semicircle of radius 25. A
     42px button at radius 10 puts a nearly-square corner 4px inside a
     curve that is turning through 90 degrees, and the two arcs read as a
     mistake: the shape looks like a control that failed to fit its
     container rather than one seated in it.

     999px makes it a 42px disc, concentric with the cap and ringed by an
     even 4px on all sides — the same relationship the collapsed brand and
     CTA circles have in the 821-1040 block above. Radius, not size: 42px
     is already the tap target and shrinking it to match a smaller radius
     would trade a real target for a cosmetic fit. */
  .nav-toggle{
    display:inline-flex; align-items:center; justify-content:center;
    position:relative; z-index:46; color:var(--text);
    width:42px; height:42px; padding:0; border-radius:999px;
    border:1px solid var(--line); background:rgba(12,16,21,0.35);
  }
  /* THE CLOSE BUTTON HAS TO OUTRANK THE DRAWER, AND `.nav-toggle`'s OWN
     z-index:46 CANNOT DO IT ALONE.

     `.nav-end` carries `backdrop-filter` (it is one of the three glass
     islands), and backdrop-filter creates a STACKING CONTEXT — the same
     property that made `header` a containing block in BUG #1 up in the
     header block, biting a second time in a different way. So the
     toggle's 46 is resolved INSIDE `.nav-end`, and `.nav-end` itself
     painted at `auto` — below the drawer's 45. The burger animates into
     an X on open, and that X was rendering behind the panel.

     Latent until now: the drawer had no background (see the specificity
     note below), so the X showed through and the button appeared to
     work. Restoring the panel is what made it visible. Measured with
     `elementFromPoint` at the toggle's centre — it returned `UL.open`,
     not the button, so the only ways out were Escape or the scrim.

     Raising the island rather than the button keeps the two in one pill,
     which is what it looks like on the page. */
  .nav-end{ position:relative; z-index:46; }
  /* WRITTEN AS `header.site-head #navlinks`, NOT `nav.wrap ul`, AND THAT
     IS THE WHOLE FIX (2026-08-05).

     Below 820px this element stops being one of the three glass islands
     and becomes the fixed right-hand drawer, so every island property has
     to be undone here. The island rules are all ID-scoped —
     `header.site-head #navlinks{ background … border-radius … height:50px }`
     at (1,1,1) — and `nav.wrap ul` is (0,1,2). Source order never gets a
     say when an ID is on the other side, so the drawer silently kept the
     island's transparent-pill background and lost its own: measured
     `rgba(0,0,0,0)`, no border, no shadow, links floating unreadably over
     the page with the top one under the dock. Matching the ID here makes
     the two a genuine tie, which source order then settles in favour of
     this block — the one the note above has always said owns the drawer.

     So: restate what the island sets, even where the value looks like a
     default. `border-radius`, `height` and `backdrop-filter` are not
     redundant, they are the undo. */
  header.site-head #navlinks{
    position:fixed; top:0; right:0; left:auto; bottom:0;
    width:min(80vw,300px); z-index:45;
    /* Was linear-gradient(180deg,#2C1116,...) — a hardcoded maroon, and
       the last piece of the retired palette left anywhere on the site. It
       survived the 2026-07-29 sweep because it's the one brand colour
       that was never a named variable or one of the documented hexes. */
    background:linear-gradient(180deg, var(--raised), var(--surface));
    border:0; border-left:1px solid var(--acc-line);
    box-shadow:-26px 0 60px -30px #000;
    display:flex; flex-direction:column; align-items:stretch;
    /* 88px of top padding, not 20: the dock floats over the page at
       ~78px, so anything less puts the first link under the wordmark
       island. --nav-gap is the desktop hairline spacing and is reset
       because the separators are off down here. */
    padding:88px 24px 32px; gap:6px; --nav-gap:6px;
    /* The islands are pinned to height:50px. Left in, the open drawer
       rendered as a 50px sliver. */
    height:auto; border-radius:0;
    backdrop-filter:none; -webkit-backdrop-filter:none;
    transform:translateX(100%); transition:transform .3s cubic-bezier(.3,1,.4,1);
    overflow-y:auto; overscroll-behavior:contain;
  }
  /* The panel is always in the layout now (translated off-screen) rather
     than display:none — you cannot transition from display:none, and the
     old menu appeared instantly with no motion at all because of it. */
  header.site-head #navlinks.open{ transform:none; }
  header.site-head #navlinks li{ border-bottom:1px solid var(--line); }
  header.site-head #navlinks li:last-child{ border-bottom:none; padding-top:16px; }
  /* The desktop hairline between links is drawn on `li + li::before` and
     is absolutely positioned into the gap. In a stacked drawer it lands
     mid-row as a stray vertical tick, so it comes off here. */
  header.site-head #navlinks li + li::before{ content:none; }
  header.site-head #navlinks a{
    display:block; padding:15px 4px; font-size:1.02rem; color:var(--text);
    border-radius:0;
  }
  header.site-head #navlinks a[aria-current="page"]{ color:var(--acc-text); }
  header.site-head #navlinks a.btn{ justify-content:center; padding:13px 22px; }
  /* `.btn-white` (the signed-in "Interface" link, swapped in by js/auth.js)
     needs dark text on its white fill everywhere it appears. The desktop
     rule (`nav ul a.btn-white{ color:#111 }`, above in the ---- nav ----
     block) has equal specificity to the generic `nav.wrap ul a{ color:
     var(--text) }` just above, and lost to it here only because this
     block comes later in the file — so the button rendered pale/illegible
     text on white in the mobile shelf. Repeat the override in this block
     so it wins on source order too. */
  header.site-head #navlinks a.btn-white,
  header.site-head #navlinks a.btn-white:hover{ color:#111; }
  /* Items fade up in sequence once the panel is open. --i is set in JS
     from the item's index, so adding a nav link needs no CSS change. */
  header.site-head #navlinks.open li{
    animation:nav-item-in .32s cubic-bezier(.3,1,.4,1) backwards;
    animation-delay:calc(var(--i,0) * 45ms + 90ms);
  }
  @keyframes nav-item-in{
    from{ opacity:0; transform:translateX(14px); }
    to{ opacity:1; transform:none; }
  }
}
@media (max-width:820px) and (prefers-reduced-motion:reduce){
  /* ID-scoped for the same reason the block above is: the drawer's
     `transition` is now set at (1,1,1), and a (0,1,2) override here would
     lose to it and quietly leave the slide animation on for someone who
     asked for no motion. */
  header.site-head #navlinks{ transition:none; }
  header.site-head #navlinks.open li{ animation:none; }
}

/* ---- app top bar + drawer ------------------------------------------- */
.app-topbar{
  display:none; position:sticky; top:0; z-index:46;
  align-items:center; gap:12px; padding:10px 14px;
  background:rgba(12,16,21,0.9);
  -webkit-backdrop-filter:blur(10px); backdrop-filter:blur(10px);
  border-bottom:1px solid var(--app-line);
}
.app-topbar .side-toggle{
  display:inline-flex; align-items:center; justify-content:center;
  position:static; width:40px; height:40px; padding:0;
  background:var(--app-raised); border:1px solid var(--app-line-strong);
  border-radius:10px; color:var(--app-text);
}
.app-topbar .tb-title{
  font-family:var(--font-mono); font-size:0.94rem; font-weight:600;
  color:var(--app-text); letter-spacing:.01em;
}

@media (max-width:1080px){
  .app-body{ --app-gutter:24px; }
  .app-body .app-head{ padding-top:28px; padding-bottom:22px; }
}

@media (max-width:900px){
  /* The toggle now lives INSIDE this bar (see tools-build-pages.py), so
     there's deliberately no rule hiding .side-toggle here — one would beat
     .app-topbar .side-toggle on source order and hide the only way to
     open the drawer. The old floating button is gone from the markup. */
  .app-topbar{ display:flex; }
  .app-shell{ grid-template-columns:1fr; }
  .app-body .sidebar{
    width:min(84vw,288px); z-index:47; padding-top:18px;
    box-shadow:26px 0 60px -30px #000;
  }
  .app-body{ --app-gutter:18px; }
  .app-body .app-main{ padding-bottom:56px; }
  /* No negative margin down here — the dock block above is scoped to
     901px up, so at this width the bar already fills its own column. */
  .app-body .app-head{
    padding:22px var(--app-gutter) 20px; margin-bottom:20px;
    align-items:flex-start; gap:18px;
  }
  .app-body .panel{ padding:20px 18px; border-radius:12px; }
  .app-body .stat-row{ grid-template-columns:repeat(2,1fr); gap:12px; }

  /* Three figures do not fit across a phone, and wrapping them left one
     alone on a second line. They scroll sideways instead, as one strip,
     with the cluster's own padding kept so it still reads as a group
     rather than three loose numbers. */
  .app-body .app-gauges{
    width:100%; flex-wrap:nowrap; overflow-x:auto;
    scroll-snap-type:x proximity; -webkit-overflow-scrolling:touch;
    padding:10px 12px;
  }
  .app-body .app-gauges > div{ scroll-snap-align:start; flex:0 0 auto; }

  /* Comfortable touch targets. 44px is the smallest reliably tappable
     size; the desktop sliders were 4px tall with an 18px thumb. */
  .app-body .slider-field input[type="range"]{ height:6px; margin:16px 0 8px; }
  .app-body .slider-field input[type="range"]::-webkit-slider-thumb{ width:24px; height:24px; }
  .app-body .slider-field input[type="range"]::-moz-range-thumb{ width:20px; height:20px; }
  .app-body .btn-sm{ padding:9px 14px; }
  .app-body .accounts-table td.act .btn{ margin:3px 0 3px 6px; }

  /* 16px is the threshold below which iOS Safari zooms the page on focus,
     which then leaves the layout scrolled sideways with no way back. */
  .app-body .ac-wrap input,
  .app-body .select,
  .app-body .field input{ font-size:16px; }
}

@media (max-width:560px){
  .app-body{ --app-gutter:14px; }
  .app-body .stat-row{ grid-template-columns:1fr; }
  .app-body .app-head h1{ font-size:1.4rem; }
  .app-body .app-sub{ font-size:0.86rem; }
  .app-body .app-head{ padding:18px var(--app-gutter) 18px; }
  .app-body .panel{ padding:18px 14px; }
  .app-body .out-panel{ padding:18px 14px; }
  .app-body .out-actions{ width:100%; }
  .app-body .out-actions .btn{ flex:1 1 auto; justify-content:center; }
  /* The tables are genuinely wide (7 columns on Find). Rather than
     shrinking the text until it's unreadable, they scroll — and say so,
     because a table that scrolls with no visible cue looks like a table
     that's simply been cut off. */
  .app-body .table-scroll{
    overflow-x:auto; -webkit-overflow-scrolling:touch;
    -webkit-mask-image:linear-gradient(90deg,#000 88%,transparent);
            mask-image:linear-gradient(90deg,#000 88%,transparent);
  }
  .app-body .table-scroll table{ min-width:640px; }
  /* .app-body prefix is load-bearing: the hidden rule below sits later in
     the file, so an equally-specific selector here would lose to it. */
  .app-body .scroll-hint{
    display:block; font-family:var(--font-mono); font-size:0.66rem;
    color:var(--app-faint); margin-top:8px;
  }
}
/* Hidden by default — the tables fit above 560px. */
.scroll-hint{ display:none; }

/* ---- business search result (find page) -----------------------------
   The "Already have a business in mind?" panel, 2026-07-28. Deliberately
   styled as ONE result card rather than a one-row table: a scan reports a
   count and a grade spread, this reports a single business, and dressing
   one result up as a table implies there might have been more. */
.app-body .biz-result{
  margin-top:18px; padding:16px 18px; border-radius:var(--radius);
  background:rgba(12,16,21,0.5); border:1px solid var(--app-line);
}
.app-body .biz-head{
  font-family:var(--font-mono); font-size:0.78rem; color:var(--app-dim);
  margin-bottom:12px;
}
.app-body .biz-head b{ color:var(--acc-text); }
.app-body .biz-head .out-sub{ display:inline; margin:0; }
.app-body .biz-card{
  padding:14px 16px; border-radius:var(--radius); margin-bottom:14px;
  background:var(--app-raised); border:1px solid var(--app-line);
}
.app-body .biz-name{
  display:flex; align-items:center; gap:10px; flex-wrap:wrap;
  font-family:var(--font-display); font-weight:400; font-size:1.05rem;
  color:var(--app-text); margin-bottom:4px;
}
.app-body .biz-facts{
  font-family:var(--font-mono); font-size:0.76rem;
  color:var(--app-dim); margin-top:10px; line-height:1.7;
}
/* Used for both "further out than you asked" and "this one already has a
   real website". Both are the same kind of message: we found it, here's
   the thing you should know before spending a claim on it. */
.app-body .biz-warn{
  margin-top:12px; padding:10px 12px; border-radius:var(--radius);
  font-size:0.8rem; line-height:1.6; color:var(--acc-text);
  background:rgba(215,255,62,0.10); border:1px solid rgba(215,255,62,0.30);
}
.app-body #biz-need-terr{ margin:0; }

/* ====================================================================
   TERRITORY SCAN VISUAL + DESIGN UPGRADES — 2026-07-28 (fifth pass)
   ====================================================================
   The scan canvas (js/territory-map.js) and a pass over the pieces that
   were the flattest on the marketing site: the pipeline tiles, buttons,
   the flowchart, and the cards on why-us.

   One rule held throughout: every hover effect here is a TRANSFORM or an
   OPACITY change, never a layout property. Animating width/height/margin
   on hover forces layout on every frame and is the usual reason a
   "polished" page stutters on a mid-range laptop. And every one of them
   is gated behind prefers-reduced-motion at the bottom of this block.
   ==================================================================== */

.territory-grid{
  display:grid; grid-template-columns:1fr 1fr; gap:48px; align-items:center;
}
@media (max-width:900px){ .territory-grid{ grid-template-columns:1fr; gap:32px; } }

.territory-map{
  position:relative; aspect-ratio:1/1; width:100%; max-width:480px;
  margin:0 auto; border-radius:var(--radius); overflow:hidden;
  background:
    radial-gradient(circle at 50% 45%, rgba(139,144,150,0.10), transparent 62%),
    var(--canvas);
  border:1px solid var(--line);
  box-shadow:0 30px 70px -30px rgba(0,0,0,0.75);
}
/* The canvas is sized by CSS and reads its own box back to set its device
   pixel buffer (see resize() in js/territory-map.js) — so it must have a
   real, non-zero box before the script runs. Hence 100%/100% here rather
   than relying on the element's width/height attributes. */
.territory-map canvas{ width:100%; height:100%; display:block; }
.territory-map .hud{
  position:absolute; left:16px; right:16px; bottom:14px; z-index:2;
  display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap;
  font-family:var(--font-mono); font-size:0.68rem;
  color:var(--dim); pointer-events:none;
}
.territory-map .hud b{ color:var(--acc-text); }
.territory-copy h2{ font-size:clamp(1.7rem,3.2vw,2.4rem); margin:12px 0 14px; }
.territory-copy p{ color:var(--dim); font-size:1rem; margin:0 0 14px; max-width:48ch; }
/* Used for any "this picture is not a claim" note. Deliberately smaller
   and dimmer than body copy but never hidden — the honesty captions on
   this site are load-bearing, not fine print to be styled away. */
/* --dim at 72% alpha measured 3.63:1 and failed AA. These captions are
   the ones that say a figure is illustrative rather than live data —
   under this project's honesty rule that is load-bearing text, not a
   footnote, so it gets a solid colour instead of a faded one. Contrast
   comes from the colour; the "quieter than body copy" job is done by the
   size and the accent rule down the left. */
.fig-caption{
  font-size:0.8rem !important; line-height:1.65;
  color:var(--dim) !important;
  border-left:2px solid var(--acc-line); padding-left:12px;
  margin-bottom:20px !important;
}

/* ---- buttons: a light sweep on hover -------------------------------
   One pseudo-element travelling across, clipped by the button. Cheap
   (transform only) and it reads as "this is live" without moving the
   label, which a scale would. */
.btn{ position:relative; overflow:hidden; }
.btn > *{ position:relative; z-index:1; }
.btn::after{
  content:""; position:absolute; top:0; bottom:0; left:-60%; width:45%;
  background:linear-gradient(100deg, transparent, rgba(255,255,255,0.22), transparent);
  transform:skewX(-18deg); opacity:0;
  transition:left .55s cubic-bezier(.3,1,.4,1), opacity .2s ease;
  pointer-events:none;
}
.btn:hover::after{ left:120%; opacity:1; }
.btn-acc::after{ background:linear-gradient(100deg, transparent, rgba(255,255,255,0.45), transparent); }
/* The ghost button's border picks up the gold as well, so a hover reads
   as one gesture rather than a sweep on an unrelated grey outline. */
.btn-ghost:hover{ border-color:rgba(215,255,62,0.5); }

/* ---- pipeline tiles: depth, a corner bracket, a lit rule ------------
   These were flat blocks with a background change on hover. The bracket
   and the top rule give them an instrument-panel feel that matches the
   scan visual above them, and the whole tile lifts as one. */
.stage{
  position:relative; overflow:hidden;
  transition:background .3s ease, transform .3s cubic-bezier(.3,1,.4,1);
}
.stage::before{
  content:""; position:absolute; top:0; left:0; right:0; height:2px;
  background:linear-gradient(90deg, var(--acc), rgba(215,255,62,0));
  transform:scaleX(0); transform-origin:left;
  transition:transform .4s cubic-bezier(.3,1,.4,1);
}
/* Corner bracket, drawn with two borders on a square that's rotated out
   of the way — no extra markup, and it can't affect the text flow. */
.stage::after{
  content:""; position:absolute; top:12px; right:12px;
  width:14px; height:14px;
  border-top:1px solid rgba(215,255,62,0.65);
  border-right:1px solid rgba(215,255,62,0.65);
  opacity:0; transform:translate(-6px,6px);
  transition:opacity .3s ease, transform .3s cubic-bezier(.3,1,.4,1);
}
.stage:hover{ transform:translateY(-4px); }
.stage:hover::before{ transform:scaleX(1); }
.stage:hover::after{ opacity:1; transform:none; }
.stage .k{ transition:letter-spacing .3s ease; }
.stage:hover .k{ letter-spacing:.06em; }
/* The grid's 1px gaps are the container's background showing through, so
   a lifted tile needs a background of its own to lift *over* — without
   this the translate reveals the gap colour underneath it. */
.pipeline{ overflow:visible; }
.pipeline .stage:first-child{ border-radius:var(--radius) 0 0 var(--radius); }
.pipeline .stage:last-child{ border-radius:0 var(--radius) var(--radius) 0; }
@media (max-width:820px){
  .pipeline .stage{ border-radius:0; }
}

/* The second connector, drawn OVER the arrow glyph, lived here — it is
   gone with the glyph. `.flow-link` above is the only connector now, and
   it is declared in one place rather than half here and half there. */
/* THE CARDS ARE OPAQUE AND SIT ABOVE THE RAIL. This is what confines the
   connector to the gaps: not a length, not a clip, just paint order. The
   background is already set on `.flow-node`; the z-index is what stops
   the rail showing through a card whose own background is translucent on
   some future theme. */
.flow-node{ overflow:hidden; z-index:1; }
/* A faint accent wash rising from the bottom of a lit node — the nodes are
   white on the paper section, so a shadow alone barely registers. */
.flow-node::after{
  content:""; position:absolute; inset:auto 0 0 0; height:0;
  background:linear-gradient(180deg, transparent, rgba(215,255,62,0.10));
  transition:height .45s cubic-bezier(.3,1,.4,1);
  pointer-events:none;
}
.flow-node.lit::after{ height:100%; }
.flow-node:hover{
  border-color:var(--acc);
  box-shadow:0 14px 34px -16px var(--acc-glow);
  transform:translateY(-4px);
}

/* ---- why-us cards: a cursor-following spotlight ---------------------
   THIS IS THE NO-JS FALLBACK ONLY, as of 2026-08-01. When js/flourish.js
   runs it adds `.tilt-3d`, and the tilt block near the end of this file
   overrides everything below except the fade — because a gradient
   POSITION cannot be animated without repainting, so the live version is
   a fixed gradient on a layer that gets translated instead. Read that
   block before editing this one; they have to keep looking the same.

   Left in place rather than deleted: with no script there are no custom
   properties, and `50%` centres the highlight, so the card still lights
   up on hover instead of looking broken. */
.diff-card{ position:relative; overflow:hidden; }
.diff-card::before{
  content:""; position:absolute; inset:0; pointer-events:none;
  background:radial-gradient(220px circle at var(--mx,50%) var(--my,50%),
             rgba(215,255,62,0.13), transparent 65%);
  opacity:0; transition:opacity .3s ease;
}
.diff-card:hover::before{ opacity:1; }
.diff-card > *{ position:relative; }

@media (prefers-reduced-motion:reduce){
  .btn::after{ display:none; }
  .stage, .stage::before, .stage::after, .stage .k,
  .flow-link::after, .flow-node::after, .diff-card::before{ transition:none; }
  .stage:hover, .flow-node:hover{ transform:none; }
}

/* ====================================================================
   PAGE TRANSITIONS  (js/page-transition.js, marketing pages only)
   ====================================================================
   A accent scan line sweeping over three staggered panels — the same visual
   language as the territory canvas and the homepage intro, rather than a
   generic fade.

   THE DURATIONS BELOW ARE MIRRORED IN THE SCRIPT (EXIT_MS / ENTER_MS).
   The script's failsafe timers assume these numbers; make them longer
   here without updating there and a navigation fires mid-wipe. They're
   deliberately short — 420ms out — because this sits between a click and
   the page the visitor actually asked for, and anything slower stops
   being a flourish and starts being a wait.

   pointer-events:none throughout: a decorative layer must never be able
   to swallow a click, including during the moment it's fading out. */

.pt-veil{
  position:fixed; inset:0; z-index:9000; pointer-events:none;
  overflow:hidden;
}
/* The panels. Three columns, each with its own delay, so the cover builds
   diagonally rather than dropping as one slab. */
.pt-bar{
  position:absolute; top:0; bottom:0; width:34%;
  background:linear-gradient(180deg,#1B232E 0%, var(--canvas) 60%);
  transform:scaleY(0); transform-origin:top;
}
.pt-bar:nth-child(1){ left:0; }
.pt-bar:nth-child(2){ left:33%; }
.pt-bar:nth-child(3){ left:66%; right:0; width:auto; }

/* The scan line itself, with a soft glow trailing behind it. */
.pt-line{
  position:absolute; left:0; right:0; top:0; height:2px;
  background:linear-gradient(90deg, transparent, var(--acc) 22%, #F5D389 50%, var(--acc) 78%, transparent);
  box-shadow:0 0 26px 4px var(--acc-glow);
  opacity:0;
}

/* ---- leaving ---- */
.pt-out .pt-bar{ animation:pt-cover .42s cubic-bezier(.6,0,.3,1) forwards; }
.pt-out .pt-bar:nth-child(2){ animation-delay:.05s; }
.pt-out .pt-bar:nth-child(3){ animation-delay:.1s; }
.pt-out .pt-line{ animation:pt-line-down .42s cubic-bezier(.6,0,.3,1) forwards; }
/* The veil's OWN animation is what the script waits on before navigating —
   it is the last thing to finish, so when it ends the screen really is
   covered. Don't remove it: without it the navigation fires on whichever
   bar happens to end first. */
.pt-out{ animation:pt-hold .47s linear forwards; }

/* ---- arriving ---- */
.pt-in .pt-bar{ animation:pt-uncover .5s cubic-bezier(.3,1,.4,1) forwards; }
.pt-in .pt-bar:nth-child(2){ animation-delay:.06s; }
.pt-in .pt-bar:nth-child(3){ animation-delay:.12s; }
.pt-in .pt-line{ animation:pt-line-up .5s cubic-bezier(.3,1,.4,1) forwards; }
.pt-in{ animation:pt-hold .56s linear forwards; }

@keyframes pt-cover{
  from{ transform:scaleY(0); }
  to  { transform:scaleY(1); }
}
@keyframes pt-uncover{
  from{ transform:scaleY(1); transform-origin:bottom; }
  to  { transform:scaleY(0); transform-origin:bottom; }
}
@keyframes pt-line-down{
  0%  { top:0; opacity:0; }
  12% { opacity:1; }
  100%{ top:100%; opacity:1; }
}
@keyframes pt-line-up{
  0%  { top:100%; opacity:1; }
  85% { opacity:1; }
  100%{ top:0; opacity:0; }
}
/* Does nothing visually — it exists purely to give the veil element an
   animationend of its own to fire, at a known duration. */
@keyframes pt-hold{ from{ opacity:1; } to{ opacity:1; } }

@media (prefers-reduced-motion:reduce){
  /* The script returns early under reduced motion so no veil is ever
     built, but if one somehow exists it must not cover the page. */
  .pt-veil{ display:none; }
}

/* ====================================================================
   THE INTERACTION LAYER  (js/flourish.js + js/mark3d.js)
   Added 2026-07-30. Marketing pages only.
   ====================================================================
   Everything in this block is decoration, and every rule in it is written
   so that its absence is invisible: each effect either has a resting
   state that looks deliberate, or is scoped to a class the script adds.
   No layout in the rest of the stylesheet depends on anything here.

   ONE RULE THAT IS NOT NEGOTIABLE: --acc stays the only accent hue, the
   same line the app-theme block holds. A second accent is how a site
   starts looking like a template.  */

/* ---- 1. the nav rail ------------------------------------------------
   A single accent bar under the nav that slides to whatever you point at
   and rests under the current page's link. `position:relative` on the
   list is what it measures against, and it is set HERE rather than in
   the mobile block below because at =<820px the same <ul> becomes the
   fixed drawer — where a horizontal underline points at nothing, so the
   rail is switched off entirely down there. */
/* DESKTOP ONLY, AND THAT MEDIA QUERY IS LOAD-BEARING. `nav.wrap ul` is
   styled again inside @media (max-width:820px) — where the same <ul> IS
   the fixed slide-out drawer — and this block sits later in the file with
   higher specificity, so an unscoped rule here wins at every width. That
   is not theoretical: written unscoped, this one line took `position`
   away from the drawer, which then laid out in flow 161px past the right
   edge and gave every marketing page 122px of horizontal overflow at
   375px. The stylesheet already carries a warning about exactly this
   selector; consider it earned twice. */
@media (min-width:821px){
  nav.wrap ul#navlinks{ position:relative; }
}
/* THE RAIL IS NOW A SLIDING PILL, NOT AN UNDERLINE (2026-08-05).
   It was a 2px bar at `bottom:-6px` — six pixels BELOW the list. That was
   right while the list was bare text in a full-width bar; once the list
   became a glass island the bar hung outside the pill, with its glow
   spilling under the dock.

   js/flourish.js needs no change: it already sets translateX and width to
   match the hovered link, which is exactly what a sliding highlight
   wants. Only the shape changed. Vertical placement is top/height rather
   than a transform, because the SCRIPT owns `transform` on this element
   and would overwrite any centring done that way. */
.nav-rail{
  position:absolute; left:0; top:8px; height:34px; width:0;
  background:rgba(255,255,255,0.075);
  border:1px solid rgba(255,255,255,0.07);
  box-shadow:inset 0 1px 0 rgba(255,255,255,0.09);
  border-radius:999px; opacity:0; pointer-events:none;
  z-index:0;
  /* Springy, but only just — this tracks a pointer, and any overshoot big
     enough to notice reads as lag rather than as life. */
  transition:transform .34s cubic-bezier(.34,1.3,.4,1), width .34s cubic-bezier(.34,1.3,.4,1), opacity .2s ease;
}
/* One faint accent line along the bottom of the pill, so the accent is
   still the thing marking your place — just contained now. */
.nav-rail::after{
  content:""; position:absolute; left:14%; right:14%; bottom:-1px; height:1px;
  background:linear-gradient(90deg, transparent, var(--acc), transparent);
  opacity:.55;
}
/* The links paint above the sliding pill. `.nav-rail` is appended last by
   the script, so without this it would cover the text it is highlighting. */
header.site-head #navlinks li{ z-index:1; }
nav.wrap ul li a{ transition:color .2s ease, transform .2s ease; }
nav.wrap ul li a:hover{ transform:translateY(-1px); }

/* ---- 2. header condense ---------------------------------------------
   Tint, hairline and shadow only — DELIBERATELY NOT PADDING. The header
   is sticky but still in flow, so shrinking its height on scroll pulls
   every following section up by that many pixels the moment the
   threshold trips, which reads as the page flinching. It would also
   re-fire the --header-h measurement main.js owns, and every sticky
   offset on how-it-works keys off that. Same height, different surface. */
/* Condensed now acts on the ISLANDS, not on a bar — there is no bar. Once
   you are reading, each island takes on more tint and a slightly stronger
   edge so it keeps separating from whatever scrolls under it. */
header.site-head .brand, header.site-head #navlinks, .nav-end{
  transition:background .3s ease, border-color .3s ease, box-shadow .3s ease;
}
/* THE CONDENSE IS DESKTOP-ONLY, AND THE MEDIA QUERY IS LOAD-BEARING.
   `#navlinks` is a glass island up here and the full-height drawer below
   821px. This selector is (1,2,1) — higher than the drawer's own
   (1,1,1) — so unscoped it would repaint an OPEN drawer with the island's
   tint, border and shadow the moment `.condensed` was on the header,
   which it is for anyone who scrolled before tapping the menu. Same
   family as the two bugs the drawer block above documents: a rule written
   for one breakpoint surviving into another where the same element is a
   different component. */
@media (min-width:821px){
  header.site-head.condensed .brand,
  header.site-head.condensed #navlinks,
  header.site-head.condensed .nav-end{
    /* WAS rgba(12,16,21,0.88), AND THAT IS MOST OF WHY THE SCROLLED DOCK
       READ AS A DARK PLATE. At 88% alpha the panel is very nearly opaque,
       so there is almost nothing left of the backdrop for a blur to act
       on — the frost becomes invisible not because it is missing but
       because it has nothing to do. Every screenshot of the "unblurred"
       dock was taken scrolled, i.e. in this state.

       The condense now does what it is actually for — separating the dock
       from content moving under it — WITHOUT a colour: a slightly stronger
       neutral lift, and the extra darkening handled by `brightness()` in
       the filter, exactly as on the base rule. It stays glass. */
    background:rgba(255,255,255,0.07);
    backdrop-filter:blur(34px) saturate(150%) brightness(0.60);
    -webkit-backdrop-filter:blur(34px) saturate(150%) brightness(0.60);
    border-color:rgba(255,255,255,0.14);
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.14),
      inset 0 -1px 0 rgba(0,0,0,0.3),
      0 14px 34px -20px rgba(0,0,0,1);
  }
}
/* The mark leans in slightly once you're reading. It is the one piece of
   the header that can move without shifting anything: the image keeps its
   box, only its painted content scales. */
.brand-mark{ transition:transform .45s cubic-bezier(.34,1.2,.4,1), filter .3s ease; }
header.site-head.condensed .brand-mark{ transform:rotate(45deg) scale(.92); }
.brand:hover .brand-mark{ transform:rotate(90deg) scale(1.06); filter:drop-shadow(0 0 10px var(--acc-glow)); }

/* ---- 3. scroll progress --------------------------------------------
   Above the header (48) so the hairline is never half-covered, well
   below the toast stack (200) and the transition veil (9000). */
.scroll-progress{
  position:fixed; top:0; left:0; right:0; height:2px; z-index:60;
  pointer-events:none; opacity:0; transition:opacity .3s ease;
}
.scroll-progress.lit{ opacity:1; }
.scroll-progress span{
  display:block; height:100%; width:100%;
  transform:scaleX(0); transform-origin:0 50%;
  background:linear-gradient(90deg, rgba(215,255,62,0.35), var(--acc));
  box-shadow:0 0 12px var(--acc-glow);
}

/* ---- 4. 3D tilt tiles ----------------------------------------------
   The perspective is on the tile itself, not on a wrapper, because
   `.diff-card` is `overflow:hidden` (it clips its own spotlight) and
   overflow:hidden flattens preserve-3d children — so a wrapper-plus-
   translateZ approach would silently collapse to 2D on exactly the cards
   this is most visible on. Rotating the tile as one plane under its own
   perspective works with the clip, and the depth comes from the moving
   specular instead.

   The transform deliberately RESTATES the hover lift each card already
   had (translateY): a transform property replaces, it doesn't compose, so
   omitting it would make hovering a tilted card sit back down.

   THREE TRANSITION STATES, and the middle one is the point of the
   2026-08-01 rewrite (the long note in js/flourish.js has the reasoning):

     .tilting    entering — .18s, easing across to the entry angle
     .tracking   under the pointer — NO transform transition at all
     .settling   leaving — .45s back to rest, then the class is removed

   A transition restarted on every frame is not smoothing. It is a fixed
   delay: the card can only ever be one transition-duration behind the
   cursor, which is what the old `transition-duration:.12s` here was doing.
   Tracking drops transform from the list entirely, so the angle is simply
   where the pointer is. ENTER_MS/SETTLE_MS in flourish.js must match the
   two durations below.

   WHY ALL THREE ARE TWO CLASSES, AND WHY THERE IS NO ONE-CLASS REST RULE.
   `.reveal.in` — two classes, and on every one of these cards — declares
   `transform .9s cubic-bezier(.16,1,.3,1) var(--d)`, and it goes on
   owning the transform transition long after the reveal it was written
   for has finished. The old rule here was a single class, so it lost:
   what actually ran during a tilt was `.reveal.in`'s curve, the old .12s
   duration, AND `--d`, the section's 50-300ms stagger delay. Restarted
   every frame, that card spent its whole time waiting to begin moving.
   It is most of both reported symptoms at once — "choppy", and "a delay
   between the cursor and the tile".

   So each rule below is two classes to win, and each states `transition`
   in full rather than a duration, because only the shorthand resets that
   inherited delay to 0. The resting state is deliberately NOT given the
   same treatment: at two classes it would win permanently and suppress
   the reveal's own opacity and blur, so the settle is a class the script
   adds for the 450ms it is actually needed and then takes away. */
.tilt-3d{
  /* No `transform` at rest, deliberately. It used to say
     `perspective(900px)`, which does nothing on its own — every rule
     below restates it — while outranking `.reveal`'s translateY(26px)
     and so quietly cancelling the slide-up half of these cards' reveal. */
  will-change:transform;
}
.tilt-3d.tilting{
  transition:transform .18s cubic-bezier(.2,.9,.3,1), box-shadow .35s ease, border-color .25s ease;
}
.tilt-3d.tracking{
  transition:box-shadow .35s ease, border-color .25s ease;
}
.tilt-3d.settling{
  transition:transform .45s cubic-bezier(.2,.9,.3,1), box-shadow .35s ease, border-color .25s ease;
}
.diff-card.tilt-3d.tilting{
  transform:perspective(900px) rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg)) translateY(-5px) scale(1.012);
  box-shadow:0 26px 60px -26px rgba(0,0,0,0.9);
}
.honesty-card.tilt-3d.tilting,
.price-card.tilt-3d.tilting{
  transform:perspective(900px) rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg)) translateY(-4px) scale(1.01);
  box-shadow:0 26px 60px -26px rgba(0,0,0,0.9);
}

/* THE SHEEN IS TRANSLATED, NEVER RE-CENTRED, and that is the difference
   between an effect that costs a frame and one that costs nothing.

   It used to be `radial-gradient(... at var(--mx) var(--my))` on a box
   the size of the card. Moving a gradient's centre changes the PAINT, so
   every frame re-rasterised a card-sized gradient on the main thread —
   there is no compositor path for it, however small the change.

   So the gradient is now fixed, centred in a 440px box that is itself
   centred on the card, and the script writes an offset in PIXELS FROM
   THE CENTRE which moves that box. `closest-side` on a 440px box gives
   the same 220px ending shape the old `220px circle` had, and the same
   65% stop, so the picture is unchanged — it is now just a bitmap the
   compositor slides around, and the card never repaints while you move
   over it. `overflow:hidden` on the card is what crops it to the face.

   The 105deg white streak the old .diff-card sheen layered on top is
   gone: it was 5.5% white, invisible in practice, and it would have had
   to either travel with the glow (wrong — it reads as a fixed highlight)
   or claim a second pseudo-element neither card has spare. */
.diff-card.tilt-3d::before,
.price-card.tilt-3d::before{
  inset:auto; top:50%; left:50%;
  width:440px; height:440px; margin:-220px 0 0 -220px;
  border-radius:50%;
  background:radial-gradient(circle closest-side, rgba(215,255,62,0.34), rgba(215,255,62,0.10) 55%, transparent 78%);
  transform:translate3d(var(--gx,0px), var(--gy,0px), 0);
  will-change:transform;

  /* THE SPOTLIGHT IS THE MARK, added 2026-08-04.
     A plain radial circle is what every site with this effect has. This
     one is masked to Scoutline's own mark — the 41-dot concave diamond —
     so the light that follows the cursor is shaped like the logo.

     images/spotlight-mask.svg is GENERATED, by tools-gen-spotlight-mask.js,
     from the construction documented at the top of images/logo-mark.svg:
     four rings at R_k = 26k, 4k dots each, rescaled onto a p-norm circle
     at p = 0.75. The generator checks itself against the 41 circles the
     shipped mark actually contains (worst delta 0.002px) and asserts the
     four vertices stay exactly on the axes. DO NOT hand-edit the mask —
     re-run the generator. And do not "simplify" the p-norm to the ordinary
     L2 norm: that turns the diamond back into a plain disc, which is the
     one substitution that destroys the mark.

     The dots are soft-edged IN THE MASK rather than blurred here, because
     CSS applies `filter` BEFORE `mask` — a blur on this layer would be
     clipped straight back to hard circles. */
  -webkit-mask-image:url(../images/spotlight-mask.svg);
  mask-image:url(../images/spotlight-mask.svg);
  -webkit-mask-size:100% 100%;
  mask-size:100% 100%;
  -webkit-mask-repeat:no-repeat;
  mask-repeat:no-repeat;
}
/* .diff-card already had this pseudo (the cursor spotlight further up,
   whose no-JS fallback centres itself); .price-card has to be given one,
   plus the clip and the positioning context it never needed before. */
.price-card.tilt-3d{ position:relative; overflow:hidden; }
.price-card.tilt-3d::before{
  content:""; position:absolute; pointer-events:none;
  opacity:0; transition:opacity .3s ease;
}
.diff-card.tilt-3d.tilting::before,
.price-card.tilt-3d.tilting::before{ opacity:1; }

/* .honesty-card GETS NO SHEEN, DELIBERATELY. It has both pseudo-elements
   spoken for already — ::before is the slit, ::after the cone of light it
   throws — and the sheen rule used to be written onto that ::after. Which
   did not replace it cleanly: `inset:0` and a new `background` won the
   cascade, but `filter:blur(34px)`, `width:78%`, `height:150px` and
   `translateX(-50%)` did not, so what actually rendered was the sheen
   gradient blurred by 34px, in the cone's box, half a card to the left of
   the cursor it was meant to follow. It destroyed the cone (the effect
   this card is built around), it never tracked the pointer, and blurring
   a repainted gradient every frame made it the most expensive thing on
   the homepage. The rotation is enough here; the light is the slit. */

/* ---- 5. magnetic buttons -------------------------------------------
   --pull-x/--pull-y are written by the script and default to 0, so a
   button with no script (or a coarse pointer, where the effect is never
   bound) sits exactly where the base rule puts it. The hover lift is
   restated for the same reason as the tiles: transforms replace. */
/* Two selectors, not a bare `.magnet`: an element that carries its own
   transform (a tilt card, the ribbon) would have it silently replaced
   by a blanket rule. `.magnetic` is the opt-in hook, `.btn` covers the
   primary CTAs that get it automatically. */
.btn.magnet,
.magnetic.magnet{ transform:translate3d(var(--pull-x,0px), var(--pull-y,0px), 0); }
.btn-acc.magnet:hover{
  transform:translate3d(var(--pull-x,0px), calc(var(--pull-y,0px) - 2px), 0);
}

/* ---- the 3D mark ---------------------------------------------------
   A square canvas that fills whatever box the layout gives it. The
   aspect-ratio is what stops a 0-height flex/grid item collapsing it to
   nothing before js/mark3d.js has measured anything. */
.mark3d-wrap{
  position:relative; width:100%; max-width:420px; margin:0 auto;
  aspect-ratio:1 / 1; display:flex; align-items:center; justify-content:center;
}
.mark3d-wrap canvas{ display:block; }
/* A soft pool of light under it, so the dots read as floating above the
   section rather than pasted onto it. */
.mark3d-wrap::before{
  content:""; position:absolute; inset:12%;
  background:radial-gradient(circle at 50% 55%, rgba(215,255,62,0.10), transparent 68%);
  pointer-events:none;
}
/* ---- the mark IN the hero, 2026-08-01 ------------------------------
   It used to sit in its own band under the hero at 280px. It is now a
   large object to the right of the headline that RUNS OFF the right edge
   of the viewport.

   The crop is the whole idea and it is why the numbers look odd: the box
   is sized off the VIEWPORT (vw), not off the 1180px content column, and
   then pushed right by a percentage of itself so roughly a fifth of it is
   outside the frame. A shape that continues past the edge reads as an
   object the page is a window onto; the same shape fully contained reads
   as a picture placed on the page. `.hero` already has overflow:hidden,
   which is what does the cropping.

   It sits at z-index 0 with the copy at z-index 1, so the headline always
   wins where they overlap. Pointer events are LEFT ON deliberately —
   mark3d.js offsets its idle spin toward the cursor, and the right half
   of the hero is empty space where that is the only thing to interact
   with. The copy is above it, so this cannot steal a click from a link. */
.hero-mark{
  position:absolute; top:50%; right:0; z-index:0;
  width:min(58vw,860px); aspect-ratio:1/1;
  transform:translate(19%,-50%);
  opacity:0.95;
}
.hero-mark #mark3d{ position:absolute; inset:0; }
.hero-mark canvas{ display:block; width:100%; height:100%; }

/* Below 1100px the headline needs the full column, so the mark stops
   being a side-by-side element: it drops behind the copy, much larger and
   much fainter, as a texture rather than a subject. Below 700px it goes
   entirely — it is a decorative rAF loop, and a phone is the worst place
   to spend a frame budget on one. */
@media (max-width:1100px){
  .hero-mark{
    top:44%; right:50%; width:min(120vw,720px);
    transform:translate(50%,-50%); opacity:0.35;
  }
}
@media (max-width:700px){ .hero-mark{ display:none; } }
@media (prefers-reduced-motion:reduce){ .hero-mark{ opacity:0.6; } }

@media (max-width:820px){
  /* At this width #navlinks is the drawer, not a row. */
  .nav-rail{ display:none; }
}

@media (prefers-reduced-motion:reduce){
  .nav-rail{ transition:opacity .2s ease; }
  .brand-mark, header.site-head, header.site-head::before{ transition:none; }
  header.site-head.condensed .brand-mark{ transform:none; }
  .brand:hover .brand-mark{ transform:none; }
  nav.wrap ul li a:hover{ transform:none; }
  .tilt-3d{ transition:none; will-change:auto; }
  /* The progress hairline is information, not motion — it stays, it just
     never eases. */
  .scroll-progress span{ transition:none; }
  .btn.magnet, .btn-acc.magnet:hover{ transform:none; }
}

/* ==== CONTACT + BUG REPORT FORMS, added 2026-07-30 ====================
   Three places share this block: contact.html, the contact panel in the
   app sidebar, and the beta testers' bug report panel. They are the same
   form with a different category list and a different accent, so they are
   one set of rules rather than three near-copies that drift.

   .field / .select already existed (sign-in, and the scan form) and are
   reused unchanged. What's new here is a textarea — the sign-in form never
   needed one — and it is styled off the SAME declarations as .field input
   rather than a fresh set, so a future change to input focus or background
   can't leave the textarea behind looking like a different control. */
.field textarea{
  width:100%; padding:12px 14px; border-radius:var(--radius);
  background:rgba(244,245,247,0.04); border:1px solid var(--line);
  color:var(--text); font-family:var(--font-body); font-size:0.92rem;
  line-height:1.6; resize:vertical; min-height:140px;
  transition:border-color .2s ease, background .2s ease;
}
.field textarea:focus{
  outline:none; border-color:var(--acc);
  background:rgba(244,245,247,0.07);
}
.field textarea::placeholder{ color:rgba(139,144,150,0.45); }
/* The live character count next to a textarea's label. Only ever styled as
   a warning once it's actually near the limit — a counter that looks urgent
   from the first keystroke trains people to ignore it. */
.field-count{ float:right; font-size:0.72rem; color:var(--faint); font-weight:400;
  letter-spacing:0; text-transform:none; }
.field-count.near{ color:var(--acc-text); }

.contact-grid{ display:grid; grid-template-columns:1.35fr 1fr; gap:26px; align-items:start; }
@media (max-width:900px){ .contact-grid{ grid-template-columns:1fr; } }

.form-row{ display:grid; grid-template-columns:1fr 1fr; gap:0 16px; }
@media (max-width:560px){ .form-row{ grid-template-columns:1fr; } }

/* One live region for the whole form, holding either the error or the
   confirmation. Two separate elements would let a stale success message sit
   underneath a fresh error. min-height stops the button jumping as it
   fills. */
.form-status{ font-size:0.84rem; margin:0 0 14px; min-height:1.15em; line-height:1.5; }
.form-status.err{ color:#E2917F; }
.form-status.ok{ color:#7FD1A8; }

/* The confirmation that replaces the form once a message is filed. It
   replaces it deliberately rather than sitting above it: leaving a filled-in
   form on screen after a successful send is how the same message gets sent
   three times. */
.form-sent{
  text-align:center; padding:34px 24px;
}
.form-sent .tick{
  width:46px; height:46px; border-radius:50%; margin:0 auto 16px;
  display:grid; place-items:center; font-size:1.3rem;
  background:var(--acc-soft); border:1px solid var(--acc-line); color:var(--acc-text);
}
.form-sent h3{ margin:0 0 8px; font-weight:400; }
.form-sent p{ color:var(--dim); font-size:0.88rem; margin:0 auto 18px; max-width:44ch; }

/* ---- the aside on contact.html ---- */
.contact-aside .panel{ padding:22px 24px; }
.contact-aside h3{ font-size:0.95rem; margin:0 0 10px; font-weight:400; }
.contact-aside p{ font-size:0.85rem; color:var(--dim); margin:0 0 12px; line-height:1.6; }
.contact-aside p:last-child{ margin-bottom:0; }
.contact-aside a{ color:var(--acc-text); text-decoration:none; }
.contact-aside a:hover{ text-decoration:underline; }
.contact-ways{ list-style:none; padding:0; margin:0; }
.contact-ways li{
  padding:12px 0; border-top:1px solid var(--line);
  font-size:0.85rem; color:var(--dim);
}
.contact-ways li:first-child{ border-top:0; padding-top:0; }
.contact-ways strong{
  display:block; color:var(--text); font-family:var(--font-mono);
  font-size:0.8rem; letter-spacing:0.06em; text-transform:uppercase;
  font-weight:600; margin-bottom:4px;
}

/* ==== THE HELP SHEET — in-app contact + bug report, added 2026-07-30 ====
   A modal sheet rather than a panel inside the sidebar, and that's a
   measurement rather than a preference: `.app-shell` gives the sidebar
   236px, and a form with a name, an email, a category select, a subject and
   a message does not fit in 236px at any font size worth reading. So the
   SIDEBAR holds the two entry points and the sheet holds the form.

   z-index sits above .app-shell and .sidebar (47) and below .toast-stack
   (200) — deliberately, because the confirmation toast has to be readable
   over the sheet while it closes. */
.sheet-backdrop{
  position:fixed; inset:0; z-index:150;
  background:rgba(6,9,13,0.72); backdrop-filter:blur(6px);
  display:grid; place-items:center; padding:22px 16px;
  opacity:0; transition:opacity .2s ease;
}
.sheet-backdrop.in{ opacity:1; }
/* Same rule as .settings-menu: author-origin `display` in a class beats the
   UA stylesheet's [hidden]{display:none} whatever the specificity looks
   like, so anything that sets its own display AND uses the hidden attribute
   must say this explicitly or `hidden` silently does nothing. That was a
   real, user-visible bug on the settings popover (2026-07-29). */
.sheet-backdrop[hidden]{ display:none; }
.sheet{
  width:100%; max-width:560px; max-height:88vh; overflow-y:auto;
  background:var(--surface); border:1px solid var(--line-strong);
  border-radius:14px; padding:26px 28px 28px;
  transform:translateY(10px) scale(.985); opacity:0;
  transition:transform .22s cubic-bezier(.2,.8,.3,1), opacity .22s ease;
}
.sheet-backdrop.in .sheet{ transform:none; opacity:1; }
.sheet-head{
  display:flex; align-items:flex-start; justify-content:space-between;
  gap:16px; margin-bottom:6px;
}
.sheet-head h2{ margin:0; font-size:1.1rem; }
.sheet .cap{ font-size:0.85rem; color:var(--dim); margin:0 0 20px; line-height:1.6; }
.sheet-close{
  flex:none; width:30px; height:30px; border-radius:8px; cursor:pointer;
  background:transparent; border:1px solid var(--line);
  color:var(--dim); font-size:1.05rem; line-height:1;
  transition:color .16s ease, border-color .16s ease, background .16s ease;
}
.sheet-close:hover{ color:var(--text); border-color:var(--line-strong); background:var(--raised); }
.sheet .btn-acc{ width:100%; justify-content:center; }

/* ---- the bug variant: gold, and the only accent panel on the site ----
   Asked for explicitly ("yellow/golden to match the theme and stand out").
   It uses the ONE accent the site has, structurally — a accent top rule, a
   accent-tinted surface, accent field labels — rather than introducing a second
   hue. Its scarcity is what makes it stand out; a second accent panel
   anywhere would cost this one its effect. */
.sheet.bug{
  background:linear-gradient(180deg, rgba(215,255,62,0.10), rgba(215,255,62,0.02)), var(--surface);
  border-color:var(--acc-line);
  position:relative; overflow:hidden auto;
}
.sheet.bug::before{
  content:''; position:absolute; inset:0 0 auto; height:2px; background:var(--acc);
}
.sheet.bug .sheet-head h2{ color:var(--acc-text); }
.sheet.bug .field span{ color:rgba(215,255,62,0.85); }
.sheet.bug .field input,
.sheet.bug .field textarea,
.sheet.bug .select{
  background:rgba(12,16,21,0.55); border-color:rgba(215,255,62,0.22);
}
.sheet.bug .field input:focus,
.sheet.bug .field textarea:focus,
.sheet.bug .select:focus{ border-color:var(--acc); background:rgba(12,16,21,0.75); }
.bug-badge{
  display:inline-block; font-family:var(--font-mono); font-size:0.64rem;
  letter-spacing:0.14em; text-transform:uppercase; font-weight:700;
  color:#12161C; background:var(--acc); padding:3px 9px; border-radius:999px;
  margin-bottom:9px;
}

/* ---- the two sidebar entry points ----
   Buttons, not links: they open a sheet in place rather than navigating, and
   a real <button> is what tells a keyboard and a screen reader that. Styled
   to match .side-nav a so the sidebar reads as one list. */
.side-help{ margin-top:auto; padding-top:16px; }
.side-help .side-label{ margin-bottom:8px; }
.side-act{
  display:flex; align-items:center; gap:9px; width:100%;
  padding:9px 11px; border-radius:8px; cursor:pointer; text-align:left;
  background:transparent; border:1px solid transparent;
  color:var(--dim); font-family:var(--font-body); font-size:0.84rem;
  transition:color .16s ease, background .16s ease, border-color .16s ease;
}
.side-act:hover{ color:var(--text); background:var(--raised); }
.side-act svg{ flex:none; opacity:0.8; }
/* The bug button carries the accent in the sidebar too, so a tester can
   find it without reading — which is the whole point of a report channel
   that has to be used on impulse, at the moment something breaks. */
.side-act.bug{ color:var(--acc-text); border-color:var(--acc-line); background:var(--acc-soft); }
.side-act.bug:hover{ background:rgba(215,255,62,0.2); color:var(--acc-text); }
.side-act.bug[hidden]{ display:none; }

@media (prefers-reduced-motion:reduce){
  .sheet-backdrop, .sheet{ transition:opacity .01s linear; }
  .sheet{ transform:none; }
}

/* ==== AN AI CALL THAT FAILED, EXPLAINED (added 2026-07-30) =============
   Rendered by App.renderAiFailure() in js/app.js, shared by the generate
   and draft pages.

   TWO VARIANTS, AND THE COLOUR IS THE MESSAGE. `.upstream` means Anthropic
   was at fault — the site's one accent, reading as "notice this" and
   NOT as "you broke something". `.ours` is red, because it is an apology and
   should look like one. Getting these the same colour would defeat the whole
   point of classifying the failure in the first place. */
.ai-fail{
  border-radius:12px; padding:20px 22px; margin:4px 0 14px;
  border:1px solid var(--line-strong); background:var(--raised);
  position:relative; overflow:hidden;
}
.ai-fail::before{ content:''; position:absolute; inset:0 0 auto; height:2px; }
.ai-fail.upstream{
  background:linear-gradient(180deg, rgba(215,255,62,0.10), rgba(215,255,62,0.02)), var(--raised);
  border-color:var(--acc-line);
}
.ai-fail.upstream::before{ background:var(--acc); }
.ai-fail.ours{
  background:linear-gradient(180deg, rgba(240,138,120,0.10), rgba(240,138,120,0.02)), var(--raised);
  border-color:rgba(240,138,120,0.30);
}
.ai-fail.ours::before{ background:#F08A78; }

.ai-fail-eyebrow{
  font-family:var(--font-mono); font-size:0.66rem; letter-spacing:0.14em;
  text-transform:uppercase; font-weight:700; margin:0 0 8px;
}
.ai-fail.upstream .ai-fail-eyebrow{ color:var(--acc-text); }
.ai-fail.ours    .ai-fail-eyebrow{ color:#F08A78; }

.ai-fail-head{ margin:0 0 8px; font-weight:400; font-size:1.05rem; color:var(--text); }
.ai-fail-detail{ margin:0 0 12px; font-size:0.88rem; line-height:1.6; color:var(--dim); }

/* The "no credit was used" line. Deliberately the most legible thing in the
   panel after the headline — it is the sentence that stops a support
   message, so it gets --text rather than --dim. */
.ai-fail-credit{
  margin:0 0 14px; font-size:0.86rem; color:var(--text);
  padding:9px 12px; border-radius:8px;
  background:rgba(244,245,247,0.05); border:1px solid var(--line);
}

/* Shown ONLY when the status page agrees with the failure we observed (see
   lib/claude-status.js). A pulsing dot, because a confirmed live incident is
   the one thing here that is genuinely current. */
.ai-fail-status{
  display:flex; align-items:center; gap:9px;
  margin:0 0 12px; font-size:0.85rem; color:var(--text);
}
.ai-fail-status .dot{
  flex:none; width:8px; height:8px; border-radius:50%;
  background:var(--acc); box-shadow:0 0 0 0 var(--acc-glow);
  animation:ai-fail-pulse 2s ease-out infinite;
}
@keyframes ai-fail-pulse{
  0%   { box-shadow:0 0 0 0 rgba(215,255,62,0.55); }
  70%  { box-shadow:0 0 0 7px rgba(215,255,62,0); }
  100% { box-shadow:0 0 0 0 rgba(215,255,62,0); }
}
.ai-fail-link{
  display:inline-block; margin:0 0 14px; font-size:0.84rem;
  color:var(--acc-text); text-decoration:none; border-bottom:1px solid var(--acc-line);
}
.ai-fail-link:hover{ border-bottom-color:var(--acc); }
.ai-fail .btn{ margin-right:8px; }

/* The raw error string. Collapsed, because it is noise to almost everyone
   and the first thing anyone wants in a bug report. */
.ai-fail-tech{ margin:0 0 8px; }
.ai-fail-tech summary{
  cursor:pointer; font-size:0.78rem; color:var(--faint);
  font-family:var(--font-mono); letter-spacing:0.05em;
  text-transform:uppercase; padding:4px 0;
}
.ai-fail-tech summary:hover{ color:var(--dim); }
.ai-fail-tech pre{ margin-top:8px; }

@media (prefers-reduced-motion:reduce){
  .ai-fail-status .dot{ animation:none; box-shadow:0 0 0 3px rgba(215,255,62,0.25); }
}

/* ==== BUILD FULL SITE (added 2026-07-30) ==============================
   The four-step flow on fullsite.html. Reuses .panel, .field, .out-panel
   and the button set; what's new here is the drop zones, the page picker,
   the real progress bar and the handoff block. */

.fs-namehint{
  padding:12px 14px; border-radius:var(--radius); margin-bottom:18px;
  background:var(--acc-soft); border:1px solid var(--acc-line);
  font-size:0.85rem; line-height:1.6; color:var(--text);
}
.fs-namehint code{
  font-family:var(--font-mono); font-size:0.82rem;
  background:rgba(12,16,21,0.5); padding:1px 6px; border-radius:5px; color:var(--acc-text);
}

.fs-drops{ display:grid; grid-template-columns:1fr 1fr; gap:14px; margin:18px 0 12px; }
@media (max-width:760px){ .fs-drops{ grid-template-columns:1fr; } }

/* A label-like drop zone wrapping a visually hidden file input. The input
   stays in the DOM and focusable rather than display:none, so keyboard and
   screen-reader users get a real control instead of a div that only
   responds to a mouse. */
.fs-drop{
  position:relative; display:flex; flex-direction:column; gap:5px;
  padding:20px 18px; border-radius:12px; cursor:pointer; text-align:center;
  background:rgba(244,245,247,0.03);
  border:1.5px dashed var(--line-strong);
  transition:border-color .18s ease, background .18s ease, transform .18s ease;
}
.fs-drop:hover{ border-color:var(--acc-line); background:rgba(244,245,247,0.05); }
.fs-drop.over{
  border-color:var(--acc); background:var(--acc-soft); transform:scale(1.01);
}
.fs-drop:focus-within{ border-color:var(--acc); }
.fs-drop input[type=file]{
  position:absolute; inset:0; opacity:0; cursor:pointer; width:100%; height:100%;
}
.fs-drop-ic{ font-size:1.3rem; color:var(--acc-text); line-height:1; }
.fs-drop strong{ font-family:var(--font-mono); font-size:0.9rem; color:var(--text); }
.fs-drop span{ font-size:0.8rem; color:var(--dim); line-height:1.5; }
.fs-drop .fs-drop-types{ font-size:0.72rem; color:var(--faint); }

.fs-filelist{ list-style:none; padding:0; margin:0 0 14px; display:flex; flex-direction:column; gap:6px; }
.fs-file{
  display:flex; align-items:center; gap:10px;
  padding:8px 12px; border-radius:8px;
  background:var(--raised); border:1px solid var(--line);
  font-size:0.83rem;
}
.fs-file::before{
  content:''; width:6px; height:6px; border-radius:2px; flex:none;
  background:var(--dim);
}
/* Gold for the images, because they are the ones that behave differently —
   never uploaded, packed straight into the zip. */
.fs-file.img::before{ background:var(--acc); }
.fs-file-name{ color:var(--text); flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.fs-file-note{ color:var(--faint); font-size:0.74rem; flex:none; }
.fs-file-x{
  flex:none; background:none; border:0; cursor:pointer; color:var(--faint);
  font-size:1.05rem; line-height:1; padding:0 2px;
}
.fs-file-x:hover{ color:#F08A78; }
.fs-privacy{ border-left:2px solid var(--acc-line); padding-left:12px; }

/* ---- page picker ---- */
.fs-pagegrid{ display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-bottom:16px; }
@media (max-width:760px){ .fs-pagegrid{ grid-template-columns:1fr; } }
.fs-page{
  display:flex; align-items:flex-start; gap:11px; cursor:pointer;
  padding:12px 14px; border-radius:10px;
  background:rgba(244,245,247,0.03); border:1px solid var(--line);
  transition:border-color .16s ease, background .16s ease;
}
.fs-page:hover{ border-color:var(--line-strong); background:rgba(244,245,247,0.05); }
.fs-page:has(input:checked){ border-color:var(--acc-line); background:var(--acc-soft); }
.fs-page input{ margin-top:3px; accent-color:var(--acc); flex:none; }
.fs-page input:disabled{ opacity:0.55; }
.fs-page-txt{ display:flex; flex-direction:column; gap:2px; min-width:0; }
.fs-page-txt strong{ font-size:0.88rem; color:var(--text); font-weight:600; }
.fs-page-file{ font-family:var(--font-mono); font-size:0.72rem; color:var(--faint); }
/* .fs-page-opt is no longer emitted: the optional pages now live behind the
   disclosure below instead of being tagged inside one flat list. Kept for a
   moment in case that decision is reverted; delete it if it is still unused
   next time you are in here. */
.fs-page-opt{
  font-size:0.68rem; color:var(--dim); text-transform:uppercase;
  letter-spacing:0.1em; font-family:var(--font-mono);
}

/* ---- "Add another page" disclosure (2026-07-31) ---------------------- */
.fs-more{ grid-column:1 / -1; border-top:1px solid var(--line); margin-top:4px; }
.fs-more-head{
  display:flex; align-items:center; gap:10px; width:100%;
  background:none; border:none; cursor:pointer; padding:12px 2px;
  color:var(--text); font:inherit; text-align:left;
}
.fs-more-label{ font-family:var(--font-display); font-size:0.9rem; flex:1; }
.fs-more-n{
  font-family:var(--font-mono); font-size:0.72rem; color:var(--dim);
  padding:1px 8px; border-radius:999px; background:rgba(139,144,150,0.14);
}
.fs-more-head.open .prof-caret{ transform:rotate(90deg); }
.fs-more-head:focus-visible{ outline:2px solid var(--acc); outline-offset:-2px; }
.fs-more-body{ padding-bottom:12px; }
/* Author `display` beats the UA's [hidden] — see the .prof-groups note. */
.fs-more-body[hidden]{ display:none; }

/* ---- progress ----
   A real percentage: the total is one request per page plus the shell, both
   known before the first call. Nothing here is estimated, so nothing here
   needs to creep or fake a finish. */
.fs-prog{
  padding:22px 24px; border-radius:12px; margin-bottom:18px;
  background:var(--raised); border:1px solid var(--line-strong);
}
.fs-prog-head{ display:flex; align-items:baseline; gap:14px; margin-bottom:14px; }
.fs-prog-pct{
  font-family:var(--font-mono); font-size:2rem; font-weight:700;
  color:var(--acc-text); line-height:1; font-variant-numeric:tabular-nums;
}
.fs-prog-label{ font-size:0.85rem; color:var(--dim); }
.fs-prog-bar{
  height:6px; border-radius:999px; overflow:hidden;
  background:rgba(244,245,247,0.08);
}
.fs-prog-fill{
  height:100%; width:0; border-radius:999px;
  background:linear-gradient(90deg, rgba(215,255,62,0.65), var(--acc));
  transition:width .5s cubic-bezier(.2,.8,.3,1);
}
/* IN FLIGHT. The percentage stays honest — a step that hasn't finished has
   contributed nothing and the number says so. What this adds is a sweep
   across the *unfilled* track, so a shell call that legitimately runs for
   two to four minutes reads as working rather than hung. Without it the bar
   sits perfectly still at 0% and people reload, which costs three credits.

   It deliberately does not touch .fs-prog-fill's width. Nothing here can
   overstate progress; it can only show that something is happening. */
.fs-prog.working .fs-prog-bar{ position:relative; overflow:hidden; }
.fs-prog.working .fs-prog-bar::after{
  content:''; position:absolute; inset:0;
  background:linear-gradient(90deg,
    transparent 0%, rgba(215,255,62,0.45) 50%, transparent 100%);
  transform:translateX(-100%);
  animation:fs-prog-sweep 1.6s ease-in-out infinite;
}
@keyframes fs-prog-sweep{ to{ transform:translateX(100%); } }
.fs-prog.working .fs-prog-label{ color:var(--acc-text); }

.fs-prog.complete .fs-prog-fill{ background:#7FD1A8; }
.fs-prog.failed .fs-prog-fill{ background:#F08A78; }
.fs-prog.failed .fs-prog-pct{ color:#F08A78; }
.fs-prog-list{
  list-style:none; padding:0; margin:14px 0 0;
  display:flex; flex-direction:column; gap:5px;
}
.fs-prog-list li{
  font-size:0.82rem; color:var(--dim); display:flex; align-items:center; gap:8px;
  animation:fs-step-in .3s ease-out both;
}
.fs-prog-list li::before{ content:'✓'; color:#7FD1A8; font-size:0.8rem; }
@keyframes fs-step-in{ from{ opacity:0; transform:translateX(-6px); } to{ opacity:1; transform:none; } }

.fs-done{ text-align:center; padding:28px 24px 30px; }
.fs-done-tick{
  width:52px; height:52px; border-radius:50%; margin:0 auto 14px;
  display:grid; place-items:center; font-size:1.4rem;
  background:rgba(127,209,168,0.14); border:1px solid rgba(127,209,168,0.4); color:#7FD1A8;
}
.fs-done h3{ margin:0 0 6px; font-weight:400; }
.fs-done .out-sub{ margin-bottom:16px; }

/* A PARTIAL DOWNLOAD IS NOT A SUCCESS, AND MUST NOT LOOK LIKE ONE.
   The green disc reads as "done" at a glance, which is exactly the wrong
   thing to say about a zip whose menu links go nowhere. Gold, not red: the
   pages inside are genuinely finished and were paid for — this is an
   unfinished job, not a failure. Same accent as .ai-fail.upstream. */
.fs-done-partial .fs-done-tick{
  background:rgba(215,255,62,0.14); border-color:rgba(215,255,62,0.45); color:var(--acc-text);
}

/* ---- handoff ---- */
.handoff{ border-top:1px solid var(--line); margin-top:26px; padding-top:24px; }
.handoff-h{ font-size:0.95rem; font-weight:400; margin:26px 0 8px; }
.handoff-h:first-child{ margin-top:0; }
.handoff-list{ margin:14px 0 0; padding-left:20px; display:flex; flex-direction:column; gap:14px; }
.handoff-item strong{ display:block; color:var(--text); font-size:0.9rem; margin-bottom:3px; }
.handoff-item span{ display:block; font-size:0.83rem; color:var(--dim); line-height:1.6; }
/* The contact form is the one that bites: a business owner whose enquiries
   silently vanish is the worst outcome this product can produce. */
.handoff-item.urgent strong{ color:var(--acc-text); }
.handoff-item.urgent strong::after{
  content:'do this first'; margin-left:9px; font-size:0.62rem;
  letter-spacing:0.12em; text-transform:uppercase; font-family:var(--font-mono);
  background:var(--acc); color:#12161C; padding:2px 7px; border-radius:999px;
  vertical-align:middle;
}

.handoff-vids{ display:grid; grid-template-columns:1fr 1fr; gap:14px; margin-top:12px; }
@media (max-width:760px){ .handoff-vids{ grid-template-columns:1fr; } }
/* The pointer to the written guide. This screen dies with the tab; the
   guide does not, so it is deliberately not styled as a footnote. */
.handoff-next{
  margin:14px 0 0; padding:12px 14px;
  border:1px solid var(--line-strong); border-radius:8px;
  background:var(--raised); color:var(--dim); font-size:0.9rem; line-height:1.5;
}
.handoff-next a{ color:var(--acc-text); font-weight:600; }
/* A click-to-load facade, not a live iframe: three YouTube embeds would set
   cookies and run their scripts on every load of a signed-in app page
   whether or not anyone watches. */
.handoff-vid{
  display:flex; flex-direction:column; gap:5px; text-align:left; cursor:pointer;
  padding:0 0 14px; background:none; border:1px solid var(--line);
  border-radius:12px; overflow:hidden;
  transition:border-color .18s ease, transform .18s ease;
}
.handoff-vid:hover{ border-color:var(--acc-line); transform:translateY(-2px); }
.handoff-vid-thumb{
  aspect-ratio:16/9; display:grid; place-items:center; margin-bottom:8px;
  background:linear-gradient(135deg, var(--raised), var(--raised-2));
  border-bottom:1px solid var(--line);
}
.handoff-play{
  width:46px; height:46px; border-radius:50%; display:grid; place-items:center;
  background:var(--acc); color:#12161C; font-size:0.95rem; padding-left:3px;
  transition:transform .18s ease;
}
.handoff-vid:hover .handoff-play{ transform:scale(1.09); }
.handoff-vid strong{ font-size:0.86rem; color:var(--text); padding:0 14px; }
.handoff-vid span{ font-size:0.78rem; color:var(--dim); padding:0 14px; line-height:1.5; }
.handoff-vid-live{ border-radius:12px; overflow:hidden; border:1px solid var(--acc-line); }
.handoff-vid-live iframe{ display:block; width:100%; aspect-ratio:16/9; height:auto; }

.handoff-prompt{
  max-height:300px; overflow:auto; margin:12px 0 14px;
  font-size:0.76rem; line-height:1.6;
}

@media (prefers-reduced-motion:reduce){
  .fs-prog-fill{ transition:none; }
  .fs-prog-list li{ animation:none; }
  .fs-drop, .handoff-vid, .handoff-play{ transition:none; }
  .fs-drop.over, .handoff-vid:hover{ transform:none; }
  /* The sweep is INFORMATION — "a request is in flight" — not decoration,
     so it becomes a static tint rather than disappearing. Removing it
     outright would leave someone who turned motion down staring at a bar
     that is genuinely indistinguishable from a hung one, which is the exact
     problem this was added to solve. Same call as .scroll-progress above. */
  .fs-prog.working .fs-prog-bar::after{
    animation:none; transform:none;
    background:rgba(215,255,62,0.22);
  }
}

/* ---- "also lock this territory", added 2026-07-30 -------------------
   Claiming leads and locking the area are different things, and the second
   spends a capped monthly allowance — so it is a deliberate tick, never a
   side effect. Hidden entirely on a plan with no territories (Solo), where
   the choice does not exist; a permanently-disabled checkbox would be
   offering a choice that isn't one. */
.lock-opt{
  display:flex; align-items:flex-start; gap:11px; cursor:pointer;
  padding:13px 15px; border-radius:10px; margin:4px 0 18px;
  background:var(--acc-soft); border:1px solid var(--acc-line);
  transition:background .16s ease, border-color .16s ease;
}
.lock-opt[hidden]{ display:none; }
.lock-opt:hover{ background:rgba(215,255,62,0.16); }
.lock-opt input{ margin-top:3px; accent-color:var(--acc); flex:none; }
.lock-txt{ display:flex; flex-direction:column; gap:3px; }
.lock-txt strong{ font-size:0.87rem; color:var(--text); font-weight:600; }
.lock-txt span{ font-size:0.79rem; color:var(--dim); line-height:1.5; }
/* Nothing left to spend: still shown, because the reason WHY is the useful
   part — and it names the way out (release one) rather than just greying. */
.lock-opt.spent{
  background:rgba(244,245,247,0.03); border-color:var(--line);
  cursor:default;
}
.lock-opt.spent:hover{ background:rgba(244,245,247,0.03); }
.lock-opt.spent .lock-txt strong{ color:var(--dim); }
@media (prefers-reduced-motion:reduce){ .lock-opt{ transition:none; } }

/* ==== STARFIELD =====================================================
   Adapted from the supplied `stars background` snippet. Three layers of
   box-shadow dots drifting upward at different speeds, which parallaxes
   for free: the small faint ones travel slowest, so the field has depth
   without a second element or a single line of JS.

   TWO CHANGES FROM THE SNIPPET, both worth knowing:

   1. 118 stars, not ~840. The original hard-codes three lists of several
      hundred coordinates and runs to ~50KB of CSS. At the size this is
      used (a footer band, not a full-page background) the density reads
      the same and this is 4.9KB. Coordinates are generated from a fixed
      seed, so regenerating produces an identical file rather than a
      diff full of moved dots.

   2. The looping copy uses `box-shadow:inherit` instead of a second
      verbatim list. The snippet pastes every coordinate twice — once on
      the element, once on its ::after — which is what makes it so large
      and means the two can silently drift apart when edited. A
      pseudo-element inherits from its originating element, so `inherit`
      is exactly the same list by construction.

   The loop: each layer is one 1600x700 tile; ::after holds the identical
   tile 700px below, and the whole thing translates up by exactly 700px
   per cycle — so the moment it wraps, the copy is where the original
   was. Any other travel distance shows a seam. */
.stars{
  position:absolute; inset:0; overflow:hidden;
  pointer-events:none; z-index:0;
  /* Fades in from the top so the field emerges out of the section above
     rather than starting on a hard line. */
  -webkit-mask-image:linear-gradient(180deg, transparent 0%, #000 45%);
          mask-image:linear-gradient(180deg, transparent 0%, #000 45%);
}
.stars span{
  position:absolute; top:0; left:0; background:transparent;
  animation-name:star-drift; animation-timing-function:linear;
  animation-iteration-count:infinite;
}
.stars span::after{
  content:""; position:absolute; top:700px; left:0;
  width:inherit; height:inherit; box-shadow:inherit;
}
@keyframes star-drift{
  from{ transform:translateY(0); }
  to  { transform:translateY(-700px); }
}
/* A drifting field is ambient motion with no informational value, so it
   simply stops — the stars stay, they just hold still. */
@media (prefers-reduced-motion:reduce){
  .stars span{ animation:none; }
}
.stars .s1{ width:1px; height:1px; animation-duration:90s;
  box-shadow:
    926px 573px rgba(244,245,247,0.75),
    1599px 476px rgba(244,245,247,0.75),
    925px 520px rgba(244,245,247,0.75),
    1202px 194px rgba(244,245,247,0.75),
    378px 524px rgba(244,245,247,0.75),
    974px 644px rgba(244,245,247,0.75),
    1257px 190px rgba(244,245,247,0.75),
    192px 457px rgba(244,245,247,0.75),
    621px 145px rgba(244,245,247,0.75),
    185px 551px rgba(244,245,247,0.75),
    1420px 649px rgba(244,245,247,0.75),
    85px 609px rgba(244,245,247,0.75),
    811px 463px rgba(244,245,247,0.75),
    1339px 630px rgba(244,245,247,0.75),
    1331px 161px rgba(244,245,247,0.75),
    1276px 15px rgba(244,245,247,0.75),
    1082px 64px rgba(244,245,247,0.75),
    121px 36px rgba(244,245,247,0.75),
    389px 247px rgba(244,245,247,0.75),
    1228px 30px rgba(244,245,247,0.75),
    1593px 475px rgba(244,245,247,0.75),
    668px 451px rgba(244,245,247,0.75),
    1210px 200px rgba(244,245,247,0.75),
    1063px 239px rgba(244,245,247,0.75),
    1311px 301px rgba(244,245,247,0.75),
    1023px 4px rgba(244,245,247,0.75),
    1356px 87px rgba(244,245,247,0.75),
    936px 670px rgba(244,245,247,0.75),
    569px 416px rgba(244,245,247,0.75),
    1128px 85px rgba(244,245,247,0.75),
    1449px 260px rgba(244,245,247,0.75),
    645px 235px rgba(244,245,247,0.75),
    1050px 295px rgba(244,245,247,0.75),
    60px 71px rgba(244,245,247,0.75),
    1153px 110px rgba(244,245,247,0.75),
    820px 110px rgba(244,245,247,0.75),
    595px 395px rgba(244,245,247,0.75),
    136px 17px rgba(244,245,247,0.75),
    1402px 0px rgba(244,245,247,0.75),
    437px 214px rgba(244,245,247,0.75),
    107px 481px rgba(244,245,247,0.75),
    768px 406px rgba(244,245,247,0.75),
    859px 74px rgba(244,245,247,0.75),
    1159px 644px rgba(244,245,247,0.75),
    406px 691px rgba(244,245,247,0.75),
    552px 344px rgba(244,245,247,0.75),
    178px 318px rgba(244,245,247,0.75),
    681px 15px rgba(244,245,247,0.75),
    839px 120px rgba(244,245,247,0.75),
    275px 252px rgba(244,245,247,0.75),
    1447px 103px rgba(244,245,247,0.75),
    22px 61px rgba(244,245,247,0.75),
    952px 498px rgba(244,245,247,0.75),
    363px 698px rgba(244,245,247,0.75),
    1145px 192px rgba(244,245,247,0.75),
    916px 521px rgba(244,245,247,0.75),
    390px 134px rgba(244,245,247,0.75),
    858px 659px rgba(244,245,247,0.75),
    785px 119px rgba(244,245,247,0.75),
    808px 430px rgba(244,245,247,0.75),
    435px 0px rgba(244,245,247,0.75),
    552px 606px rgba(244,245,247,0.75),
    622px 20px rgba(244,245,247,0.75),
    431px 191px rgba(244,245,247,0.75),
    807px 616px rgba(244,245,247,0.75),
    1314px 590px rgba(244,245,247,0.75),
    205px 43px rgba(244,245,247,0.75),
    299px 218px rgba(244,245,247,0.75),
    904px 264px rgba(244,245,247,0.75),
    19px 624px rgba(244,245,247,0.75); }
.stars .s2{ width:2px; height:2px; animation-duration:140s;
  box-shadow:
    673px 303px rgba(244,245,247,0.55),
    790px 75px rgba(244,245,247,0.55),
    152px 92px rgba(244,245,247,0.55),
    427px 596px rgba(244,245,247,0.55),
    1303px 248px rgba(244,245,247,0.55),
    31px 615px rgba(244,245,247,0.55),
    755px 380px rgba(244,245,247,0.55),
    1274px 464px rgba(244,245,247,0.55),
    260px 601px rgba(244,245,247,0.55),
    990px 588px rgba(244,245,247,0.55),
    277px 395px rgba(244,245,247,0.55),
    374px 642px rgba(244,245,247,0.55),
    315px 318px rgba(244,245,247,0.55),
    467px 625px rgba(244,245,247,0.55),
    510px 194px rgba(244,245,247,0.55),
    324px 644px rgba(244,245,247,0.55),
    1134px 201px rgba(244,245,247,0.55),
    1406px 397px rgba(244,245,247,0.55),
    988px 618px rgba(244,245,247,0.55),
    160px 431px rgba(244,245,247,0.55),
    97px 106px rgba(244,245,247,0.55),
    223px 39px rgba(244,245,247,0.55),
    1049px 261px rgba(244,245,247,0.55),
    488px 401px rgba(244,245,247,0.55),
    526px 430px rgba(244,245,247,0.55),
    1221px 502px rgba(244,245,247,0.55),
    600px 532px rgba(244,245,247,0.55),
    359px 70px rgba(244,245,247,0.55),
    258px 233px rgba(244,245,247,0.55),
    981px 572px rgba(244,245,247,0.55),
    1338px 630px rgba(244,245,247,0.55),
    1258px 75px rgba(244,245,247,0.55),
    573px 217px rgba(244,245,247,0.55),
    417px 16px rgba(244,245,247,0.55); }
.stars .s3{ width:2px; height:2px; animation-duration:200s;
  box-shadow:
    141px 275px rgba(244,245,247,0.9),
    842px 456px rgba(244,245,247,0.9),
    510px 61px rgba(244,245,247,0.9),
    95px 180px rgba(244,245,247,0.9),
    577px 377px rgba(244,245,247,0.9),
    1087px 585px rgba(244,245,247,0.9),
    269px 94px rgba(244,245,247,0.9),
    741px 141px rgba(244,245,247,0.9),
    922px 338px rgba(244,245,247,0.9),
    1345px 534px rgba(244,245,247,0.9),
    1196px 143px rgba(244,245,247,0.9),
    1208px 35px rgba(244,245,247,0.9),
    36px 486px rgba(244,245,247,0.9),
    732px 319px rgba(244,245,247,0.9); }
/* ==== LEGAL PAGES ===================================================
   terms.html and privacy.html. A sticky contents rail beside a long
   single column of prose.

   The measure is capped at 72ch rather than the usual ~58ch used for
   marketing copy: legal text is scanned and re-read rather than read
   through once, and a narrower column turns a 20-section document into
   an unnecessarily long scroll. It is still well short of the full
   content width.

   The rail is `position:sticky` with no JS. There is deliberately no
   scrollspy: the existing one in js/main.js is wired to the how-it-works
   sub-nav and reusing it here would couple two unrelated pages. */
.legal{ padding-top:clamp(28px,4vh,52px); }
.legal-grid{
  display:grid; grid-template-columns:minmax(0,240px) minmax(0,1fr);
  gap:clamp(32px,5vw,76px); align-items:start;
}
.legal-toc{ position:sticky; top:calc(var(--header-h) + 24px); }
.legal-toc-inner{
  border-left:1px solid var(--line); padding-left:20px;
  max-height:calc(100vh - var(--header-h) - 60px); overflow-y:auto;
}
.legal-toc .lbl{ margin-bottom:16px; }
.legal-toc ol{ list-style:none; margin:0; padding:0; }
.legal-toc li{ margin-bottom:9px; }
.legal-toc a{
  font-family:var(--font-mono); font-size:0.68rem; line-height:1.45;
  color:var(--dim); text-decoration:none; letter-spacing:-0.01em;
  display:block; transition:color .18s ease;
}
.legal-toc a:hover{ color:var(--acc-text); }

.legal-body{ max-width:72ch; min-width:0; }
.legal-body h2{
  font-size:1.32rem; letter-spacing:-0.026em; line-height:1.25;
  margin:52px 0 14px; padding-top:22px;
  border-top:1px solid var(--line);
  /* Clears the sticky header when jumped to from the contents rail.
     `scroll-margin-top` is the correct tool — adding padding would move
     the visible rule too. */
  scroll-margin-top:calc(var(--header-h) + 28px);
}
.legal-body h2:first-child{ margin-top:0; border-top:0; padding-top:0; }
.legal-body p{ color:var(--dim); margin:0 0 14px; line-height:1.72; }
.legal-body strong{ color:var(--text); font-weight:600; }
.legal-body a{ color:var(--acc-text); text-decoration:underline; text-underline-offset:2px; }
.legal-body ul{ margin:0 0 18px; padding-left:20px; color:var(--dim); line-height:1.72; }
.legal-body li{ margin-bottom:9px; }
.legal-body code{
  font-family:var(--font-mono); font-size:0.86em;
  background:var(--raised); padding:1px 5px; border-radius:4px; color:var(--text);
}

/* Definition list — used for the glossary in the terms. */
.legal-dl{ margin:0 0 18px; }
.legal-dl dt{
  font-family:var(--font-mono); font-size:0.66rem; text-transform:uppercase;
  letter-spacing:0.12em; color:var(--acc-text); margin-top:16px;
}
.legal-dl dd{ margin:5px 0 0; padding:0; color:var(--dim); line-height:1.68; }

.legal-table{
  width:100%; border-collapse:collapse; margin:0 0 22px;
  font-size:0.9rem; color:var(--dim);
}
.legal-table th{
  text-align:left; font-family:var(--font-mono); font-weight:500;
  font-size:0.6rem; text-transform:uppercase; letter-spacing:0.13em;
  color:var(--faint); padding:0 14px 10px 0;
  border-bottom:1px solid var(--line-strong);
}
.legal-table td{
  padding:13px 14px 13px 0; border-bottom:1px solid var(--line);
  vertical-align:top; line-height:1.6;
}
.legal-table tr td:first-child{ color:var(--text); }

/* The two callouts. `.legal-key` marks a passage that carries real
   consequence for the reader; `.legal-todo` marks something the business
   must still decide and is the same visible-placeholder convention the
   generated sites use — an unknown is shown, never invented. */
.legal-key{
  border-left:2px solid var(--acc); padding:12px 0 12px 16px;
  color:var(--text) !important; background:var(--acc-soft);
  padding-right:16px; border-radius:0 var(--r) var(--r) 0;
}
.legal-todo{
  border:1px dashed var(--line-strong); border-left:2px solid var(--warn);
  padding:14px 16px; border-radius:0 var(--r) var(--r) 0;
  background:rgba(255,194,75,0.05); font-size:0.9rem;
}
.legal-todo strong{ color:var(--warn); }

.legal-dates{
  display:flex; gap:26px; flex-wrap:wrap; margin-top:18px;
  font-family:var(--font-mono); font-size:0.64rem;
  text-transform:uppercase; letter-spacing:0.12em; color:var(--faint);
}
.legal-dates strong{ color:var(--dim); font-weight:400; }
.legal-foot{
  margin-top:44px; padding-top:22px; border-top:1px solid var(--line);
  font-size:0.9rem;
}

@media (max-width:900px){
  .legal-grid{ grid-template-columns:minmax(0,1fr); gap:26px; }
  /* Un-stick it: a rail pinned above the prose on a phone eats a third
     of the screen for the whole document. */
  .legal-toc{ position:static; }
  .legal-toc-inner{
    max-height:none; overflow:visible;
    border-left:0; padding-left:0;
    border-top:1px solid var(--line); padding-top:20px;
  }
  .legal-toc ol{ columns:2; column-gap:22px; }
  .legal-body{ max-width:none; }
  /* A table with three columns of prose does not fit a phone; let it
     scroll rather than squeezing every cell to two words wide. */
  .legal-table{ display:block; overflow-x:auto; white-space:nowrap; }
  .legal-table td, .legal-table th{ white-space:normal; min-width:150px; }
}

/* Footer link row. Was a single "Contact us" link; Terms and Privacy
   joined it 2026-08-01, and three links in a column read as a stack of
   unrelated items — so they sit on one line with hairline separators. */
.foot-links{ display:inline-flex; align-items:center; gap:0; flex-wrap:wrap; justify-content:center; }
.foot-links a{ padding:0 14px; position:relative; color:var(--acc-text); text-decoration:none; }
.foot-links a:hover{ text-decoration:underline; }
.foot-links a + a::before{
  content:""; position:absolute; left:0; top:50%;
  width:1px; height:11px; margin-top:-5px; background:var(--line-strong);
}

/* ==== THE ADMIN PIN GATE (accounts.html) =============================
   A narrow panel on its own, because that is the whole page while it is
   showing — the admin surface behind it is hidden, not dimmed, so there
   is nothing for a full-width panel to sit against.

   No padlock iconography and no red. This is a routine second step for
   the one person who is meant to be here, not a warning that something
   has gone wrong; `.auth-err` inside it carries the only alarming colour
   on the panel, and only once something actually is wrong. */
.pin-gate{ max-width:420px; }
.pin-gate .field{ margin:14px 0 0; }
.pin-gate input{
  /* Tracked-out mono, so six identical dots are still countable. */
  font-family:var(--font-mono); font-size:1.1rem; letter-spacing:0.35em;
  text-align:center;
}
.pin-gate .row-actions{ display:flex; gap:10px; align-items:center; margin-top:16px; }
.pin-gate .row-actions .btn{ margin:0; }
/* The change-PIN panel keeps the same mono treatment, so the two read as
   the same control in two places. */
#pin-panel input{ font-family:var(--font-mono); letter-spacing:0.3em; }

/* ==== EMPTY IS NOT A STATE WORTH DRAWING ==============================
   Two elements that are in the markup on every load and only ever get
   content sometimes. Both were reserving space, and one was drawing a
   border around nothing.

   `.app-gauges` is the allowance rings in the page header. accounts.html
   is the one app page with no allowances to show, so it shipped an empty
   819x22 outlined capsule under the heading — a box with nothing in it,
   which reads as something that failed to load rather than as something
   that was never there.

   `.auth-err` is the inline error line above a form. Empty, it still took
   its own height plus a 14px margin, so every form opened with a gap in
   it that closed the moment an error appeared — the layout shifting
   backwards on the way to a bad outcome. */
.app-gauges:empty{ display:none; }
.auth-err:empty{ display:none; margin:0; }

/* ==== GLOW BUTTON ===================================================
   Added 2026-08-01. Ported from PRODUCTION/cool effects/glow effect
   button.jsx — a spectral halo that drifts behind a call to action.

   THE HUES ARE --spec-1/2/3 PLUS --acc, AND THIS IS THE SECOND PLACE
   THEY ARE ALLOWED. The token header says those three may only appear
   far out of focus, in the aurora. That rule is about not letting a
   second accent turn into paint — so the condition, not the location, is
   what matters, and this satisfies it: 18px of blur, never a fill, never
   a text colour, and the button itself keeps the one accent it always
   had. If a future glow appears anywhere sharp, that is the line being
   crossed, not this.

   IT IS A HALO, NOT A BACKGROUND. The layer sits at z-index -1 behind an
   opaque button, so the label's contrast is exactly what it was; nothing
   here is under any text.

   WHAT IT COSTS, PLAINLY: animating `background-position` repaints the
   layer each frame rather than compositing it, which is the thing the
   card sheen was rewritten to stop doing. It is accepted here and only
   here because the layer is one small button-sized box rather than a
   card-sized one, and because there are a handful on the site rather
   than one per grid item. If glow buttons ever multiply, this becomes a
   translated oversized layer instead. */
.glow-wrap{
  position:relative; display:inline-flex; isolation:isolate;
  /* --r-pill, not --r. The halo is a SEPARATE layer behind the button and
     both of its pseudo-elements take `border-radius:inherit` from here —
     so when buttons became stadium on 2026-08-04 this was left describing
     the old 6px rounded rectangle, and the glow kept the shape the button
     no longer had. Visible as square-ish corners of light behind a pill. */
  border-radius:var(--r-pill);
}
.glow-wrap::before{
  content:""; position:absolute; inset:-3px; z-index:-1;
  border-radius:inherit;
  background:linear-gradient(90deg,
    var(--spec-1), var(--spec-2), var(--acc), var(--spec-3), var(--spec-1));
  background-size:300% 100%;
  filter:blur(14px); opacity:0.55;
  animation:glow-drift 7s linear infinite;
}
/* A second, tighter copy gives the halo an edge to sit against — without
   it the blur alone reads as a smudge rather than as light coming from
   behind the button. */
.glow-wrap::after{
  content:""; position:absolute; inset:-1px; z-index:-1;
  border-radius:inherit;
  background:linear-gradient(90deg,
    var(--spec-1), var(--spec-2), var(--acc), var(--spec-3), var(--spec-1));
  background-size:300% 100%;
  filter:blur(4px); opacity:0.4;
  animation:glow-drift 7s linear infinite;
}
.glow-wrap:hover::before{ opacity:0.8; }
.glow-wrap:hover::after{ opacity:0.6; }
.glow-wrap > .btn{ position:relative; z-index:1; }
@keyframes glow-drift{
  from{ background-position:0% 50%; }
  to{ background-position:300% 50%; }
}
/* Still lit, no longer moving. Removing the glow outright would take the
   emphasis off the primary action for the people who asked for less
   motion, which is not what that preference means. */
@media (prefers-reduced-motion:reduce){
  .glow-wrap::before,
  .glow-wrap::after{ animation:none; background-position:35% 50%; }
}

/* ==== TEXT LOOP =====================================================
   js/text-effects.js. One word in a sentence cycles through a list.

   The items are absolutely stacked and the CONTAINER's width is animated
   to the active one, so the words after it slide rather than jumping.
   `overflow:hidden` is what clips a word on its way in and out — this is
   a slot, and the letters travel through it.

   WITHOUT JAVASCRIPT the first item is simply visible and the rest are
   not, because `.tl-ready` (which is what stacks and hides them) is
   added by the script. The sentence reads correctly either way, which is
   the point: it is a marketing claim, not decoration. */
.text-loop{
  position:relative; display:inline-block; vertical-align:bottom;
  overflow:hidden; text-align:left;
  transition:width .42s cubic-bezier(.3,1,.4,1);
}
.text-loop > span{ display:inline-block; white-space:nowrap; }
.text-loop.tl-ready > span{
  position:absolute; left:0; top:0;
  opacity:0; transform:translateY(0.75em);
  filter:blur(3px);
  transition:opacity .38s ease, transform .42s cubic-bezier(.3,1,.4,1),
             filter .38s ease;
  pointer-events:none;
}
.text-loop.tl-ready > span.in{
  position:relative;
  opacity:1; transform:none; filter:none; pointer-events:auto;
}
.text-loop.tl-ready > span.out{ opacity:0; transform:translateY(-0.75em); filter:blur(3px); }
@media (prefers-reduced-motion:reduce){
  .text-loop{ transition:none; }
  .text-loop.tl-ready > span{ transition:none; }
}

/* ==== TEXT ROLL =====================================================
   js/text-effects.js. Each character flips in once, as the line arrives.
   ONCE — a heading that keeps flipping is a heading nobody can read.

   `perspective` on the line and `rotateX` on the character is what makes
   this a flip rather than a squash. The stagger is `--i`, the
   character's index, so the wave reads left to right. */
.text-roll.tr-split{ perspective:600px; }
.tr-inner{ display:inline-block; }
.tr-c{
  display:inline-block; transform-origin:50% 50% -0.35em;
  transform:rotateX(-92deg); opacity:0;
  filter:blur(2px);
  transition:transform .5s cubic-bezier(.22,1,.3,1),
             opacity .34s ease, filter .34s ease;
  transition-delay:calc(var(--i,0) * 26ms);
}
.text-roll.rolled .tr-c{ transform:none; opacity:1; filter:none; }
@media (prefers-reduced-motion:reduce){
  .tr-c{ transition:none; transform:none; opacity:1; filter:none; }
}

/* ==== TEXT SHIMMER WAVE =============================================
   js/text-effects.js. A wave of light travels through a line.

   FOR WORK IN PROGRESS ONLY. It is a progress signal; on a finished
   label it means nothing and reads as a stuck spinner. `--n` is the
   character count, set by the script, so a long label and a short one
   travel at the same SPEED rather than taking the same time.

   The colour lift lands on --acc rather than on a second hue: this is
   the site's own accent brightening as the wave passes, not a rainbow. */
.shimmer-wave.sw-split{ display:inline-block; color:var(--dim); }
.sw-inner{ display:inline-block; }
.sw-c{
  display:inline-block; white-space:pre;
  animation:sw-wave calc(var(--n,10) * 90ms + 700ms) linear infinite;
  animation-delay:calc(var(--i,0) * 90ms);
}
@keyframes sw-wave{
  0%, 62%, 100%{ color:inherit; transform:none; }
  22%{ color:var(--acc-text); transform:translateY(-0.16em) scale(1.06); }
}
/* A wave crossing a word forever is exactly what the preference is
   asking about. It stops; the label stays. */
@media (prefers-reduced-motion:reduce){
  .sw-c{ animation:none; }
}

/* ---- glow button in context ----------------------------------------
   The wrapper became the flex item where the button used to be one, so
   the two places whose layout depended on the button being that item get
   the spacing moved onto the wrapper. `.price-card ul{ margin:0 0 auto }`
   is what bottom-pins the CTA in a plan card; without the width:100%
   here the wrapper stretches and the button inside it does not. */
.price-card .glow-wrap{ margin-top:22px; width:100%; }
.price-card .glow-wrap .plan-cta{ margin-top:0; width:100%; }

/* The looping line under the pipeline heading. Sits with the section
   head's own copy, one step quieter than it — it is a caption on the
   argument above, not a second claim. */
.loop-line{
  font-family:var(--font-mono); font-size:0.82rem; color:var(--dim);
  letter-spacing:0.02em; margin-top:10px;
}
.loop-line .text-loop{ color:var(--acc-text); }

/* ==== THE LAUNCH OFFER BAND =========================================
   Homepage only. Took over from the EDGE_LOOP ribbon on 2026-08-01 —
   see the comment above the section in index.html for the honesty rules
   that govern its wording, which are the load-bearing part.

   It is the ONE section on the homepage that sits on a lit surface with
   a gold edge, so it reads as an interruption rather than as another
   band of argument. That only works while it is the only one: a second
   would cost this one its whole effect, exactly as with the gold-tinted
   bug sheet in the app. */
.promo-band{
  /* `isolation:isolate` gives the aurora a stacking context to sit behind
     the content in — it does NOT clip it, and those are different jobs.
     Without the clip, `.aurora`'s deliberate `inset:-20%` put this band's
     glow over the section above it. Every other aurora host on the site
     clips (.hero, .numpin); this one was the exception. */
  position:relative; isolation:isolate; overflow:clip;
  padding:clamp(48px,7vw,78px) 0;
  border-top:1px solid var(--acc-line);
  border-bottom:1px solid var(--acc-line);
  background:
    linear-gradient(180deg, rgba(215,255,62,0.055), transparent 60%),
    var(--canvas);
}
.promo-band .aurora{ opacity:0.18; }
.promo-inner{
  display:flex; align-items:center; justify-content:space-between;
  gap:clamp(24px,4vw,56px); flex-wrap:wrap;
}
.promo-copy{ flex:1 1 460px; min-width:0; }
.promo-copy h2{
  font-size:clamp(1.5rem,2.9vw,2.15rem); line-height:1.16;
  letter-spacing:-0.03em; margin:10px 0 12px;
}
.promo-copy p{ color:var(--dim); max-width:60ch; margin:0; }
.promo-copy p b{ color:var(--acc-text); font-weight:600; }
/* The struck-through old price. `line-through` on the real number rather
   than a separate "was £40" line: it is the same claim in less space, and
   it is the same treatment `.price-card .struck` gives it on the pricing
   page, so the two pages agree visually as well as numerically. */
.promo-was{
  text-decoration:line-through; text-decoration-thickness:1px;
  color:var(--faint);
}
.promo-act{
  display:flex; flex-direction:column; align-items:flex-start; gap:12px;
  flex:0 0 auto;
}
.promo-note{
  font-family:var(--font-mono); font-size:0.66rem; letter-spacing:.08em;
  text-transform:uppercase; color:var(--faint);
}
@media (max-width:700px){
  .promo-inner{ flex-direction:column; align-items:flex-start; }
  .promo-act{ width:100%; }
  .promo-act .glow-wrap{ width:100%; }
  .promo-act .btn{ width:100%; }
}

/* ==== THE DIRECT ANSWER + THE VISIBLE FAQ ===========================
   Both on find-businesses-without-a-website.html, added 2026-08-01, and
   both there for the same reason: a search engine (and an AI answer
   citing one) can only quote what is on the page. See the comments
   around the markup for the substance — the FAQ answers in particular
   MUST stay word-for-word identical to the FAQPage block in that page's
   <head>, which is the whole point of having added them.

   Nothing here is decorative. `.answer-box` is styled to read as the
   answer rather than as a pull-quote: left rule, normal body size, no
   italics. An answer set apart as a design flourish is one people skim
   past, and one a snippet is less likely to lift cleanly. */
.answer-box{
  max-width:74ch; margin:22px 0 0;
  padding:16px 20px; border-left:2px solid var(--acc-line);
  background:rgba(215,255,62,0.035); border-radius:0 var(--r) var(--r) 0;
  color:var(--dim); line-height:1.7;
}
.answer-box b{ color:var(--text); font-weight:600; }

.faq-list{ max-width:74ch; margin:0 auto; display:grid; gap:0; }
.faq-item{
  border-bottom:1px solid var(--line);
  padding:2px 0;
}
.faq-item:first-child{ border-top:1px solid var(--line); }
/* The whole row is the control, so the hit target is the width of the
   list rather than the width of the words. */
.faq-item > summary{
  cursor:pointer; list-style:none;
  padding:18px 34px 18px 2px; position:relative;
  font-size:1.02rem; color:var(--text);
  transition:color .18s ease;
}
.faq-item > summary::-webkit-details-marker{ display:none; }
.faq-item > summary:hover{ color:var(--acc-text); }
/* A rotating chevron drawn in CSS rather than the UA triangle, which is
   the one part of a <details> that cannot be styled consistently. */
.faq-item > summary::after{
  content:""; position:absolute; right:6px; top:50%;
  width:8px; height:8px; margin-top:-6px;
  border-right:1.5px solid var(--dim); border-bottom:1.5px solid var(--dim);
  transform:rotate(45deg); transform-origin:50% 50%;
  transition:transform .22s cubic-bezier(.3,1,.4,1), border-color .18s ease;
}
.faq-item[open] > summary::after{ transform:rotate(-135deg); margin-top:-2px; }
.faq-item > summary:hover::after{ border-color:var(--acc); }
.faq-item > summary:focus-visible{ outline:2px solid var(--acc); outline-offset:3px; }
.faq-item > p{
  margin:0 0 20px; padding-right:34px;
  color:var(--dim); line-height:1.75; max-width:66ch;
}
@media (prefers-reduced-motion:reduce){
  .faq-item > summary,
  .faq-item > summary::after{ transition:none; }
}

/* The launch stamp comes off the card whenever the card is not actually
   showing a discount — the yearly view, most obviously, where Solo
   Operator is £390/yr with nothing struck through. `display:none` and
   not opacity: a transparent badge still covers the corner of the card
   and still takes the pointer. Hiding the element takes its glow with
   it, because the glow is that element's own box-shadow.
   js/pricing-stamp.js owns when this class is on. */
.offer-stamp.off,
.offer-shock.off{ display:none; }

/* ==== GLASS ==========================================================
   Added 2026-08-02. A translucent-surface language for the app chrome.

   THE BLUR GOES ON A PSEUDO-ELEMENT, NEVER ON THE ELEMENT ITSELF, and
   on this page that is not a style preference — it is the difference
   between a working dock and a broken one. `backdrop-filter` makes an
   element a containing block for its `position:fixed` DESCENDANTS, the
   same way `transform`/`filter`/`perspective` do. This site has already
   lost the mobile nav to exactly that once (see BUG #1 by
   `header:not(.app-head)::before` above), and the dock below is
   `position:fixed`. Put the filter on `.app-shell`, `.sidebar` or any
   ancestor of a fixed child and the child starts resolving against that
   box instead of the viewport.

   So: `.glass` paints nothing itself. `::before` carries the tint and
   the blur at z-index -1; `::after` carries the specular top edge. Both
   inherit the radius, so a stadium's frost is stadium-shaped.

   WHY `isolation:isolate`. The two pseudo-elements sit at z-index -1,
   which without a local stacking context paints them behind the
   element's ANCESTOR background, not just behind its own content — on a
   panel inside a card, the frost disappeared. Isolating pins -1 to mean
   "behind this element's content" and nothing further back. */
:root{
  --glass-bg:rgba(20,24,30,0.55);
  --glass-bg-strong:rgba(14,17,21,0.74);
  --glass-line:rgba(244,245,247,0.10);
  --glass-hi:rgba(244,245,247,0.14);
  --glass-blur:18px;
  --glass-shadow:0 22px 55px -26px rgba(0,0,0,0.9);
  /* One curve for every glass motion, so a dock expanding and a circle
     growing under the cursor feel like the same material. Slight
     overshoot is what reads as "fluid" rather than "linear". */
  --fluid:cubic-bezier(.22,1,.36,1);
}
/* THE POSITION IS IN `:where()` ON PURPOSE, AND IT IS NOT COSMETIC.
   `::before` is `position:absolute`, so `.glass` has to guarantee a
   positioned ancestor — but writing `.glass{ position:relative }` flatly
   means a plain class selector (0,1,0) sitting LATE in this file, which
   silently outranks any earlier equal-specificity rule that set a
   different position. It did exactly that on first render: the settings
   popover is `.settings-menu{ position:absolute }`, declared ~3000 lines
   earlier, so adding `.glass` to it dropped the popover into normal flow,
   stacked it under the gear and stretched its wrapper from 52px to 210px.
   `:where()` contributes ZERO specificity, so any element that states its
   own position keeps it, and anything that doesn't still gets a
   containing block. Add `.glass` to a positioned element freely. */
:where(.glass){ position:relative; }
.glass{
  isolation:isolate;
  background:transparent;
  border:1px solid var(--glass-line);
  box-shadow:var(--glass-shadow);
}
.glass::before{
  content:""; position:absolute; inset:0; z-index:-1; border-radius:inherit;
  background:var(--glass-bg);
  backdrop-filter:blur(var(--glass-blur)) saturate(160%);
  -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(160%);
}
.glass::after{
  content:""; position:absolute; inset:0; z-index:-1; border-radius:inherit;
  pointer-events:none;
  background:linear-gradient(180deg, var(--glass-hi) 0%, transparent 46%);
  opacity:.55;
}
.glass-strong::before{ background:var(--glass-bg-strong); }

/* Safari and Firefox both ship backdrop-filter, but a browser without it
   would render `.glass` as a nearly-invisible outline over live content —
   text on text. The fallback is opaque, which loses the effect and keeps
   the legibility. That is the right way round. */
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .glass::before{ background:var(--app-raised, #1A1C1F); }
  .glass::after{ display:none; }
}

/* ==== GLASS DOCK + THE TWO FLOATING CIRCLES ==========================
   2026-08-02. The rail stops being an edge-to-edge column and becomes
   three things floating over the page: a stadium holding the nav, a
   circle top-left for settings, a stack bottom-left for help.

   THE COLUMN STILL EXISTS AND IS STILL RESERVED. `.sidebar` is
   `position:fixed`, so it is out of the grid entirely and `.app-main`
   is told `grid-column:2` (see the original dock block above — still
   load-bearing, and the reason the whole dashboard once rendered 76px
   wide). What changed is the column's WIDTH: it now has to cover the
   stadium plus the gap on both sides, or the content sits under it.

   NOTHING FIXED MAY BE PUT INSIDE `.sidebar`. It carries a `transform`
   for vertical centring, which makes it a containing block for fixed
   descendants — the same trap as backdrop-filter, from the other
   direction. That is precisely why the settings popover was moved out
   rather than left in place and restyled. */
@media (min-width:901px){
  /* THE GUTTER IS THE DOCK PLUS ONE GAP, NOT TWO. The first version
     reserved `rail + gap*2` and then let `.app-main` keep its own 40px
     padding on top, so content began 148px in while the dock ended at
     92px — 56px of dead space that read as a mistake rather than a
     margin. The column is now the dock's own footprint plus a single
     gap, and --app-gutter below trims the rest.

     A NAMED VALUE, because the header has to cancel it exactly (see the
     negative margin below). Two places computing `rail + gap * 1.5`
     independently is one edit away from a header that misses the edge by
     a few pixels — visible, and hard to attribute. */
  .app-body{ --dock-gap:14px; --dock-col:calc(var(--dock-rail) + var(--dock-gap) * 1.5); }
  .app-shell{ grid-template-columns:var(--dock-col) 1fr; }
  /* Was 18px, on top of the column, on top of .app-inner's own gutter —
     three stacked insets for one margin. The column reserves the dock and
     --app-gutter does the spacing; nothing is left for this to add. */
  .app-body .app-main{ padding-left:0; }

  /* ---- the header bar spans the viewport ---------------------------
     Asked for directly, 2026-08-03. It is a block inside the grid's
     second column, so a negative left margin of exactly that column's
     width pulls its left edge onto the viewport's — no `100vw`, which
     would include the scrollbar and overflow the page by ~15px, and no
     `position:fixed`, which would take the bar out of flow and mean
     hand-maintaining a padding-top on every app page to replace it.

     The padding puts the title back where the panels below it start, so
     the BACKGROUND is edge-to-edge while the CONTENT stays in the column.
     The dock and the two floating circles are `position:fixed` at a
     higher z-index, so they ride over the bar rather than being covered
     by it — which is the same relationship they already have with the
     rest of the page. */
  .app-body .app-head{
    margin-left:calc(-1 * var(--dock-col));
    padding-left:calc(var(--dock-col) + var(--app-gutter));
  }

  .app-body .sidebar{
    left:var(--dock-gap); top:50%; transform:translateY(-50%);
    height:auto; max-height:calc(100vh - var(--dock-gap) * 2);
    padding:14px 11px;
    background:transparent; border:1px solid var(--glass-line);
    /* ONE RADIUS, NEVER ANIMATED — and the stadium is emergent, not a
       second value. At the rail's 76px width a 38px corner consumes the
       entire top edge, so the cap is a true semicircle and the shape IS a
       stadium; at 252px the same 38px reads as an ordinary rounded
       rectangle. The pill therefore "becomes" a rectangle purely by
       getting wider.

       This replaced a 999px -> 26px transition. Animating the two
       together made the corner still near-stadium while the pill was
       half open, so it read as a stadium stretching and then squaring
       off — reported as exactly that. Splitting the timing per direction
       fixed the symptom; not having a second radius at all removes the
       cause. Keep it one value.

       calc() off --dock-rail rather than a typed 38px, so changing the
       rail width cannot quietly stop the collapsed state being a
       stadium. */
    border-radius:calc(var(--dock-rail) / 2);
    box-shadow:var(--glass-shadow);
    /* SHORTER, AND A GENTLER CURVE, BECAUSE THIS ANIMATES `width`.
       A width change is a LAYOUT change: every frame re-flows the pill
       and its five rows, and the backdrop-filter behind it has to
       re-blur the newly exposed area each time. `--fluid` has a long
       slow tail, which on a layout animation is exactly where frames
       start missing and it reads as stutter. A standard ease-out over
       .22s spends far less time in the expensive part. Don't reach for
       `--fluid` here just because the rest of the glass uses it - that
       curve is for transform/opacity, which the compositor handles. */
    transition:width .22s cubic-bezier(.4,0,.2,1),
               box-shadow .22s ease;
    /* A lighter blur ON THE DOCK SPECIFICALLY. 18px over an area that
       changes size every frame is the single most expensive thing in
       this component; 12px is visually near-identical while it moves. */
    --glass-blur:12px;
  }
  /* No radius override when open — see the single value above. */

  /* ==== THE ACCENT GLOW SURVIVES THE DOCK OPENING (2026-08-06) ========
     The page's own accent light is `body::before` —
     `radial-gradient(900px 420px at 22% -8%, rgba(215,255,62,0.11))` —
     so its brightest part sits about 317px in from the left edge, which
     is exactly the ground the dock expands across (76px -> 252px, plus
     its 14px offset). Opening it therefore put a tinted, 12px-blurred
     pane over the glow and the left side of the dashboard went flat and
     grey. Reported as the green glow disappearing on hover.

     THE ANSWER IS A GLOW ON THE DOCK, NOT A HOLE THROUGH IT. Letting the
     page light through — dropping the tint, or masking the backdrop —
     would put a moving gradient directly behind five nav labels and
     their numbers, and that is the thing being asked against: a glow,
     not a wash over the text. So the dock keeps its own opaque-enough
     glass and re-emits the light at its EDGE instead.

     Two parts, both outside the text:
       - an outer halo in the accent, wide and very soft, so the pill
         reads as sitting in the light rather than blocking it;
       - a 1px inset top highlight in the same hue, which is the edge
         actually catching it.
     Alpha is kept low deliberately. Measured against the nav labels
     (--text on the glass): the labels stay above 4.5:1, because none of
     this lands on the panel's interior — the halo is outside the border
     and the highlight is one pixel of it.

     It rides the existing `box-shadow .26s` transition, so the light
     grows as the pill widens instead of switching on at the end. */
  .app-shell.dock-open .sidebar{
    box-shadow:
      0 0 0 1px rgba(215,255,62,0.10),
      0 18px 60px -28px rgba(215,255,62,0.34),
      var(--glass-shadow);
  }

  /* The brand divider was a full-width rule against a flat column edge.
     Inside a floating pill it cut the glass in half. */
  /* ==== THE MARK NEVER MOVES (fixed 2026-08-06) ======================
     IT USED TO FLY 90px RIGHT AND SLIDE BACK, EVERY TIME THE DOCK CLOSED.

     The row was `justify-content:center` collapsed and `flex-start` open.
     `justify-content` is not animatable, so it flips the instant the
     class changes — but `width` IS animated, over .22s. On CLOSE those
     two disagree for the whole of that .22s: the moment `.dock-open`
     comes off, the mark re-centres inside a row that is still 252px
     wide, which puts it at x~124, and it then travels left to x~36 as
     the pill narrows underneath it. Measured: a 90px jump right followed
     by a 90px glide back, on a logo that is meant to be the one fixed
     point in the component.

     Centring was there because the collapsed wordmark keeps its width
     while invisible (`color:transparent`), so the row was centring
     LOGO-PLUS-INVISIBLE-WORD. Clipping `.side-brand` to 32px below
     already fixed that, which left the centring doing nothing but
     causing this.

     SO THE ROW IS flex-start IN BOTH STATES, and the padding is set so
     the mark lands on the same x as the nav icons below it: 13px matches
     `.side-nav.dock a`'s own 13px side padding, so mark and icons share
     one optical column at 38px from the viewport edge. That is a better
     resting place than the centre of the rail anyway — the eye reads the
     column, not the pill. Nothing here is animated, because nothing here
     should move: only the labels and their numbers fade in, and the
     wordmark beside the mark. */
  .app-body .side-brand-row{
    border-bottom:none; margin-bottom:6px; padding:2px 8px 10px 13px;
    justify-content:flex-start;
  }
  /* THE WORDMARK STILL TAKES ITS WIDTH WHEN IT IS INVISIBLE. The collapsed
     rail hides "Scoutline" with `color:transparent` (deliberately - the
     letters stay in the DOM for anything reading it), but transparent text
     is still laid out, so `.side-brand` stayed ~120px wide and centring
     the row centred LOGO-PLUS-INVISIBLE-WORD. The mark sat left of centre
     and looked misaligned against a symmetrical pill. Clipping the link to
     the mark's own 32px is what actually centres it; the margin that
     separates mark from word has to go with it, or it re-offsets by 10px. */
  .app-shell:not(.dock-open) .side-brand{
    width:32px; overflow:hidden; flex:none;
  }
  /* THE MARK'S OWN BOX MUST BE IDENTICAL IN BOTH STATES. This used to
     drop the 10px margin when collapsed, which is a second thing changing
     on the element the fix above exists to hold still. It is not needed:
     `.side-brand` is clipped to the mark's 32px, so the margin only ever
     spaces the (clipped, invisible) wordmark and never shifts the image.
     Leaving it constant means the collapse changes nothing about the mark
     at all — not its position, not its size, not its margins. */
  /* Help lived at the bottom of the rail behind `margin-top:auto`, which
     in an auto-height pill would push nothing anywhere. It is a floating
     stack now. */
  .app-body .sidebar .side-help{ display:none; }
  .app-body .sidebar .side-label{ display:none; }
}

/* ---- the floating circles ------------------------------------------
   Both are `position:fixed` siblings of `.sidebar` inside `.app-shell`,
   NOT children of it — see the containing-block note above. */
.dock-fab-wrap{ position:fixed; z-index:31; }
/* 32, one above .help-fab, so the OPEN SETTINGS MENU paints over the
   other two circles instead of under them.
   Found 2026-08-04 while fixing the popover's mobile position: on a
   phone all three circles stack in the same bottom-right corner, and
   the menu opens across the two below it. Both wrappers were z-index:31,
   and `z-index:31` makes .settings-fab a stacking context — so the
   menu's own `z-index:5` is confined INSIDE it and can never rise above
   a sibling at 31. Raising the wrapper is what actually works.
   Clicks were never blocked (the buttons' centres tested clear at
   320/375/414); this is a purely visual fix, and a permanent bump
   rather than a :has() rule because the two circles never overlap each
   other in either layout, so there is nothing to toggle. */
.settings-fab{ left:16px; top:16px; z-index:32; }
.help-fab{ left:16px; bottom:16px; display:flex; flex-direction:column; gap:10px; }

.fab{
  width:52px; height:52px; padding:0; border-radius:999px;
  display:grid; place-items:center;
  color:var(--app-dim); cursor:pointer;
  transition:transform .34s var(--fluid), color .2s ease,
             border-color .2s ease, box-shadow .3s ease;
}
.fab:hover,
.fab[aria-expanded="true"]{
  color:var(--app-text);
  transform:scale(1.08);
  border-color:rgba(244,245,247,0.2);
}
.fab:active{ transform:scale(.96); }
.fab svg{ flex:none; }

/* The label is a second pill that grows out of the circle's right edge.
   Width, not display, so it can be transitioned; `white-space:nowrap`
   so it never wraps mid-grow. */
.fab-label{
  position:absolute; left:60px; top:50%; transform:translateY(-50%) translateX(-6px);
  white-space:nowrap; pointer-events:none;
  padding:8px 14px; border-radius:999px;
  font-size:0.82rem; color:var(--app-text);
  opacity:0;
  transition:opacity .2s ease, transform .28s var(--fluid);
}
.fab-slot{ position:relative; display:block; }
.fab-slot:hover .fab-label,
.fab-slot:focus-within .fab-label{ opacity:1; transform:translateY(-50%) translateX(0); }

/* The bug circle keeps the one gold tint it had as a sidebar button —
   still the only accent-tinted control in the app chrome. */
.fab.bug{ color:var(--acc-text); border-color:var(--acc-line); }
.fab.bug::before{ background:rgba(215,255,62,0.10); }
.fab.bug:hover{ color:var(--acc-text); border-color:var(--acc-glow); }
.fab.bug[hidden]{ display:none; }

/* The popover now hangs off the RIGHT of a circle pinned to the left
   edge, not below-left of a gear in a column. Origin follows, or it
   scales out of the wrong corner. */
.settings-fab .settings-menu{
  top:0; left:calc(100% + 12px); right:auto;
  transform:translateX(-8px) scale(.96); transform-origin:top left;
}
.settings-fab .settings-menu.open{ transform:none; }

/* ---- below the dock breakpoint --------------------------------------
   `.sidebar` is the slide-out drawer here and the circles would sit on
   top of real content at the left edge, where the drawer opens from and
   where `.app-topbar`'s hamburger already is. They stack bottom-RIGHT
   instead: one column, clear of the topbar, clear of the drawer. The
   labels are dropped — at this width the pill would reach the far edge. */
@media (max-width:900px){
  .settings-fab, .help-fab{
    left:auto; right:14px; top:auto; bottom:14px;
    flex-direction:column-reverse; gap:10px;
  }
  .settings-fab{ bottom:14px; }
  .help-fab{ bottom:76px; }
  .fab{ width:48px; height:48px; }
  .fab-label{ display:none; }

  /* ⚠️ THE POPOVER HAS TO MOVE WITH THE CIRCLE. Fixed 2026-08-04, and
     it is the bug Ehan reported as "the settings button not working on
     the interface on mobile".

     The button was working perfectly. The rule above moved the circle
     from the top-LEFT edge to the bottom-RIGHT one, but the desktop
     rule positioning the menu — `top:0; left:calc(100% + 12px)` — was
     never retargeted. So the menu was still being told to open 12px to
     the RIGHT of a circle already 14px from the right edge, and
     downward from a circle already 14px off the bottom.

     MEASURED before the fix, on all five app pages at 320/375/414/768/
     900: the menu opened 192px past the right edge and 216px past the
     bottom. Nothing was broken and nothing errored — it simply rendered
     where nobody could see it, which is exactly why it presented as
     "the button does nothing".

     Clean at 901px and above, because that is where this block stops
     applying. Opening up-and-left, with the origin in the corner it is
     actually attached to, is what the bottom-right pinning requires.

     THE GENERAL LESSON, and it is now the third bug of this shape in
     this stylesheet: WHEN A MEDIA QUERY MOVES AN ANCHOR, GREP FOR
     EVERY RULE POSITIONED AGAINST IT. */
  .settings-fab .settings-menu{
    top:auto; bottom:calc(100% + 12px);
    left:auto; right:0;
    transform:translateY(8px) scale(.96); transform-origin:bottom right;
  }
  .settings-fab .settings-menu.open{ transform:none; }
}

@media (prefers-reduced-motion:reduce){
  .app-body .sidebar,
  .fab, .fab-label{ transition:none; }
  .fab:hover, .fab:active, .fab[aria-expanded="true"]{ transform:none; }
}

/* ==== .btn-inner — the nested magnet's label box ======================
   2026-08-02. js/flourish.js wraps every .btn-acc's contents in this span
   so the LABEL can be transformed independently of the button, which is
   what PRODUCTION/cool effects/magnetic nested.jsx actually animates. A
   bare text node has no box and cannot be moved.

   IT HAS TO REBUILD THE BUTTON'S OWN FLEX LAYOUT. `.btn` is
   `display:inline-flex; align-items:center; gap:9px` over what used to be
   two children (the label text and `.arr`). Wrapping them makes the
   button a flex container with ONE child, so without these three lines
   the gap between word and arrow disappears and they collide.
   `gap:inherit` rather than a repeated 9px, so .btn-lg or any future
   variant that changes the gap is followed automatically instead of
   silently disagreeing. */
.btn-inner{ display:inline-flex; align-items:center; gap:inherit; }
/* GATED ON `.magnet`, matching the `.btn.magnet, .magnetic.magnet` rule
   above rather than styling `.btn-inner` flatly. Two reasons, and the
   second is the one this session kept re-learning: a blanket transform
   would clobber any element that carries its own, AND `transform` makes
   an element a containing block for fixed descendants. An unconditional
   one here would be a third of those created for nothing. Only a wired
   magnet gets it. */
.btn-inner.magnet{
  transform:translate3d(var(--pull-x,0px), var(--pull-y,0px), 0);
}
@media (prefers-reduced-motion:reduce){
  .btn-inner.magnet{ transform:none; }
}

/* ==== THE BUILD STEPPER + THE FLOWING BAR ============================
   2026-08-02. The full-site build used to show a percentage, a bar and a
   list that grew as things finished. The list only ever said where you
   had BEEN; the stepper says where you are going, which is the question
   somebody staring at a four-minute wait is actually asking.

   EVERY STEP HERE IS A REAL CALL. This build is an N+1 sequence the
   client drives — design, stylesheet, one request per page, then packing
   — so the whole route is known before the first request. Nothing is
   invented to pad the line. That matters more on this product than most:
   lib/ai-failure.js exists to tell somebody honestly whose fault a
   failure was, and a fake step ticking over while the real call fails
   would undo it. */
.fs-steps{
  list-style:none; margin:18px 0 0; padding:0;
  display:flex; align-items:flex-start; gap:0;
  overflow-x:auto; scrollbar-width:none;
}
.fs-steps::-webkit-scrollbar{ display:none; }
.fs-step{
  position:relative; flex:1 1 0; min-width:74px;
  display:flex; flex-direction:column; align-items:center; gap:7px;
  text-align:center;
}
/* THE CONNECTING LINE IS DRAWN BY THE STEPS THEMSELVES, as a bar behind
   each dot reaching back to the previous one. A single line element
   underneath would have to know the first and last dot's centres to stop
   short of them, and would re-measure on every resize. `:first-child`
   simply has no incoming line. */
.fs-step::before{
  content:""; position:absolute; top:12px; right:50%; width:100%; height:2px;
  background:var(--app-line); z-index:0;
}
.fs-step:first-child::before{ display:none; }
.fs-step.done::before,
.fs-step.active::before{ background:var(--acc); }
.fs-step-dot{
  position:relative; z-index:1;
  width:26px; height:26px; border-radius:999px;
  display:grid; place-items:center;
  font-family:var(--font-mono); font-size:0.64rem;
  background:var(--app-surface); border:1px solid var(--app-line);
  color:var(--app-faint);
  transition:background .25s ease, border-color .25s ease,
             color .25s ease, transform .25s cubic-bezier(.4,0,.2,1);
}
.fs-step-name{
  font-size:0.62rem; letter-spacing:.02em; color:var(--app-faint);
  max-width:88px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  transition:color .25s ease;
}
.fs-step.done .fs-step-dot{
  background:var(--acc); border-color:var(--acc); color:var(--acc-ink);
}
.fs-step.done .fs-step-name{ color:var(--app-dim); }
.fs-step.active .fs-step-dot{
  border-color:var(--acc); color:var(--acc-text);
  transform:scale(1.14);
  /* The pulse is the only thing on the line that moves, so it reads as
     "this one, now" without the other dots competing. */
  animation:fs-step-pulse 1.8s ease-in-out infinite;
}
.fs-step.active .fs-step-name{ color:var(--acc-text); }
@keyframes fs-step-pulse{
  0%,100%{ box-shadow:0 0 0 0 var(--acc-soft); }
  50%    { box-shadow:0 0 0 7px transparent; }
}

/* THE FILL FLOWS, AND IT STILL NEVER LIES. The width is set from real
   completed steps and nothing here touches it — this is a sheen moving
   THROUGH whatever width the fill already has, so a bar at 40% still
   reads 40%. Same discipline as .fs-prog.working's sweep above, which
   deliberately animates the bar's own overlay and not the fill. */
.fs-prog-fill{
  background-image:linear-gradient(90deg,
    transparent 0%, rgba(255,255,255,0.28) 45%, transparent 90%);
  background-size:220% 100%;
  animation:fs-fill-flow 2.2s linear infinite;
}
@keyframes fs-fill-flow{ from{ background-position:120% 0; } to{ background-position:-120% 0; } }
.fs-prog.complete .fs-prog-fill,
.fs-prog.failed .fs-prog-fill{ animation:none; background-image:none; }

/* ---- the don't-close-this-tab notice ---- */
.fs-prog-warn{
  display:flex; align-items:flex-start; gap:9px;
  margin:16px 0 0; padding:11px 13px; border-radius:9px;
  background:var(--acc-soft); border:1px solid var(--acc-line);
  color:var(--app-text); font-size:0.78rem; line-height:1.55;
}
.fs-prog-warn-i{
  flex:none; width:17px; height:17px; margin-top:1px; border-radius:999px;
  display:grid; place-items:center;
  background:var(--acc); color:var(--acc-ink);
  font-family:var(--font-mono); font-size:0.6rem;
}
/* Gone once there is nothing left to lose. */
.fs-prog.complete .fs-prog-warn{ display:none; }

@media (prefers-reduced-motion:reduce){
  .fs-step-dot{ transition:none; }
  .fs-step.active .fs-step-dot{ animation:none; transform:none; }
  .fs-prog-fill{ animation:none; background-image:none; }
}
@media (max-width:640px){
  .fs-step{ min-width:60px; }
  .fs-step-name{ display:none; }   /* numbers still carry the sequence */
}

/* ======================================================================
   ADMIN DASHBOARD — admin.html (2026-08-03)
   ======================================================================
   Visual language taken from the Pulmify reference in
   inspiration/interface inspo, INVERTED for this site's dark canvas: the
   reference is dark ink on cream with one deep-green accent, this is the
   same grammar on --canvas with --acc.

   What was carried over, and what deliberately was not:
     KEPT  dashed 1px card borders — the signature, and genuinely useful
           here because it separates a wall of small panels without adding
           another luminance step to the four-rung ladder.
     KEPT  monospace uppercase micro-labels with a rule under them.
     KEPT  enormous numerals as the primary object of a card.
     KEPT  dot-matrix columns and heat grids over solid bars (js/charts.js
           explains why the dot is a unit and when it stops being one).
     DROPPED the reference's one dark feature-card-among-light-ones. On a
           dark canvas the equivalent would be a LIGHT card, which at this
           size reads as an error state rather than as emphasis. Emphasis
           is carried by the accent rule on `.acard-hot` instead.

   THE DASHED BORDER IS THE ONLY PLACE ON THIS SITE THAT USES ONE. That is
   what makes the admin area feel like a different room while still being
   built from the same tokens — no new colours, no new radii, no new type. */

.admin-body{ background:var(--canvas); }

/* ---- the admin dock ---------------------------------------------------
   ITS CLASSES ARE ITS OWN, and that is the fix for a real bug rather than
   a preference. The first build reused `.side-t` from the sidebar; inside
   `@media (min-width:901px)` that class is `opacity:0` until
   `.app-shell.dock-open` restores it, and this page has no `.app-shell`.
   So every label in the dock was INVISIBLE on any real desktop — and
   visible below 901px, which is the width a preview pane tends to be. It
   looked correct in testing while being broken in use.

   The lesson generalises: never borrow a class from a component that
   hides itself by default. `tdock-*` shares nothing. */
.tdock-wrap{
  position:sticky; top:0; z-index:48;
  padding:10px 14px 0;
}
.tdock{
  display:flex; align-items:center; gap:12px;
  padding:8px 10px 8px 14px;
  border-radius:999px;
  max-width:1400px; margin:0 auto;
}
.tdock-brand{
  display:flex; align-items:center; gap:0;
  font-family:var(--font-display); font-weight:600;
  font-size:14.5px; letter-spacing:-.01em; color:var(--text);
  text-decoration:none; white-space:nowrap; flex:0 0 auto;
}
/* gap:0 above and the margin here — `.brand` is a flex container and the
   wordmark is TWO items, so any gap lands inside the word and renders
   "Scout line". Same rule as `.brand` / `.side-brand`. */
.tdock-brand img{ margin-right:8px; }
.tdock-brand .dot{ color:var(--acc); }
.tdock-badge{
  font-family:var(--font-mono); font-size:9.5px; letter-spacing:.12em;
  text-transform:uppercase; color:var(--acc);
  border:1px solid var(--acc-line); border-radius:999px;
  padding:3px 8px; flex:0 0 auto;
}

.tdock-nav{
  display:flex; align-items:center; gap:2px;
  flex:1 1 auto; min-width:0; flex-wrap:wrap;
}
.tdock-a{
  appearance:none; border:0; background:none; cursor:pointer;
  display:flex; align-items:center; gap:8px;
  padding:8px 13px; border-radius:999px;
  color:var(--dim); white-space:nowrap;
  font-family:var(--font-body); font-size:13px; line-height:1;
  transition:color .18s var(--fluid), background .18s var(--fluid),
             transform .18s var(--fluid);
}
/* NO opacity trick anywhere near this. The label is always shown. */
.tdock-t{ display:inline; opacity:1; }
.tdock-i{ display:flex; align-items:center; color:inherit; }
.tdock-a:hover{ color:var(--text); background:rgba(244,245,247,0.05); }
.tdock-a:focus-visible{ outline:2px solid var(--acc); outline-offset:2px; }
.tdock-a.on{ color:var(--acc-ink); background:var(--acc); font-weight:600; }
.tdock-a.on .tdock-i{ color:var(--acc-ink); }

/* Horizontal, so the magnify SCALES rather than translates — neighbours
   are immediately left and right and a translate would collide. */
@media (hover:hover) and (pointer:fine){
  .tdock-a{ transform:scale(calc(1 + (var(--mag, 0) * 0.08))); }
}
@media (prefers-reduced-motion: reduce){
  .tdock-a{ transform:none !important; transition:none; }
}

.tdock-end{ display:flex; align-items:center; gap:8px; flex:0 0 auto; }
.tdock-exit{ display:inline-flex; align-items:center; gap:6px; }
/* Prints how long ago the last successful poll was, not the word "live".
   A stamp that says live is still saying it an hour into an outage. */
.live-stamp{
  font-family:var(--font-mono); font-size:10.5px; letter-spacing:.06em;
  text-transform:uppercase; color:var(--faint); white-space:nowrap;
}

/* ---- the briefing ------------------------------------------------------ */
.brief-hero{
  border:1px dashed var(--acc-line); border-radius:var(--r-lg);
  background:linear-gradient(180deg, var(--acc-soft), transparent 70%), var(--surface);
  padding:26px 26px 28px; margin:0 0 18px;
}
.brief-eyebrow{
  font-family:var(--font-mono); font-size:10px; letter-spacing:.14em;
  text-transform:uppercase; color:var(--acc); margin:0 0 10px;
}
.brief-hero h2{ font-size:clamp(22px,3.2vw,32px); line-height:1.15; margin:0 0 14px; }
.brief-summary{ color:var(--dim); font-size:14.5px; line-height:1.65; max-width:78ch; margin:0; }
.brief-warning{
  color:var(--warn); font-size:13.5px; line-height:1.6; max-width:78ch;
  margin:16px 0 0; padding-left:14px; border-left:2px solid var(--warn);
}

/* padding:0 IS LOAD-BEARING. These are <section> elements, and the
   marketing stylesheet gives a bare `section` ~110px of vertical padding
   for its full-width bands. That leaked in here and opened a 160px hole
   between the hero and the first block — invisible in the markup, obvious
   the moment the gap was measured rather than eyeballed. Any new
   <section> on this page needs the same reset. */
.brief-block{ margin:0 0 26px; padding:0; }
.brief-h{
  display:flex; align-items:baseline; justify-content:space-between;
  gap:12px; flex-wrap:wrap;
  padding-bottom:10px; margin-bottom:14px;
  border-bottom:1px solid var(--line-strong);
}
.brief-h h3{ font-size:17px; margin:0; }
.brief-count, .brief-note{
  font-family:var(--font-mono); font-size:10px; letter-spacing:.1em;
  text-transform:uppercase; color:var(--faint);
}

/* ==== THE LAUNCH PANEL (admin.html, added 2026-08-06) =================
   The nine-day campaign's own block. Reuses the dashed-border card
   language of .acard rather than inventing a second one.

   NOTE the `padding:0` reset above applies to #launch-block too — it is
   a <section> on this page and would otherwise inherit the marketing
   stylesheet's ~110px band padding. It gets it from .brief-block. */
.launch-grid{
  display:grid; grid-template-columns:minmax(230px,1fr) 2.4fr;
  gap:12px; align-items:stretch;
}
.launch-gauge, .launch-kpi, .launch-chart{
  border:1px dashed var(--line-strong); border-radius:var(--r-lg);
  background:var(--surface); padding:14px 16px;
}
.launch-kpis{
  display:grid; grid-template-columns:repeat(auto-fit,minmax(104px,1fr)); gap:12px;
}
.launch-kpi{ display:flex; flex-direction:column; }
.launch-lab{
  font-family:var(--font-mono); font-size:9.5px; letter-spacing:.1em;
  text-transform:uppercase; color:var(--faint); margin:0 0 8px;
}
.launch-fig{
  font-family:var(--font-display); font-weight:600;
  font-size:clamp(24px,3.2vw,34px); line-height:1;
  letter-spacing:-.03em; color:var(--text);
  margin:auto 0 0; font-variant-numeric:tabular-nums;
}
/* Not a second figure — the cap belongs to the number beside it, so it
   is sized down and dimmed rather than given its own slot. */
.launch-of{
  font-family:var(--font-mono); font-size:12px; letter-spacing:.04em;
  color:var(--faint); font-weight:400;
}
.launch-sub{
  font-family:var(--font-mono); font-size:10px; letter-spacing:.03em;
  color:var(--dim); margin:10px 0 0; line-height:1.5;
}
.launch-bar{
  height:6px; border-radius:999px; background:var(--raised-2);
  overflow:hidden; margin-top:12px;
}
/* Fills with what has been USED, so a full bar reads as "out of room" —
   the direction that matches the alarm. Gold, then warn, then bad. */
.launch-bar i{ display:block; height:100%; background:var(--acc); transition:width .4s var(--fluid); }
.launch-bar i.warm{ background:var(--warn); }
.launch-bar i.hot{ background:var(--bad); }
.launch-gauge.warm{ border-color:var(--warn); }
.launch-gauge.hot{ border-color:var(--bad); }
.launch-chart{ margin-top:12px; }
/* THE DOT CHART MUST BE CONSTRAINED OR IT STRANDS ITSELF IN THE MIDDLE.
   js/charts.js emits a viewBox sized to its data (~220x96 for 30 days at
   maxDots 10) with `xMidYMid meet`, so in a full-width card it scales to
   fit the HEIGHT and centres, leaving several hundred pixels of dead space
   on both sides — measured at 1101px host, 391px of actual chart. Capping
   the host at the width the aspect ratio actually wants makes it fill,
   and left-aligning keeps it under its own caption rather than floating.
   Found by rendering it; it is invisible in the markup. */
.launch-chart > div{ max-width:430px; }

@media (prefers-reduced-motion:reduce){
  .launch-bar i{ transition:none; }
}
@media (max-width:820px){
  .launch-grid{ grid-template-columns:1fr; }
}

.alert-list{ display:flex; flex-direction:column; gap:8px; }
.alert{
  border:1px dashed var(--line-strong); border-radius:var(--r-lg);
  background:var(--surface); padding:14px 16px;
}
.alert-h{ display:flex; align-items:center; gap:9px; flex-wrap:wrap; }
.alert-h h4{ font-size:14.5px; margin:0; flex:1 1 auto; }
.alert-dot{ width:7px; height:7px; border-radius:50%; flex:0 0 auto; background:var(--dim); }
.alert-lv{
  font-family:var(--font-mono); font-size:9px; letter-spacing:.1em;
  text-transform:uppercase; color:var(--faint);
  border:1px solid var(--line-strong); border-radius:999px; padding:2px 7px;
}
.alert-body{ color:var(--dim); font-size:13px; line-height:1.6; margin:8px 0 0; }
/* Severity in the dot and the border, never in the body text colour —
   a whole paragraph in red reads as broken rather than as urgent. */
.alert.high{ border-color:rgba(240,138,120,0.45); }
.alert.high .alert-dot{ background:var(--bad); }
.alert.high .alert-lv{ color:var(--bad); border-color:rgba(240,138,120,0.45); }
.alert.med .alert-dot{ background:var(--warn); }
.alert.med .alert-lv{ color:var(--warn); }
.alert.low .alert-dot{ background:var(--dim); }

.play-list{
  display:grid; gap:12px;
  grid-template-columns:repeat(auto-fit, minmax(330px, 1fr));
}
.play{
  border:1px dashed var(--line-strong); border-radius:var(--r-lg);
  background:var(--surface); padding:18px 20px 20px;
  display:flex; flex-direction:column; min-width:0;
}
.play-h{ display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; margin-bottom:10px; }
.play-h h4{ font-size:15.5px; margin:0; flex:1 1 auto; min-width:0; }
.play-n{
  font-family:var(--font-mono); font-size:11px; color:var(--acc);
  letter-spacing:.06em; flex:0 0 auto;
}
.play-tag{
  font-family:var(--font-mono); font-size:9px; letter-spacing:.1em;
  text-transform:uppercase; color:var(--acc-ink); background:var(--acc);
  border-radius:999px; padding:3px 8px; flex:0 0 auto;
}
.play-body{ color:var(--dim); font-size:13.2px; line-height:1.65; margin:0; }
.play-act, .play-why{ margin-top:14px; padding-top:12px; border-top:1px solid var(--line); }
.play-act p, .play-why p{ margin:5px 0 0; font-size:13px; line-height:1.6; }
.play-act p{ color:var(--text); }
.play-why p{ color:var(--faint); }
.play-lab{
  font-family:var(--font-mono); font-size:9px; letter-spacing:.12em;
  text-transform:uppercase; color:var(--acc);
}
.play-why .play-lab{ color:var(--faint); }
.play-list.plain .play-h h4{ font-size:15px; }

.admin-main{
  max-width:1360px; margin:0 auto; padding:26px 18px 90px;
}
.admin-head{ margin:14px 0 22px; }
.admin-head h1{ font-size:clamp(28px,4vw,42px); line-height:1.1; margin:.15em 0 .3em; }
.admin-sub{ color:var(--dim); font-size:14px; max-width:62ch; margin:0; }

/* ---- the sub-tab row --------------------------------------------------
   The reference's underlined tab strip, in its own dashed container. */
.atabs{
  display:flex; gap:4px; flex-wrap:wrap;
  border:1px dashed var(--line-strong); border-radius:var(--r-lg);
  padding:6px; margin:0 0 20px;
}
.atab{
  appearance:none; background:none; border:0; cursor:pointer;
  font-family:var(--font-mono); font-size:11px; letter-spacing:.1em;
  text-transform:uppercase; color:var(--dim);
  padding:9px 16px; border-radius:calc(var(--r-lg) - 8px);
  transition:color .18s var(--fluid), background .18s var(--fluid);
}
.atab:hover{ color:var(--text); }
.atab.on{ color:var(--acc-ink); background:var(--acc); font-weight:600; }
.atab:focus-visible{ outline:2px solid var(--acc); outline-offset:2px; }

/* ---- the card grid ----------------------------------------------------
   auto-fit rather than a fixed column count, so the same markup is a
   six-across wall on a monitor and a single column on a phone with no
   breakpoint list to maintain. */
.acard-grid{
  display:grid; gap:12px; margin:0 0 12px;
  grid-template-columns:repeat(auto-fit, minmax(190px, 1fr));
}
.acard-grid.wide{
  grid-template-columns:repeat(auto-fit, minmax(300px, 1fr));
}

.acard{
  border:1px dashed var(--line-strong);
  border-radius:var(--r-lg);
  background:var(--surface);
  padding:14px 16px 16px;
  min-width:0;                 /* grid items default to min-content — see
                                  the .app-main note; a wide table or a
                                  long unbroken figure would otherwise
                                  push the whole row out */
  display:flex; flex-direction:column;
}
/* min-height, and the meta truncates rather than wraps. A two-line meta
   pushed its card's rule down while its neighbours' stayed put, so the big
   figures in one row sat at three different heights — the one thing a wall
   of KPI cards has to get right. The full text stays in `title`. */
.acard-h{
  display:flex; align-items:baseline; justify-content:space-between;
  gap:10px; padding-bottom:8px; margin-bottom:12px;
  min-height:30px;
  border-bottom:1px solid var(--line);
}
.acard-t{
  font-family:var(--font-mono); font-size:10.5px; letter-spacing:.11em;
  text-transform:uppercase; color:var(--text);
}
.acard-m{
  font-family:var(--font-mono); font-size:9.5px; letter-spacing:.09em;
  text-transform:uppercase; color:var(--faint); text-align:right;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  flex:0 1 auto; min-width:0;
}
.acard-t{ flex:1 1 auto; min-width:0; }
.acard-sub{
  font-size:12px; line-height:1.5; color:var(--dim); margin:8px 0 0;
}

/* The big numeral. Tabular figures so a number changing from 8 to 11 does
   not shuffle everything beside it. */
.afig{
  font-family:var(--font-display); font-weight:600;
  font-size:clamp(30px,4.6vw,46px); line-height:1;
  letter-spacing:-.03em; color:var(--text);
  margin:auto 0 0; font-variant-numeric:tabular-nums;
}

/* Flashes ONLY when the value actually changed (js/admin.js decides).
   A figure that re-animates on every poll teaches you to ignore it. */
@keyframes afig-tick{
  0%{ color:var(--acc); transform:translateY(-2px); }
  100%{ color:var(--text); transform:none; }
}
.afig.tick{ animation:afig-tick .7s var(--fluid); }
@media (prefers-reduced-motion: reduce){
  .afig.tick{ animation:none; }
}

.acard-hot{ border-color:var(--acc-line); }
.acard-hot .afig{ color:var(--acc); }
.acard-in .afig{ color:var(--ok); }
.acard-out .afig{ color:var(--text); }
.acard-net .afig.neg{ color:var(--bad); }
/* An estimate is marked in the border, not only in prose — the prose is
   the thing people skip. */
.acard-est{ border-style:dotted; }
.acard-est .acard-m{ color:var(--warn); }
.acard-err{ border-color:rgba(240,138,120,0.4); }
.acard-note{ border-style:solid; border-color:var(--line); }
/* Quiet until it isn't. A permanently-lit warning is one nobody sees when
   it finally means something. */
.acard-watch.warn{ border-color:var(--warn); }
.acard-watch.warn .afig{ color:var(--warn); }

.stripe-mode{
  font-family:var(--font-mono); font-size:11px; letter-spacing:.08em;
  text-transform:uppercase; color:var(--warn);
  border:1px dashed rgba(255,194,75,0.4); border-radius:var(--r);
  padding:8px 12px; margin:0 0 12px;
}

/* ---- key/value lists --------------------------------------------------- */
.kv{ display:flex; flex-direction:column; gap:0; }
.kv-row{
  display:flex; align-items:baseline; justify-content:space-between;
  gap:12px; padding:7px 0; border-bottom:1px solid var(--line);
}
.kv-row:last-child{ border-bottom:0; }
.kv-k{ font-size:12.5px; color:var(--dim); text-transform:capitalize; }
.kv-v{
  font-family:var(--font-mono); font-size:13px; color:var(--text);
  font-variant-numeric:tabular-nums;
}
.kv-v.muted{ color:var(--faint); text-decoration:line-through; }

/* ---- charts (js/charts.js) --------------------------------------------
   The SVGs inherit currentColor, so everything here is a colour and a
   size; no chart config lives in JavaScript. */
.chart-host{ margin-top:2px; }
.chart{ display:block; width:100%; overflow:visible; }
.chart-empty{
  font-family:var(--font-mono); font-size:11px; letter-spacing:.05em;
  color:var(--faint); margin:10px 0; text-transform:uppercase;
}
.chart-scale{
  font-family:var(--font-mono); font-size:9.5px; letter-spacing:.07em;
  text-transform:uppercase; color:var(--faint); margin:8px 0 0;
}

/* height:auto — the viewBox carries the aspect ratio, so a fixed box here
   would letterbox the grid and shrink the dots to a smudge. That is exactly
   what it did on the first render. */
.chart-dots{ height:auto; max-height:170px; }
.chart-dots .dot{ fill:var(--acc); opacity:.85; }
.chart-dots .dot.hot{ fill:var(--acc); opacity:1; }
/* The empty slots. Faint enough to read as headroom rather than as data —
   see the note in js/charts.js on why the ceiling has to be visible. */
.chart-dots .dot.track{ fill:var(--text); opacity:.07; animation:none; }
@media (prefers-reduced-motion: no-preference){
  .chart-dots .dot{ animation:dot-in .32s var(--fluid) both; }
}
@keyframes dot-in{ from{ opacity:0; transform:translateY(3px); } }

.chart-heat .heat{ fill:var(--acc); }
.chart-heat .heat.s0{ fill:var(--raised-2); }
.chart-heat .heat.s1{ opacity:.28; }
.chart-heat .heat.s2{ opacity:.5; }
.chart-heat .heat.s3{ opacity:.74; }
.chart-heat .heat.s4{ opacity:1; }

.chart-seg{ height:12px; }
.chart-seg .seg{ fill:var(--raised-2); }
.chart-seg .seg.on{ fill:var(--acc); }
.chart-seg .seg.part{ fill:var(--acc); opacity:.4; }

.chart-spark{ height:56px; }
.spark-fill{ fill:var(--acc-soft); stroke:none; }
.spark-line{ fill:none; stroke:var(--acc); stroke-width:1.6; stroke-linejoin:round; }
.spark-dot{ fill:var(--acc); }

/* Four steps, ordered by how much of the bill each usually is. Not the
   spectrum tokens: those are used for unrelated marketing accents and
   reusing them here would imply a relationship that isn't there. */
.chart-split{ height:14px; }
.split.s0{ fill:var(--acc); }
.split.s1{ fill:var(--acc); opacity:.6; }
.split.s2{ fill:var(--dim); }
.split.s3{ fill:var(--raised-2); }
.chart-key{
  list-style:none; margin:10px 0 0; padding:0;
  display:flex; flex-wrap:wrap; gap:6px 14px;
}
.chart-key li{
  display:flex; align-items:center; gap:6px;
  font-family:var(--font-mono); font-size:10.5px; color:var(--dim);
  text-transform:uppercase; letter-spacing:.05em;
}
.key-sw{ width:9px; height:9px; border-radius:2px; flex:0 0 auto; }
.key-sw.s0{ background:var(--acc); }
.key-sw.s1{ background:var(--acc); opacity:.6; }
.key-sw.s2{ background:var(--dim); }
.key-sw.s3{ background:var(--raised-2); }

.chart-radar{ height:180px; }
.radar-ring{ fill:none; stroke:var(--line-strong); stroke-width:1; }
.radar-area{ fill:var(--acc-soft); stroke:var(--acc); stroke-width:1.5; }
.radar-label{
  font-family:var(--font-mono); font-size:8px; fill:var(--faint);
  text-transform:uppercase; letter-spacing:.06em;
}

/* ---- the admin page reuses the app's panels for tab 3 ----------------- */
.admin-main .panel{ margin-bottom:14px; }

@media (max-width:900px){
  .tdock{ border-radius:var(--r-lg); padding:9px 11px; flex-wrap:wrap; }
  .tdock-brand{ order:1; }
  .tdock-badge{ order:2; }
  .tdock-end{ order:3; margin-left:auto; }
  /* Its own row, full width, so five labelled items wrap rather than
     being squeezed to icons with no text. */
  .tdock-nav{ order:4; width:100%; padding-top:5px; }
  .admin-main{ padding:18px 12px 70px; }
  .afig{ font-size:32px; }
  .brief-hero{ padding:20px 18px 22px; }
  .play-list{ grid-template-columns:1fr; }
}

/* ====================================================================
   FIELD RINGS, TILE GLOW AND DELIBERATE OFFSETS        added 2026-08-04
   ====================================================================
   The rest of TASK 8. Everything here is decorative and every piece of
   it sits behind content, catches no pointer, and collapses on small
   screens or under reduced motion.

   ONE ELEMENT, THREE RINGS. A single circle with two spread-only
   box-shadows gives concentric rings for the cost of one pseudo-element,
   which is why these are so cheap to scatter around. Spread is in `vw`
   so the rings scale with the viewport instead of crowding the content
   column on a laptop.

   `overflow:clip`, never `overflow:hidden`, on anything that contains
   one — and NEVER on html/body, which turns them into scroll containers
   and silently breaks every position:sticky descendant on the site (the
   header and the how-it-works sub-nav both depend on it). */

.ring-field{ position:relative; overflow:clip; }
.ring-field > *{ position:relative; z-index:1; }
.ring-field::after{
  content:""; position:absolute; z-index:0; pointer-events:none;
  width:min(72vw,880px); aspect-ratio:1; border-radius:50%;
  right:-14vw; bottom:-32vw;
  border:1px solid rgba(215,255,62,0.10);
  box-shadow:0 0 0 13vw rgba(215,255,62,0.020),
             0 0 0 26vw rgba(215,255,62,0.010);
}

/* The hero's own ring. `.hero` is already position:relative + isolate +
   overflow:hidden, and ::after was free (::before is the grid). */
.hero::after{
  content:""; position:absolute; z-index:-1; pointer-events:none;
  width:min(77vw,900px); aspect-ratio:1; border-radius:50%;
  right:-10vw; bottom:-30vw;
  border:1px solid rgba(215,255,62,0.13);
  box-shadow:0 0 0 13vw rgba(215,255,62,0.021),
             0 0 0 26vw rgba(215,255,62,0.011);
}

/* ---- footer: more room, and a ring rising behind the crest ----------
   Padding up from clamp(80,10vh,120) — "less is more" needs the space to
   be there in the first place. The ring is centred and mostly ABOVE the
   fold of the footer, so what you see is the top arc of something much
   larger passing behind the mark. z-index:0 keeps it under `.wrap`,
   which is already z-index:1, and under `.stars`. */
footer.pin-field{ padding:clamp(112px,14vh,172px) 0 40px; }
footer.pin-field::before{
  content:""; position:absolute; z-index:0; pointer-events:none;
  top:-46vw; left:50%; transform:translateX(-50%);
  width:min(104vw,1500px); aspect-ratio:1; border-radius:50%;
  border:1px solid rgba(215,255,62,0.10);
  box-shadow:0 0 0 12vw rgba(215,255,62,0.018),
             0 0 0 24vw rgba(215,255,62,0.009);
}
.foot-cta{ margin-bottom:clamp(64px,9vh,96px); }

/* ---- a faint glow behind a tile ------------------------------------
   Only on the DARK card contexts. The scan strip's cards sit inside
   `section.invert`, which flips the whole token ladder to bone — an
   accent bloom designed against near-black does not translate there, so
   that strip gets the offsets below and no glow. */
/* `isolation:isolate` + a z-index:-1 ring, NOT a z-index:0 ring plus
   `.price-card > *{ position:relative }`.

   That was the first version and it broke `.offer-stamp`: the stamp is a
   direct child of this card and is `position:absolute; top:-16px`, so a
   blanket `> *{ position:relative }` overrode it and dropped the -25%
   badge out of the corner and into the bottom of the card in normal flow.
   Isolating the card instead lets the ring sit behind the content with
   -1 without escaping the card, and leaves every child's own positioning
   completely alone. */
.price-card{ position:relative; isolation:isolate; }
/* ---- the corner motif, ONE PER TIER ---------------------------------
   Added 2026-08-04. The three cards carry visibly different corner
   geometry, getting more elaborate as the plan does: a plain hairline
   for Solo, concentric rings for Agency, and a dense radial figure for
   Full-time. It is decoration that happens to say something true — you
   can tell the tiers apart at a glance, from across the room, before
   reading a single number.

   ALL THREE ARE ONE PSEUDO-ELEMENT. `::before` is spoken for by the
   cursor light (see the tilt block), so everything here is built on
   `::after` alone: the border draws the outer ring, `box-shadow` spreads
   give the concentric ones for free, and `background` layers the
   patterning. No extra markup, and nothing for a future change to have
   to keep in step. */
.price-card::after{
  content:""; position:absolute; z-index:-1; pointer-events:none;
  top:-135px; right:-135px; width:290px; aspect-ratio:1;
  border-radius:50%; border:1px solid rgba(244,245,247,0.07);
  opacity:0.5;
}

/* SOLO — a single hairline circle. Nothing else. It is the entry plan
   and it looks like it: the restraint IS the signal. */
.price-card:nth-child(1)::after{
  border-color:rgba(215,255,62,0.22);
  background:radial-gradient(circle, rgba(215,255,62,0.07), transparent 70%);
  opacity:0.75;
}

/* GROWING AGENCY — PURELY CONCENTRIC, and no spokes. The first version
   gave this conic spokes too, which made it read as a faint copy of
   Full-time rather than as its own thing: three tiers that look like one
   idea at three opacities say nothing. Rings here, spokes there. */
.price-card:nth-child(2)::after{
  border-color:rgba(244,245,247,0.18);
  box-shadow:0 0 0 20px rgba(244,245,247,0.055),
             0 0 0 42px rgba(244,245,247,0.035),
             0 0 0 66px rgba(244,245,247,0.02);
  opacity:0.8;
  background:radial-gradient(circle, transparent 62%, rgba(244,245,247,0.035) 63%, transparent 66%);
}

/* FULL-TIME — the intricate one. The concentric rings, plus radial
   spokes, plus a ring of dots, plus a faint core. Densest geometry on
   the page, and deliberately the only place this much of it appears. */
.price-card:nth-child(3)::after{
  border-color:rgba(244,245,247,0.16);
  box-shadow:0 0 0 16px rgba(244,245,247,0.05),
             0 0 0 34px rgba(244,245,247,0.035),
             0 0 0 54px rgba(244,245,247,0.022);
  opacity:0.72;
  background:
    /* spokes */
    repeating-conic-gradient(from 7.5deg,
      rgba(244,245,247,0.075) 0deg 1.4deg, transparent 1.4deg 15deg),
    /* an inner dotted orbit */
    radial-gradient(circle at 50% 8%,  rgba(244,245,247,0.16) 1.6px, transparent 2px),
    radial-gradient(circle at 92% 50%, rgba(244,245,247,0.16) 1.6px, transparent 2px),
    radial-gradient(circle at 50% 92%, rgba(244,245,247,0.16) 1.6px, transparent 2px),
    radial-gradient(circle at 8%  50%, rgba(244,245,247,0.16) 1.6px, transparent 2px),
    /* a faint core so the centre is not a hole */
    radial-gradient(circle, rgba(244,245,247,0.05), transparent 46%);
}

/* Below the three-column breakpoint the cards stack, but each motif is
   still worth keeping — `.price-card{ overflow:clip }` (below) already
   confines it to its own card, so there's no crowding to strip it for.
   Removed 2026-08-06 after it made every stacked card's corner identical
   on mobile, which is the one place a reader looks at just one card at a
   time and the detail could actually register. */

/* ---- pricing: bigger tiles, and Solo bigger than the other two ------
   `align-items:center` rather than the grid's default stretch is what
   lets one card be taller WITHOUT transforms — which matters because
   js/motion.js's tilt owns `transform` on .price-card and a static
   offset written there would be overwritten on first hover.
   Solo is `.feat`, and it is the plan being promoted. */
/* THE FEATURED CARD IS WIDER, not just taller. Corrected 2026-08-04:
   the first pass made Solo 54px taller at equal width, and Ehan's read
   was that it did not look larger at all — correctly, because at a
   glance a column of three equal-width cards reads as three equal
   cards whatever their heights are. The reference design gets the
   effect from WIDTH (1.45fr against 1fr), and width is the signal.
   Kept at 1.4 rather than 1.45 so the two smaller cards still hold a
   comfortable measure for their bullet lists at 1340. */
/* THE THREE-COLUMN TRACK IS DESKTOP-ONLY, AND THE MEDIA QUERY IS THE
   WHOLE POINT OF THIS BLOCK (fixed 2026-08-05).

   Written unscoped, this rule sits ~3,700 lines BELOW
   `@media (max-width:820px){ .price-grid{ grid-template-columns:1fr } }`
   at equal specificity, so it won at every width and the mobile collapse
   never happened. Measured at 375px: three tracks of 116px / 83px / 83px.
   "Most popular · launch price" wrapped over four lines, "Solo operator"
   over two, and the feature bullets came out 79-90px wide — a pricing
   table nobody could read, on the page people are on immediately before
   entering card details.

   Same trap as `nav.wrap ul#navlinks{ position:relative }` further up,
   which is documented there and cost this file 122px of horizontal
   overflow: an unscoped late rule silently overriding a mobile one. When
   you add a track definition here, scope it. */
@media (min-width:821px){
  .price-grid{
    align-items:center; gap:20px;
    grid-template-columns:minmax(0,1.4fr) minmax(0,1fr) minmax(0,1fr);
  }
}
/* Gap applies at every width; only the track list is desktop-only. */
.price-grid{ gap:20px; }
.price-card{
  min-height:470px; padding:34px 30px; border-radius:var(--r-lg);
  /* Matches what `.price-card.tilt-3d` already computes to in production
     (verified: overflow is `hidden` on the live card today, because the
     tilt block sets it for the spotlight). Stated on the base rule so the
     ring above is clipped identically whether or not the script runs. */
  overflow:clip;
}
.price-card.feat{ min-height:524px; padding:42px 32px; z-index:2; }

/* THE CTA IS PINNED TO THE BOTTOM. Making one card taller opened a band
   of dead space above its button, because the card is a column flexbox
   and the content simply stopped where it stopped. `margin-top:auto` on
   the last real item pushes the button down and takes the slack into the
   gap above it, which is where extra room belongs.

   Targeted at the CTA rather than `> :last-child`, deliberately:
   js/pricing-offer.js APPENDS `.offer-stamp` to this card, so after the
   offer is accepted the last child is the stamp, and the button would
   silently stop being pinned. */
.price-card > .plan-cta,
.price-card > .glow-wrap{ margin-top:auto; }
.price-card ul{ margin-bottom:26px; }

/* THE STAMP SITS INSIDE THE CARD NOW. It was `top:-16px; right:-14px`,
   which only works on an unclipped card — and the card has NOT been
   unclipped since the tilt block started setting `overflow:hidden` for
   the cursor spotlight. So the badge has been rendering as a cut-off
   crescent on the live site rather than as a full circle. Pre-existing,
   and easy to miss at the old card size; obvious once the tiles grew.
   Moved in rather than un-clipping the card, because the clip is what
   keeps the spotlight and the corner ring inside the rounded edge. */
.offer-stamp{ top:14px; right:14px; }

/* ---- "what differs, and what doesn't" as a panel --------------------
   Was a bare heading over a bare table, which read as an appendix. It is
   the page's actual argument for the tiers, so it gets a surface, an
   edge and room to breathe. */
.cmp-wrap{
  margin-top:clamp(56px,8vh,88px);
  background:var(--surface); border:1px solid var(--line);
  border-radius:var(--r-lg); padding:clamp(22px,3vw,40px);
  position:relative; overflow:clip;
}
.cmp-wrap::after{
  content:""; position:absolute; z-index:0; pointer-events:none;
  top:-180px; left:-120px; width:420px; aspect-ratio:1; border-radius:50%;
  border:1px solid rgba(215,255,62,0.07);
}
.cmp-wrap > *{ position:relative; z-index:1; }

/* ==== TWO SECTIONS, NOT ONE RUN-ON BLOCK (spacing, 2026-08-06) ========
   "The same on every plan" and "What differs between plans" are two
   separate arguments, and they were set 32px apart with each heading only
   6px off its own content. At a heading that renders up to 1.9rem, 6px is
   less than a third of its own cap-height — so each title read as a label
   stuck to the block below it, and the two blocks read as one crowded
   list. Reported as bunched, and it is the same fault twice: the gaps
   between the parts were smaller than the parts themselves.

   THE RULE APPLIED HERE IS THAT SEPARATION MUST EXCEED ADJACENCY. A
   heading has to sit closer to the thing it names than the two groups sit
   to each other, or the grouping inverts and the eye pairs a title with
   the block above it. So: ~20px title-to-content, and a clamped 56-88px
   between the two groups — comfortably more than double, at every width.

   Clamped rather than fixed because this well is up to 1120px wide: a
   flat 32px that looks tight at 1440 looks absurd at 640. The floor is
   what a phone gets, the ceiling is what stops it becoming a gap you have
   to scroll through. No divider rule between them — the space is the
   separator, which is the cheaper and quieter of the two. */
.cmp-title{ font-size:clamp(1.3rem,2.4vw,1.9rem); margin:0 0 clamp(14px,1.6vw,20px); }
.cmp-title-differs{ margin-top:clamp(56px,7vw,88px); }
/* The list is one of the two groups, so it earns the same interior air as
   the panel around it rather than staying at a flat 18/10. */
.cmp-included{ gap:13px; padding:clamp(20px,2.4vw,28px); }
.cmp-note{ margin-top:clamp(20px,2.4vw,30px); }

/* ---- deliberate offsets --------------------------------------------
   TWO PLACES ONLY, and that restraint is the whole effect: used
   everywhere it stops reading as deliberate and starts reading as
   broken alignment.

   Composed through a custom property rather than by overwriting
   `transform`, because .pin-card's hover already owns that property and
   a second declaration would simply win and kill the lift. */
.pin-card{ transform:translateY(var(--off,0px)) translateZ(0); }
.pin-card:hover{ transform:translateY(calc(var(--off,0px) - 6px)) scale(1.022); }
@media (min-width:901px){
  .scan-row .pin-card:nth-child(2){ --off:15px; }
  .scan-row .pin-card:nth-child(3){ --off:-9px; }
  .scan-row .pin-card:nth-child(4){ --off:8px; }
}

/* Below the two-column breakpoint an offset is just a misaligned card,
   and under reduced motion a staggered row reads as an unfinished
   layout rather than as a composition. Both go flat. */
@media (max-width:900px){
  .scan-row .pin-card{ --off:0px; }
  .ring-field::after, .hero::after{ opacity:0.6; }
  footer::before{ top:-70vw; }
}
@media (prefers-reduced-motion:reduce){
  .scan-row .pin-card{ --off:0px; }
}

/* ====================================================================
   WHY US — demand evidence, workflow steps          added 2026-08-04
   ====================================================================
   Components for the rebuilt why-us.html. Everything here is content
   presentation; there is no new effect and no new pointer listener. */

/* ---- the counted figures -------------------------------------------
   These lead the demand section because they are the SAFEST evidence we
   have as well as the strongest: a group's membership is a public fact
   about a group, not about any person in it. */
.demand-stats{
  display:grid; grid-template-columns:repeat(3,1fr); gap:18px;
  margin:34px 0 40px;
}
.dstat{
  background:var(--surface); border:1px solid var(--line);
  border-radius:var(--r-lg); padding:26px 24px;
}
.dnum{
  display:block; font-family:var(--font-display);
  font-size:clamp(2rem,4vw,3.1rem); font-weight:600;
  letter-spacing:-0.04em; line-height:1; color:var(--acc);
  margin-bottom:12px;
}
.dlab{ display:block; color:var(--dim); font-size:0.92rem; line-height:1.5; }

/* ---- the wall of recreated enquiries --------------------------------
   RECREATIONS, NOT SCREENSHOTS, and the markup says so in a caption that
   must not be removed. Styled as handwritten-ish notes rather than as
   platform screenshots ON PURPOSE: anything that mimicked a real Facebook
   or Threads card would imply these are captures of real posts, which is
   the exact impression the legal constraint exists to avoid. */
.wall{
  display:grid; grid-template-columns:repeat(3,1fr); gap:14px;
}
.note{
  background:var(--raised); border:1px solid var(--line);
  border-left:2px solid var(--acc-line);
  border-radius:var(--r-lg); padding:22px 22px 20px;
}
.note p{
  margin:0; color:var(--text); font-size:1rem; line-height:1.55;
}
/* The caption under any block of other people's data. --dim rather than
   --faint: this is the line that makes the block honest, so it is not
   the line to make hardest to read. */
.ev-note{
  margin:22px 0 0; max-width:76ch;
  color:var(--dim); font-size:0.84rem; line-height:1.6;
}
.ev-note a{ color:var(--acc-text); }

/* ---- the five workflow steps ---------------------------------------
   Each step carries a real interface screenshot. The frame is a plain
   bordered surface, not a browser mock — these are app screens, and
   dressing them in fake browser chrome would be decoration pretending
   to be evidence. */
.flow-steps{ list-style:none; margin:34px 0 0; padding:0; display:grid; gap:44px; }
.fstep{ margin:0; }
.fstep-head{ display:flex; align-items:baseline; gap:14px; margin-bottom:10px; }
.fstep-n{
  font-family:var(--font-mono); font-size:0.8rem; color:var(--acc);
  letter-spacing:0.14em;
}
.fstep h3{ margin:0; }
.fstep > p{ max-width:70ch; margin:0 0 20px; }
.shot{
  margin:0; border:1px solid var(--line); border-radius:var(--r-lg);
  overflow:hidden; background:var(--surface);
  box-shadow:0 30px 70px -40px rgba(0,0,0,0.95);
}
.shot img{ display:block; width:100%; height:auto; }

@media (max-width:900px){
  .demand-stats{ grid-template-columns:1fr; gap:12px; }
  .wall{ grid-template-columns:1fr; }
  .flow-steps{ gap:34px; }
}
@media (min-width:901px) and (max-width:1150px){
  .wall{ grid-template-columns:repeat(2,1fr); }
}

/* `.diff-grid` is three columns, which leaves a four-card section as
   3 + 1 — a lone card on its own row reads as a layout that broke, not
   as a deliberate arrangement. why-us.html has two four-card sections
   (the pillars, and the limitations), so they opt into a 2x2 instead.
   Kept as a modifier rather than made automatic: the three-card grids
   on this page and on how-it-works are correct as they are. */
.diff-grid.grid-4{ grid-template-columns:repeat(2,1fr); gap:16px; }
@media (max-width:900px){ .diff-grid.grid-4{ grid-template-columns:1fr; } }

/* ====================================================================
   CONTACT — ONE QUESTION AT A TIME                  added 2026-08-04
   ====================================================================
   Styles for js/contact-steps.js. Every rule here is scoped to markup
   that script creates, so with no JavaScript none of it applies and
   contact.html renders as the ordinary form it has always been. */

.cf-shell{ display:block; }

/* ---- progress -------------------------------------------------------
   Always visible, and the step count is stated in words as well as
   drawn. Hidden length is what makes a stepped form feel endless. */
.cf-prog{ display:flex; align-items:center; gap:14px; margin:0 0 26px; }
.cf-prog-track{
  flex:1; height:4px; border-radius:999px;
  background:var(--raised-2); overflow:hidden;
}
.cf-prog-fill{
  display:block; height:100%; width:20%; border-radius:999px;
  background:var(--acc); transition:width .32s var(--fluid);
}
.cf-prog-txt{
  font-family:var(--font-mono); font-size:0.68rem; letter-spacing:0.12em;
  text-transform:uppercase; color:var(--dim); white-space:nowrap;
}

/* ---- a step ---------------------------------------------------------
   `.cf-step[hidden]` needs an EXPLICIT display:none. The `hidden`
   attribute is UA-origin and an author rule setting `display` on the
   same element always beats it regardless of specificity — the exact
   trap that made the settings popover show on every page load once
   already. Do not remove this rule. */
.cf-step{
  border:0; margin:0; padding:0; min-width:0;
}
.cf-step[hidden]{ display:none; }
.cf-legend{
  padding:0; margin:0 0 6px;
  font-family:var(--font-display); font-weight:600;
  font-size:clamp(1.35rem,2.6vw,1.85rem); letter-spacing:-0.03em;
  line-height:1.2; color:var(--text);
}
.cf-hint{ margin:0 0 22px; color:var(--dim); font-size:0.95rem; }

.cf-anim .cf-step:not([hidden]){ animation:cf-in .34s var(--fluid) both; }
@keyframes cf-in{
  from{ opacity:0; transform:translateY(10px); }
  to{ opacity:1; transform:none; }
}
/* Reduced motion gets a cross-fade, not nothing: the transition is
   feedback that the step changed, so it is reduced rather than removed. */
.cf-still .cf-step:not([hidden]){ animation:cf-fade .12s linear both; }
@keyframes cf-fade{ from{ opacity:0; } to{ opacity:1; } }

/* ---- the category cards --------------------------------------------
   Six labelled targets instead of a native <select> popup, which is the
   step that benefits most on a phone. The real <select> is still in the
   DOM and still carries the value — see the header of
   js/contact-steps.js for why that matters to the test suite. */
.cf-hidden-control{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
}
.cf-cards{
  display:grid; grid-template-columns:repeat(3,1fr); gap:10px;
}
.cf-card{
  text-align:left; cursor:pointer; font:inherit;
  padding:16px 16px; border-radius:var(--r-lg);
  border:1px solid var(--line); background:var(--raised);
  color:var(--text); font-size:0.95rem; line-height:1.35;
  transition:border-color .18s ease, background .18s ease, transform .18s var(--fluid);
}
.cf-card:hover{ border-color:var(--line-strong); background:var(--raised-2); }
.cf-card.on{
  border-color:var(--acc); background:var(--acc-soft); color:var(--text);
}
.cf-card:focus-visible{ outline:2px solid var(--acc); outline-offset:2px; }

/* ---- nav ------------------------------------------------------------
   Outside the steps, so the buttons do not move as the step height
   changes. A Send button that jumps is a Send button that gets
   mis-clicked. */
.cf-nav{ display:flex; align-items:center; gap:12px; margin-top:30px; }
.cf-nav [hidden]{ display:none; }
.cf-next, .cf-nav #c-send{ margin-left:auto; }
.cf-back + .cf-next{ margin-left:auto; }

.cf-escape{
  display:block; margin:20px auto 0; padding:6px 2px;
  background:none; border:0; cursor:pointer;
  color:var(--dim); font-size:0.85rem; text-decoration:underline;
  text-underline-offset:3px; font-family:inherit;
}
.cf-escape:hover{ color:var(--text); }

/* The escape hatch: every step visible at once, which is exactly the
   page as it was before this enhancement existed. */
.cf-all .cf-step{ margin-bottom:26px; }
.cf-all .cf-prog{ display:none; }
.cf-all .cf-legend{ font-size:1.05rem; }

/* ---- per-field validation ------------------------------------------ */
.cf-bad input, .cf-bad textarea, .cf-bad .cf-cards{
  border-color:var(--bad);
}
.cf-err{
  margin:8px 0 0; color:var(--bad); font-size:0.85rem;
}

@media (max-width:760px){
  .cf-cards{ grid-template-columns:1fr; }
  .cf-nav{ flex-wrap:wrap; }
  /* 30px as a bare text button, which is under the 44px touch-target
     guideline. Padded rather than made bigger so it stays visually a
     quiet secondary action. */
  .cf-escape{ padding:13px 12px; }
  /* A 44px minimum on everything you have to hit, and the primary
     action goes full width so the on-screen keyboard cannot leave it as
     a sliver at the edge. */
  .cf-nav .btn{ min-height:48px; }
  .cf-next, .cf-nav #c-send{ flex:1 1 100%; margin-left:0; }
}
/* See the note in js/contact-steps.js: the category field's visible
   label duplicates the step legend right above it. */
.cf-catfield > span{ display:none; }

/* ====================================================================
   THE APP, BROUGHT INTO THE SAME LANGUAGE          added 2026-08-04
   ====================================================================
   TASK 8's remaining half. The marketing site got rings, a stronger
   spectral wash and a wider well; crossing into the workspace should
   not feel like crossing into a different product — which was the exact
   reason the site was rethemed to the app's ladder in the first place.

   RESTRAINT IS THE BRIEF HERE, and it is not the same brief as the
   marketing pages. A landing page is asking for attention; a dashboard
   is a tool somebody is trying to read numbers off. So this pass adds
   depth and no ornament: nothing here sits near a figure, nothing here
   moves, and every value is well below what the marketing pages carry.

   Both rules the app block already sets still hold:
     - nothing outside a `.app-body` selector
     - --acc stays the only ACCENT. These are washes, far out of focus,
       never a fill, a border or a text colour — the same exemption the
       marketing side gives --spec-*. Note the wash below was ALREADY
       two-hue (accent plus a blue-grey) before this change; this
       deepens it, it does not introduce the idea. */

.app-body::before{
  background:
    radial-gradient(900px 420px at 22% -8%, rgba(215,255,62,0.11), transparent 70%),
    radial-gradient(700px 380px at 90% 0%, rgba(127,161,209,0.08), transparent 70%),
    radial-gradient(760px 460px at 96% 78%, rgba(110,91,255,0.055), transparent 72%),
    radial-gradient(680px 400px at 4% 88%, rgba(43,184,255,0.04), transparent 72%);
}

/* A single ring, low and to the right, behind the working area. Same
   construction as the marketing rings — one circle, two spread-only
   shadows — at roughly a third of their alpha, because here it sits
   behind a table rather than behind a headline.
   `overflow:clip` on .app-main, never `hidden`: this page has sticky
   descendants and `hidden` would make it a scroll container. */
.app-body .app-main{ position:relative; overflow:clip; }
.app-body .app-main::after{
  content:""; position:absolute; z-index:0; pointer-events:none;
  right:-16vw; bottom:-30vw;
  width:min(64vw,780px); aspect-ratio:1; border-radius:50%;
  border:1px solid rgba(215,255,62,0.05);
  box-shadow:0 0 0 10vw rgba(215,255,62,0.010),
             0 0 0 20vw rgba(215,255,62,0.005);
}
/* Everything real sits above it. Scoped to the direct children so an
   absolutely-positioned descendant keeps its own placement — the
   mistake that dropped the pricing page's -25% stamp into normal flow
   earlier today. */
.app-body .app-main > *{ position:relative; z-index:1; }

/* The wider well, applied to the workspace too. A lead table is the one
   thing on this site that most wants the extra room. */
.app-body .app-main{ max-width:var(--well); }

/* A corner ring on a panel, at the threshold of visible. Enough to stop
   a large empty panel reading as a flat rectangle, not enough to be
   noticed while reading one. */
.app-body .panel{ position:relative; overflow:clip; isolation:isolate; }
.app-body .panel::after{
  content:""; position:absolute; z-index:-1; pointer-events:none;
  top:-120px; right:-120px; width:260px; aspect-ratio:1;
  border-radius:50%; border:1px solid rgba(244,245,247,0.045);
}

/* ====================================================================
   THE HOMEPAGE TERRITORY DEMO                       added 2026-08-04
   ====================================================================
   Sits in the hero, under the two CTAs. Deliberately reads as part of
   the page rather than as a widget dropped on it: same glass surface,
   same radius, same mono micro-labels as everything else in the hero. */
.demo{
  margin-top:30px; max-width:min(100%,560px);
  background:var(--glass-bg); border:1px solid var(--glass-line);
  border-radius:var(--r-lg); padding:18px 18px 14px;
  backdrop-filter:blur(var(--glass-blur));
  box-shadow:var(--glass-shadow);
}
.demo-head{ display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:12px; }
.demo-lbl, .demo-state{
  font-family:var(--font-mono); font-size:0.62rem; letter-spacing:0.12em;
  text-transform:uppercase;
}
.demo-lbl{ color:var(--text-on-wash); }
.demo-state{ color:var(--faint); }
.demo-state.working{ color:var(--acc-text); }

.demo-row{ display:flex; gap:10px; align-items:stretch; }
.demo-field{ flex:1; min-width:0; display:block; }
.demo-field input{
  width:100%; height:100%; min-height:48px;
  background:rgba(0,0,0,0.28); border:1px solid var(--line-strong);
  border-radius:var(--r-pill); padding:0 18px;
  color:var(--text); font-family:inherit; font-size:0.98rem;
}
.demo-field input::placeholder{ color:var(--faint); }
.demo-field input:focus-visible{ outline:2px solid var(--acc); outline-offset:2px; }
.demo-row .btn{ min-height:48px; flex:0 0 auto; }

.demo-out{ margin-top:14px; }
.demo-out[hidden]{ display:none; }
.demo-head-line{ margin:0 0 10px; font-size:0.92rem; color:var(--dim); }
.demo-head-line strong{ color:var(--text); }

.demo-list{ list-style:none; margin:0; padding:0; display:grid; gap:6px; }
.demo-hit{
  display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,1fr) auto;
  grid-template-areas:"trade area grade" "why why why";
  gap:2px 10px; align-items:center;
  padding:10px 12px; border-radius:10px;
  background:rgba(255,255,255,0.035); border:1px solid var(--line);
}
.demo-trade{ grid-area:trade; color:var(--text); font-size:0.9rem; text-transform:capitalize; }
.demo-area{ grid-area:area; color:var(--dim); font-size:0.85rem; }
.demo-why{
  grid-area:why; color:var(--faint);
  font-family:var(--font-mono); font-size:0.64rem; letter-spacing:0.06em;
}
.demo-grade{
  grid-area:grade; justify-self:end;
  font-family:var(--font-mono); font-size:0.68rem; font-weight:600;
  min-width:22px; text-align:center; padding:2px 7px; border-radius:6px;
  background:rgba(255,255,255,0.06); color:var(--dim);
}
/* Only A and B get the accent. Every grade lighting up is the same as
   none of them doing so. */
.demo-grade.g-a{ background:var(--acc-soft); color:var(--acc-text); }
.demo-grade.g-b{ background:rgba(215,255,62,0.10); color:var(--acc-text); }

.demo-more{ margin:12px 0 0; font-size:0.92rem; color:var(--dim); }
.demo-more strong{ color:var(--text); }
.demo-more a{ color:var(--acc-text); }
.demo-msg{ margin:0; font-size:0.9rem; color:var(--dim); }
.demo-msg.quiet{ margin-top:8px; font-size:0.8rem; color:var(--faint); }
.demo-msg a{ color:var(--acc-text); }

/* The caption is not decoration. It states that this spends a real
   Places request and that businesses are shown by trade and area only —
   the two facts a visitor is owed about what just happened. */
.demo-cap{
  margin:12px 0 0; font-size:0.72rem; line-height:1.5; color:var(--faint);
}

@media (max-width:560px){
  .demo{ margin-top:24px; }
  .demo-row{ flex-wrap:wrap; }
  .demo-row .btn{ flex:1 1 100%; }
  .demo-hit{ grid-template-columns:minmax(0,1fr) auto; grid-template-areas:"trade grade" "area grade" "why why"; }
}

/* ====================================================================
   THE APPEARANCE PICKER                             added 2026-08-04
   ====================================================================
   Style descriptors on generate.html and fullsite.html. A <details>, so
   it is genuinely closed by default with no script involved — the
   zero-input path is the common path and must stay fast. */
.app-body .ap{
  margin-top:16px; border:1px solid var(--app-line);
  border-radius:var(--r-lg); background:var(--app-surface);
  overflow:clip;
}
.app-body .ap-sum{
  display:flex; align-items:center; justify-content:space-between; gap:14px;
  padding:14px 18px; cursor:pointer; list-style:none;
}
.app-body .ap-sum::-webkit-details-marker{ display:none; }
.app-body .ap-sum::after{
  content:"+"; font-family:var(--font-mono); color:var(--app-dim);
  font-size:1.1rem; line-height:1; order:3;
}
.app-body .ap[open] .ap-sum::after{ content:"\2212"; }
.ap-sum-t{ color:var(--app-text); font-size:0.95rem; }
.ap-opt-tag{
  font-family:var(--font-mono); font-size:0.6rem; letter-spacing:0.1em;
  text-transform:uppercase; color:var(--app-faint); margin-left:8px;
}
.ap-count{
  margin-left:auto; font-family:var(--font-mono); font-size:0.66rem;
  letter-spacing:0.06em; color:var(--app-faint);
}
.ap-count.warn{ color:var(--warn); }
.app-body .ap.has-any .ap-count{ color:var(--acc-text); }
.ap-body{ padding:0 18px 18px; border-top:1px solid var(--app-line); }
.ap-body .cap{ margin:14px 0 14px; }

.ap-chips{ display:flex; flex-wrap:wrap; gap:8px; }
.ap-chips:empty{ display:none; }
.ap-chip{
  display:inline-flex; align-items:center; gap:7px;
  padding:6px 8px 6px 12px; border-radius:999px;
  background:var(--acc-soft); border:1px solid var(--acc-line);
  color:var(--app-text); font-size:0.84rem;
}
.ap-chips + .field{ margin-top:14px; }

.ap-list{ display:grid; gap:10px; margin-top:12px; }
.ap-group{ display:flex; flex-wrap:wrap; gap:6px; align-items:center; }
.ap-group-t{
  font-family:var(--font-mono); font-size:0.6rem; letter-spacing:0.12em;
  text-transform:uppercase; color:var(--app-faint);
  width:64px; flex:0 0 64px;
}
.ap-opt{
  padding:6px 11px; border-radius:999px; cursor:pointer;
  border:1px solid var(--app-line); background:var(--app-raised);
  color:var(--app-dim); font:inherit; font-size:0.82rem;
  transition:border-color .16s ease, color .16s ease, background .16s ease;
}
.ap-opt:hover{ border-color:var(--acc-line); color:var(--app-text); }
.ap-opt.ap-own{ border-color:var(--acc-line); color:var(--acc-text); }
.ap-opt:focus-visible{ outline:2px solid var(--acc); outline-offset:2px; }

/* Three stops, not a slider. A slider implies a precision the model
   cannot deliver — there is no meaningful difference between 61% and
   64% "luxury", and offering one is a promise the output cannot keep. */
.ap-strength{
  margin:18px 0 0; padding:14px 0 0; border:0;
  border-top:1px solid var(--app-line);
  display:flex; flex-wrap:wrap; gap:10px; align-items:center;
}
.ap-strength legend{
  padding:0; font-size:0.84rem; color:var(--app-dim); margin-bottom:10px;
}
.ap-strength label{
  display:inline-flex; align-items:center; gap:7px; cursor:pointer;
  padding:7px 13px; border-radius:999px;
  border:1px solid var(--app-line); background:var(--app-raised);
  color:var(--app-dim); font-size:0.84rem;
}
.ap-strength input{ accent-color:var(--acc); margin:0; }
.ap-strength label:has(input:checked){
  border-color:var(--acc); color:var(--app-text); background:var(--acc-soft);
}

@media (max-width:640px){
  .app-body .ap-sum{ flex-wrap:wrap; }
  .ap-count{ width:100%; margin-left:0; }
  .ap-group-t{ width:100%; flex:0 0 100%; }
}

/* ---- the in-app editor (TASK 5) -------------------------------------
   Refusal, done and failure are three visually distinct states, and the
   REFUSAL IS NOT STYLED AS AN ERROR — nothing went wrong when the guard
   works, and colouring it like a failure teaches people that asking for
   something reasonable breaks the tool. */
.app-body .ed .cap{ margin-bottom:16px; }
.ed-count{
  margin-left:auto; font-family:var(--font-mono); font-size:0.66rem;
  letter-spacing:0.08em; color:var(--app-faint);
}
.ed-count.spent{ color:var(--warn); }
.ed-actions{ display:flex; flex-wrap:wrap; gap:10px; margin-top:14px; }
.ed-out{
  margin-top:16px; padding:14px 16px; border-radius:12px;
  border:1px solid var(--app-line); background:var(--app-raised);
}
.ed-out[hidden]{ display:none; }
.ed-msg{ margin:0; color:var(--app-dim); font-size:0.92rem; line-height:1.55; }
.ed-msg + .ed-msg{ margin-top:8px; }
.ed-msg strong{ color:var(--app-text); }
.ed-msg.quiet{ font-size:0.84rem; color:var(--app-faint); }
.ed-refused-t{
  margin:0 0 6px; font-family:var(--font-mono); font-size:0.66rem;
  letter-spacing:0.12em; text-transform:uppercase; color:var(--app-dim);
}
.ed-out.refused{ border-color:var(--app-line-strong); }
.ed-out.done{ border-color:var(--acc-line); }
.ed-out.bad{ border-color:var(--bad); }
.ed-out.working .ed-msg{ color:var(--acc-text); }

/* ---- cookie notice (js/cookie-notice.js) ---------------------------
   A NOTICE, NOT A CONSENT GATE — the reasoning is in that file's header,
   and the short version is that this site sets no cookies at all until
   you sign in, so there is nothing to ask permission for.

   Styled as a fourth island so it belongs to the dock's language rather
   than looking like a bolted-on compliance widget. Bottom-LEFT, where it
   cannot collide with the settings/bug FABs on the right. */
.cookie-note{
  position:fixed; left:20px; bottom:20px; z-index:120;
  width:min(360px, calc(100vw - 40px));
  padding:18px 20px 16px;
  background:rgba(18,23,30,0.86);
  border:1px solid rgba(255,255,255,0.09);
  border-radius:16px;
  backdrop-filter:blur(14px) saturate(150%);
  -webkit-backdrop-filter:blur(14px) saturate(150%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.10),
    0 20px 44px -22px rgba(0,0,0,1);
  opacity:0; transform:translateY(12px);
  transition:opacity .28s ease, transform .28s cubic-bezier(.34,1.2,.4,1);
}
.cookie-note.in{ opacity:1; transform:none; }
.cookie-note strong{
  display:block; font-size:0.95rem; font-weight:600;
  color:var(--text); margin-bottom:6px;
}
.cookie-note p{
  margin:0 0 14px; font-size:0.83rem; line-height:1.6; color:var(--dim);
}
.cookie-note a{ color:var(--acc-text); }
.cookie-note-row{ display:flex; gap:8px; flex-wrap:wrap; }
@media (prefers-reduced-motion:reduce){
  .cookie-note{ transition:opacity .01s linear; transform:none; }
}


/* ======================================================================
   PASSWORD STRENGTH, CODE ENTRY AND MANAGE ACCOUNT       (2026-08-06)
   ====================================================================== */

/* ---- the strength meter (js/password-strength.js) -------------------
   FOUR SEGMENTS, ONE PER REQUIREMENT. It is a checklist wearing a bar,
   not a guess at entropy — see that file for why there is deliberately no
   "Weak / Medium / Strong" verdict here. Every segment stands for a rule
   written directly underneath it, so a full bar means exactly "this will
   be accepted" and claims nothing else. */
.pw-meter{ margin:8px 0 2px; }

.pw-bar{ display:grid; grid-template-columns:repeat(4,1fr); gap:4px; }
.pw-bar span{
  height:4px; border-radius:2px; background:var(--line-strong);
  transition:background .22s ease;
}
.pw-bar span.on{ background:var(--warn); }
/* Green only when EVERY rule passes. A part-green bar would read as
   "good enough", which is the one thing it must not say — three of four
   is refused by the server exactly as harshly as none of four. */
.pw-meter.done .pw-bar span.on{ background:var(--ok); }

.pw-rules{
  list-style:none; margin:8px 0 0; padding:0;
  display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
  gap:4px 14px;
}
.pw-rules li{
  display:flex; align-items:center; gap:7px;
  font-size:0.76rem; color:var(--faint); transition:color .22s ease;
}
.pw-rules li i{
  flex:0 0 auto; width:12px; height:12px; border-radius:50%;
  border:1.5px solid var(--line-strong); position:relative;
  transition:border-color .22s ease, background .22s ease;
}
.pw-rules li.ok{ color:var(--dim); }
.pw-rules li.ok i{ border-color:var(--ok); background:var(--ok); }
/* The tick is drawn rather than typed so it needs no icon font and no
   second glyph to line up — and it is on a pseudo-element of an
   aria-hidden marker, so nothing here reaches a screen reader twice. The
   list itself is aria-live, which is what announces the change. */
.pw-rules li.ok i::after{
  content:''; position:absolute; left:3px; top:0.5px;
  width:3px; height:6px; border:solid var(--canvas);
  border-width:0 1.5px 1.5px 0; transform:rotate(45deg);
}

/* Nothing typed yet: the rules are a preview of what is coming, not a
   list of mistakes already made. */
.pw-meter.empty .pw-bar span{ background:var(--line); }
.pw-meter.empty .pw-rules li{ color:var(--faint); }

@media (max-width:420px){
  .pw-rules{ grid-template-columns:1fr; }
}

/* ---- the 6-digit code box ------------------------------------------
   Wide tracking and a monospace face so six digits read as six separate
   things to check against an email, rather than as a number. */
.code-input{
  font-family:var(--font-display);
  font-size:1.5rem; letter-spacing:0.45em; text-align:center;
  text-indent:0.45em;   /* re-centres: the tracking adds space after the last digit */
  padding:14px 12px;
}
.code-hint{ font-size:0.76rem; color:var(--faint); margin:8px 0 0; }

#code-resend.is-waiting{
  color:var(--faint); pointer-events:none; text-decoration:none; cursor:default;
}

/* ---- the two-step offer (sign-in.html) ------------------------------ */
.twofa-actions{ display:flex; flex-wrap:wrap; gap:10px; margin-top:14px; }
.twofa-actions .btn{ flex:1 1 auto; justify-content:center; }

/* "Recommended" on the safer option. Recommending it is fair; the other
   button is a real, plainly-labelled button beside it and is deliberately
   NOT a grey link — nudging is honest, hiding is not. */
.pill-rec{
  display:inline-block; margin-left:8px; padding:2px 8px;
  border-radius:999px; font-family:var(--font-display); font-size:0.62rem;
  letter-spacing:0.08em; text-transform:uppercase;
  background:rgba(0,0,0,0.22); color:inherit;
}

/* ---- Manage account sheet ------------------------------------------- */
.acct-block{
  padding:18px 0; border-top:1px solid var(--line);
}
.acct-block:first-of-type{ border-top:0; padding-top:6px; }

.acct-head{
  display:flex; align-items:center; justify-content:space-between;
  gap:12px; margin-bottom:6px;
}
.acct-head h3{
  font-family:var(--font-heading); font-weight:400;
  font-size:1.02rem; line-height:1.3; margin:0;
}

.acct-state{
  font-family:var(--font-display); font-size:0.68rem;
  letter-spacing:0.08em; text-transform:uppercase;
  padding:3px 9px; border-radius:999px;
  border:1px solid var(--line-strong); color:var(--faint);
}
.acct-state.on{ border-color:var(--ok); color:var(--ok); }

.acct-actions{ display:flex; flex-wrap:wrap; gap:10px; }

/* Step two of the email change sits directly under step one whenever a
   change is pending, and with no separation the two forms read as one
   long form with two submit buttons in the middle of it. A rule rather
   than a margin, because they are genuinely sequential steps. */
#acct-email-confirm{
  margin-top:18px; padding-top:16px; border-top:1px dashed var(--line-strong);
}

/* The sheet is taller than the other two and every section is a form, so
   it scrolls rather than pushing its own buttons off screen. */
#account-sheet .sheet{ max-height:88vh; overflow-y:auto; }

/* ==== CONNECT-THE-FORM BLOCK (fullsite.html step 3) ==================== */
.fs-owner{
  margin:22px 0 4px; padding:18px 18px 14px;
  border:1px dashed var(--line-strong); border-radius:10px;
  background:var(--raised);
}
.fs-owner h3{ margin:0 0 6px; font-size:1.02rem; }
.fs-owner .cap{ margin:0 0 14px; }
.fs-owner .field{ margin-bottom:12px; }
.fs-owner .field:last-of-type{ margin-bottom:0; }
#fs-owner-note{ margin:12px 0 0; }

/* ==== THE OWNER'S ENQUIRY PAGE (bookings.html) =========================
   Deliberately the plainest page in the project. The reader is a business
   owner on a phone who wants to know who wants to buy something from them,
   and every element that is not that is noise. No dock, no gauges, no
   brand furniture beyond the wordmark's absence being unremarkable. */
.bookings-body{ background:var(--canvas); color:var(--text); }
.bk-wrap{
  max-width:720px; margin:0 auto;
  /* No dock on this page, so no dock clearance — see the DOCK CLEARANCE
     note elsewhere in this file before copying a figure from there. */
  padding:clamp(28px,6vw,56px) clamp(16px,4vw,28px) 72px;
}
.bk-head{ margin-bottom:26px; }
.bk-eyebrow{
  margin:0 0 6px; font-family:var(--font-display);
  font-size:.72rem; letter-spacing:.14em; text-transform:uppercase; color:var(--acc-text);
}
.bk-head h1{ margin:0 0 8px; line-height:1.16; }
.bk-sub{ margin:0; color:var(--dim); }

.bk-list{ display:flex; flex-direction:column; gap:14px; }
.bk-card{
  border:1px solid var(--line-strong); border-radius:10px;
  background:var(--surface); padding:16px 16px 14px;
}
/* Handled is DIMMED, NEVER HIDDEN. An owner who ticks something off by
   accident must be able to see it is still there — a message that
   disappears reads as a message that was deleted. */
.bk-card.is-handled{ opacity:.58; }
.bk-card-h{
  display:flex; flex-wrap:wrap; align-items:baseline;
  justify-content:space-between; gap:8px; margin-bottom:8px;
}
.bk-name{ margin:0; font-size:1.05rem; line-height:1.3; }
.bk-when{ font-size:.82rem; color:var(--faint); }
.bk-ways{ display:flex; flex-wrap:wrap; gap:10px 16px; margin:0 0 10px; }
.bk-way{ color:var(--acc-text); text-decoration:none; word-break:break-word; }
.bk-way:hover{ text-decoration:underline; }
/* The message keeps the line breaks the sender typed. They wrote it in a
   textarea; collapsing their paragraphs makes a considered enquiry read as
   a run-on. */
.bk-msg{ margin:0 0 12px; white-space:pre-wrap; }
.bk-tick{ margin:0; }

.bk-empty{
  margin:0; padding:26px 18px; text-align:center; color:var(--dim);
  border:1px dashed var(--line-strong); border-radius:10px;
}
.bk-foot{
  margin-top:34px; padding-top:18px; border-top:1px solid var(--line);
  color:var(--faint); font-size:.88rem;
}
.bk-foot p{ margin:0 0 8px; }
.bk-keep strong{ color:var(--dim); }

/* ---- connected contact forms (fullsite.html) ------------------------ */
.fs-sites{ display:flex; flex-direction:column; gap:14px; }
.fs-site{
  border:1px solid var(--line-strong); border-radius:10px;
  background:var(--raised); padding:16px;
}
.fs-site-h{
  display:flex; flex-wrap:wrap; align-items:baseline;
  justify-content:space-between; gap:6px 14px; margin-bottom:12px;
}
.fs-site-h h3{ margin:0; font-size:1rem; }
.fs-site-meta{ font-family:var(--font-mono); font-size:0.72rem; color:var(--faint); }
.fs-site .field{ margin-bottom:10px; }
.fs-site-actions{ display:flex; flex-wrap:wrap; gap:8px; align-items:center; }
/* The note is the only thing that explains what a save actually did —
   which enquiries moved and which did not — so it is body-sized, not
   fine print. Failure is the ONE state that gets a colour, and it gets
   the same one every other failure on the site uses. */
.fs-site-note{ margin:0 0 10px; }
.fs-site-note.is-bad{ color:#F08A78; }
