/* ============================================================
   Lumen Web — studio site v2
   Palette : void / paper / sage / cobalt. No cream, no terracotta.
   Type    : system grotesque display, serif body, mono for data.
   Loads   : nothing. No font CDN, no analytics, no third party.
   Contrast: every text pair verified, lowest in use 6.68:1.
   ============================================================ */
:root{
  --void:#12141A; --void-2:#1B1F27; --void-3:#242A35;
  --ink:#1C2027; --muted:#4A5260; --muted-light:#A8B3C6;
  --paper:#F1F2EE; --card:#FFFFFF; --card-2:#FAFAF8;
  --line:#D9DBD4; --line-dark:#31384A;
  --sage:#3F5B45; --sage-light:#C3D2C5; --sage-wash:#E8EFE9;
  --cobalt:#2743C8; --cobalt-dark:#1C33A8; --cobalt-light:#9FB6FF; --cobalt-wash:#E7ECFB;
  /* Fixed light-text colour for surfaces that are dark regardless of
     colour scheme (masthead, .band--dark, .card--dark, the diagrams) —
     distinct from --paper, which is the PAGE-level light surface colour
     and flips to near-black under prefers-color-scheme:dark. Using
     --paper directly as text colour on an always-dark background was a
     real bug: in OS dark mode the masthead, every dark hero band, both
     dark card variants, and the diagram sub-labels all rendered
     near-black text on a near-black background — invisible, not just
     low-contrast. Same bug class practice-foundation hit and fixed with
     its own -band tokens; caught here later because this static site has
     no automated a11y suite watching for it. */
  --paper-band:#F1F2EE;
  /* Fixed dark sage for SOLID FILLS carrying white text (the comparison
     slider's "Rebuilt" tag). Distinct from --sage, which brightens to
     #7FBE86 in dark mode so it still works as accent TEXT on a near-black
     page — correct for that job, and 2.0:1 under white text when the same
     token is used as a background. Same split, and the same reason, as
     practice-foundation's --color-deep / --color-deep-band pair. */
  --sage-band:#3F5B45;
  /* Scheme-aware "bad/warning" red — .ex-bad/.ex-dash/.ex-fill-bad/
     .ex-lbl-bad were hardcoded to this light-mode value with no dark
     counterpart, unlike --sage which already adapts. Dark value matches
     .ex--dark's own pre-existing #F0A6A0 so the two systems agree. */
  --bad:#B3261E;

  --display:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
  --body:Charter,"Bitstream Charter","Iowan Old Style","Palatino Linotype",Palatino,Georgia,"Times New Roman",serif;
  --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Segoe UI Mono","Liberation Mono",monospace;

  --measure:64ch;
  --gap:clamp(1rem,2.6vw,1.75rem);
  --pad:clamp(1.15rem,5vw,2.5rem);
  --band:clamp(2.75rem,7vw,5rem);
  --r:10px; --r-lg:20px; --r-xl:28px; --r-pill:999px;
  --shadow:0 1px 2px rgba(18,20,26,.05), 0 12px 32px -16px rgba(18,20,26,.28);
  --shadow-lift:0 4px 10px rgba(18,20,26,.08), 0 28px 56px -22px rgba(18,20,26,.4);
  --shadow-glow:0 4px 10px rgba(39,67,200,.08), 0 24px 48px -20px rgba(39,67,200,.32);
  --grad-cobalt:linear-gradient(135deg,#2743C8 0%,#5B3FBF 100%);
  --grad-sage:linear-gradient(135deg,#3F5B45 0%,#1F3A2E 100%);
  --grad-mesh:radial-gradient(circle at 15% 20%,rgba(63,91,69,.35),transparent 55%),radial-gradient(circle at 85% 15%,rgba(39,67,200,.32),transparent 50%),radial-gradient(circle at 60% 85%,rgba(91,63,191,.22),transparent 55%);
  --ease:cubic-bezier(.22,.68,0,1);

  /* Stacking order, in one place. Every z-index on this site references one
     of these — no bare numbers anywhere else in the file. Before this, the
     values were scattered magic numbers (99, 40, 30, 25, 4, 3, 2, 1, -1)
     across 700 lines, which is how two overlays silently end up on the same
     layer. The gaps are deliberate: room to slot something between two
     layers without renumbering the ones above it.
     Read top to bottom = back to front. */
  --z-below:-1;     /* decorative mesh gradients, behind their own content */
  --z-raised:1;     /* in-flow lift: the before/after slider's own panels */
  --z-sticky:30;    /* the masthead */
  --z-docked:25;    /* the bottom mini-CTA bar (below the masthead on purpose:
                       if both are visible, the header wins the overlap) */
  --z-overlay:40;   /* the scroll-progress bar, over everything but the skip link */
  --z-top:99;       /* the keyboard skip link — must never be covered by anything */
}
/* Dark theme: follows the OS/browser preference by default, and can be
   forced either way by the toggle in the masthead (see .theme-toggle and
   assets/theme-init.js) — the same `data-theme` attribute convention
   practice-foundation already uses for its own theme system, reused here
   rather than inventing a second one for the same problem.
   `:root:not([data-theme="light"])` means: apply when the OS prefers dark,
   UNLESS the visitor has explicitly forced light — that's what makes the
   toggle able to override the OS setting in the "go light anyway" direction.
   Values are new, not the existing --*-light / --line-dark / --void-*
   tokens above — those are already spoken for by .band--dark/.card--dark,
   which stay exactly as designed regardless of the page-level theme.
   Every pair here is contrast-checked the same way as the light palette;
   lowest ratio in use is 5.9:1 against WCAG's 4.5:1 floor. */
@media (prefers-color-scheme:dark){
  :root:not([data-theme="light"]){
    --ink:#F1F2EE; --muted:#A8B3C6;
    --paper:#12141A; --card:#1B1F27; --card-2:#242A35;
    --line:#333B4C;
    --sage:#7FBE86;
    --cobalt:#7C93FF; --cobalt-dark:#6B82F0; --cobalt-wash:#1B2340;
    --sage-wash:#1E2B22;
    --bad:#F0A6A0;
    --toggle-x:1; /* slides the theme-toggle thumb to its "dark" position */

    /* Elevation has to be re-stated for dark mode, not inherited.
       The light-mode shadows above are near-black (rgba(18,20,26,...)) —
       correct against a #F1F2EE page, and completely invisible against a
       #12141A one. Without these overrides every card, plan, panel and
       docked bar on the site rendered perfectly flat in dark mode: the
       whole elevation system silently stopped existing, while every
       contrast check still passed because contrast is not what broke.

       Two changes, not one. The shadow itself goes to pure black at a
       much higher opacity, since a dark shadow on a dark surface needs
       far more of it to register at all. And a hairline top highlight is
       added — on a dark UI most of the perceived lift comes from a lit
       top edge, not from the shadow underneath, which is why dark-mode
       elevation looks wrong when it's built from shadows alone. */
    --shadow:inset 0 1px 0 rgba(255,255,255,.05), 0 1px 2px rgba(0,0,0,.4), 0 12px 32px -16px rgba(0,0,0,.72);
    --shadow-lift:inset 0 1px 0 rgba(255,255,255,.07), 0 4px 10px rgba(0,0,0,.5), 0 28px 56px -22px rgba(0,0,0,.85);
    /* The cobalt glow keeps its hue — it reads as the brand accent, not as
       depth — but brightens, because the light-mode value is tuned to sit
       against white and vanishes against near-black. */
    --shadow-glow:inset 0 1px 0 rgba(255,255,255,.08), 0 4px 10px rgba(39,67,200,.3), 0 24px 48px -20px rgba(39,67,200,.6);
  }
}

/* Forced dark, regardless of OS preference — same values as the media
   block above, kept as an explicit duplicate rather than trying to make
   the media query also match on the attribute. Positioned after the media
   block so it wins on source order for the one case where both could
   apply (OS already prefers dark AND the visitor also forced it), and is
   the only rule that applies when the OS prefers light but dark was
   forced anyway. Exact pattern practice-foundation's globals.css already
   uses for the same reason — see its own comment there. */
:root[data-theme="dark"]{
  --ink:#F1F2EE; --muted:#A8B3C6;
  --paper:#12141A; --card:#1B1F27; --card-2:#242A35;
  --line:#333B4C;
  --sage:#7FBE86;
  --cobalt:#7C93FF; --cobalt-dark:#6B82F0; --cobalt-wash:#1B2340;
  --sage-wash:#1E2B22;
  --bad:#F0A6A0;
  --toggle-x:1;
  --shadow:inset 0 1px 0 rgba(255,255,255,.05), 0 1px 2px rgba(0,0,0,.4), 0 12px 32px -16px rgba(0,0,0,.72);
  --shadow-lift:inset 0 1px 0 rgba(255,255,255,.07), 0 4px 10px rgba(0,0,0,.5), 0 28px 56px -22px rgba(0,0,0,.85);
  --shadow-glow:inset 0 1px 0 rgba(255,255,255,.08), 0 4px 10px rgba(39,67,200,.3), 0 24px 48px -20px rgba(39,67,200,.6);
}
:root[data-theme="light"],:root[data-theme="dark"]{color-scheme:light dark}
:root[data-theme="light"]{color-scheme:light}
:root[data-theme="dark"]{color-scheme:dark}
:root[data-theme="light"]{--toggle-x:0}
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;color-scheme:light dark}
body{
  margin:0;background:var(--paper);color:var(--ink);font-family:var(--body);
  font-size:clamp(1.0625rem,.35vw + 1rem,1.1875rem);line-height:1.65;
  text-rendering:optimizeLegibility;
}
/* overflow-wrap: a heading is normally sized to its container and wraps at
   word boundaries, but a single long unbroken token (a hyphen-free town
   name, a long practitioner name, a stray URL) has no break opportunity and
   pokes past the container instead of wrapping — invisibly, since the
   heading's own box stays the container's width and only the inline text
   run itself overflows. Confirmed via practice-demo.html's placeholder
   heading at 320px: {{PRACTITIONER_NAME}} alone rendered 27px past the
   viewport with no page-level scrollbar to reveal it. break-word only
   breaks when there's no other option, so normal short headings are
   unaffected. */
h1,h2,h3,h4{font-family:var(--display);font-weight:700;letter-spacing:-0.025em;line-height:1.12;margin:0;text-wrap:balance;overflow-wrap:break-word}
h1{font-size:clamp(2.05rem,5vw,3.4rem)}
h2{font-size:clamp(1.55rem,3.2vw,2.3rem)}
h3{font-size:clamp(1.12rem,1.5vw,1.3rem);letter-spacing:-0.012em}
h4{font-size:1rem;letter-spacing:0}
p,ul,ol,dl{margin:0 0 1.1rem}
p,li{max-width:var(--measure)}
a{color:var(--cobalt);text-underline-offset:.18em;text-decoration-thickness:from-font}
a:hover{text-decoration-thickness:2px}
hr{border:0;border-top:1px solid var(--line);margin:0}
:focus-visible{outline:3px solid var(--cobalt);outline-offset:3px;border-radius:2px}
.on-dark :focus-visible{outline-color:var(--cobalt-light)}
.vh{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
[hidden]{display:none !important}
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.001ms !important;animation-iteration-count:1 !important;transition-duration:.001ms !important}
}

/* ---------- layout ---------- */
.wrap{width:100%;max-width:74rem;margin:0 auto;padding-inline:var(--pad)}
.wrap--narrow{max-width:56rem}
.band{padding-block:var(--band)}
.band--tight{padding-block:clamp(1.75rem,4vw,2.75rem)}
.band--dark{background:var(--void);color:var(--paper-band);position:relative;isolation:isolate;overflow:hidden}
.band--dark::before{
  content:"";position:absolute;inset:0;background:var(--grad-mesh);opacity:.55;z-index:var(--z-below);
  pointer-events:none;will-change:transform;
}
@media (prefers-reduced-motion:no-preference){
  .band--dark::before{animation:mesh-drift 22s ease-in-out infinite alternate}
}
@keyframes mesh-drift{
  0%{transform:translate3d(0,0,0) scale(1)}
  100%{transform:translate3d(-1.5%,1.5%,0) scale(1.06)}
}
[data-motion="paused"] .band--dark::before{animation-play-state:paused !important}
.band--dark h1,.band--dark h2,.band--dark h3,.band--dark h4{color:#fff}
.band--dark a{color:var(--cobalt-light)}
.band--wash{background:var(--card-2)}
.band + .band:not(.band--dark){border-top:1px solid var(--line)}
.split{display:grid;gap:clamp(1.5rem,4vw,3.25rem)}
@media (min-width:52rem){.split{grid-template-columns:minmax(0,19rem) minmax(0,1fr)}}
.grid{display:grid;gap:var(--gap)}
@media (min-width:46rem){
  .grid--2{grid-template-columns:repeat(2,1fr)}
  .grid--3{grid-template-columns:repeat(3,1fr)}
}
/* For a card count that doesn't divide evenly into a fixed column count
   (five demo cards, not three or six) — auto-fit wraps to however many
   270px-minimum columns actually fit, rather than forcing a fixed column
   count that would either cramp five cards into three columns or leave
   an oddly empty cell. Used by /demos/ instead of .grid--3. */
@media (min-width:46rem){
  .grid--auto{grid-template-columns:repeat(auto-fit,minmax(17rem,1fr))}
}
/* auto-fit still wraps to two columns for a stretch of viewport widths
   (roughly 46–59rem) before three fit, which leaves the fifth card alone
   in its own row — flush left, with a bare gap beside it where a sixth
   card would go. Centring that lone card and matching its width to a
   single column reads as a deliberate closing card instead of an
   incomplete row. Below 46rem it's already full-width in the 1-column
   stack, so this only needs to apply in that in-between band; once three
   columns fit, the last two cards pair up naturally and no longer need
   the treatment. */
@media (min-width:46rem) and (max-width:58.99rem){
  .grid--auto > .card:last-child:nth-child(odd){
    grid-column:1 / -1;max-width:24rem;margin-inline:auto;
  }
}
/* Cards sitting side by side in a comparison grid align their closing
   action to a common baseline, instead of each button floating wherever
   its own card's copy happens to stop. Grid already equalises the card
   HEIGHTS; without this the contents still sat top-aligned inside them,
   so on /demos/ the "Open …" buttons landed at different heights purely
   because one card's description runs a line longer — which reads as
   unrelated blocks rather than options being compared side by side.
   `margin-top:auto` on the trailing action is what pins them level.
   Scoped to the last .btn-row in a card so a card with mid-body actions
   is unaffected. */
.grid > .card{display:flex;flex-direction:column}
.grid > .card > .btn-row:last-child{margin-top:auto;padding-top:.35rem}
.eyebrow{
  display:block;font-family:var(--mono);font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;
  color:var(--muted);margin:0 0 .85rem;max-width:none;
}
.band--dark .eyebrow{color:var(--sage-light)}
/* .gate is a solid --void panel (the guide's email-capture card) but carries
   neither .band--dark nor .on-dark, so its eyebrow and links were still
   using the light-surface palette: 2.16:1 for the "Free download" label and
   2.35:1 for the privacy-notice link, both against near-black. Scoped to
   .gate rather than folded into a broader .on-dark rule on purpose — the
   masthead and footer also carry .on-dark and already set their own link
   colours, and widening this would have quietly changed both. */
.gate .eyebrow{color:var(--sage-light)}
.gate a{color:var(--cobalt-light)}
.lede{font-size:clamp(1.14rem,1.35vw,1.32rem);line-height:1.55;max-width:52ch}
.small{font-size:.9rem;line-height:1.55;color:var(--muted)}
.band--dark .small,.on-dark .small{color:var(--muted-light)}
/* .on-dark, not just .band--dark: the footer is an always-dark surface
   carrying the same marker class, and a `.small` inside it was landing at
   2.33:1 (#4a5260 on #12141a) on /layouts/ — invisible to every check
   because the studio site has no automated a11y gate of its own. Scoping
   to the marker class covers every current and future always-dark surface
   rather than the one page axe happened to be pointed at, which is the
   same reasoning as practice-foundation's .bg-deep-band .text-muted fix. */
.stack > * + *{margin-top:1.1rem}

/* ---------- skip + header ---------- */
.skip{position:absolute;left:-9999px;background:var(--void);color:#fff;padding:.75rem 1rem;z-index:var(--z-top);border-radius:var(--r)}
.skip:focus{left:.5rem;top:.5rem}
.masthead{
  position:sticky;top:0;z-index:var(--z-sticky);color:var(--paper-band);
  /* Was rgba(18,20,26,.78) (--void at 78%) with saturate(1.5) — at that
     opacity, enough of whatever sits behind the bar (page content, the
     hero's own mesh gradient, a lighter card on scroll) blended through,
     and the saturate boost exaggerated it further, that the bar read as
     a washed, slightly-off grey rather than the deliberate --void navy
     used everywhere else on this dark chrome (.band--dark, the footer).
     Raised to 94% and dropped the saturate boost so the bar reads as a
     confident, near-solid dark navy that actually matches the rest of
     the palette; kept a touch of blur and the last sliver of
     transparency for the same soft "frosted" texture, not a hard cut. */
  background:rgba(18,20,26,.94);border-bottom:1px solid rgba(255,255,255,.08);
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);
  transition:background .25s var(--ease);
}
@supports not (backdrop-filter:blur(1px)){.masthead{background:var(--void)}}
.masthead .wrap{display:flex;align-items:center;gap:.75rem 1.25rem;padding-block:.7rem;flex-wrap:wrap}
.wordmark{
  font-family:var(--display);font-weight:700;letter-spacing:-.03em;font-size:1.08rem;color:#fff;
  text-decoration:none;margin-right:auto;display:flex;align-items:center;gap:.55rem;
}
.wordmark svg{width:22px;height:22px;flex:none}
.nav-toggle{
  display:inline-flex;align-items:center;gap:.5rem;background:transparent;color:var(--paper-band);
  border:1px solid var(--line-dark);border-radius:var(--r-pill);padding:.45rem .85rem;
  font:inherit;font-family:var(--display);font-size:.9rem;cursor:pointer;
}
.nav-toggle:hover{background:var(--void-2)}
.nav-toggle svg{width:14px;height:14px}
.nav{display:flex;flex-wrap:wrap;gap:.2rem;list-style:none;margin:0;padding:0}
.nav a{
  display:block;padding:.4rem .7rem;color:var(--paper-band);text-decoration:none;border-radius:var(--r-pill);
  font-family:var(--display);font-size:.9rem;font-weight:500;
}
.nav a:hover{background:var(--void-2);text-decoration:underline}
.nav a[aria-current="page"]{background:var(--sage-light);color:var(--void);font-weight:700}
.masthead .btn{padding:.45rem .95rem;font-size:.88rem}

/* ---------- theme toggle ----------
   A pill switch, not a single icon button that silently swaps glyphs — the
   track itself (sun fixed left, moon fixed right, thumb sliding between)
   shows both the current state AND the other option at a glance, which is
   the point of "easy adjustable": a visitor never has to click it once
   just to find out what it does.

   Thumb position is driven by --toggle-x (0 = light, 1 = dark), which is
   set inside the SAME cascade that already drives every other colour on
   the page — the OS-preference media query and the [data-theme] override
   — not by a separate JS-only code path. That means the switch is in the
   correct position on first paint with zero JavaScript, the same
   zero-flash guarantee the rest of the theme system already has. JS (see
   assets/site.js) only has two jobs: flip data-theme + localStorage on
   click, and keep aria-pressed/aria-label correct for assistive tech,
   since those two can't be derived from CSS alone. */
.theme-toggle{
  --tt-w:2.9rem; --tt-h:1.6rem; --tt-pad:.18rem;
  position:relative;display:inline-flex;align-items:center;flex:none;
  width:var(--tt-w);height:var(--tt-h);padding:0 var(--tt-pad);
  border:1px solid var(--line-dark);border-radius:var(--r-pill);
  background:rgba(255,255,255,.04);cursor:pointer;
  transition:border-color .2s var(--ease),background .2s var(--ease);
}
.theme-toggle:hover{border-color:rgba(255,255,255,.4);background:rgba(255,255,255,.08)}
.theme-toggle svg{width:.85rem;height:.85rem;flex:none;position:relative;z-index:1}
.theme-toggle__sun{color:#F0B429}
.theme-toggle__moon{color:#C3D2C5;margin-left:auto}
/* Both flanking icons stay at a constant, moderate opacity regardless of
   state — this is what makes it read as "here is the whole control" on
   sight rather than "here is the current state and nothing else". The
   moving thumb underneath is what actually communicates which one is
   active, matching where a Notion/Linear-style switch puts the emphasis. */
.theme-toggle__sun,.theme-toggle__moon{opacity:.85}
.theme-toggle__thumb{
  position:absolute;top:50%;left:var(--tt-pad);
  width:calc(var(--tt-h) - 2 * var(--tt-pad) - 2px);
  height:calc(var(--tt-h) - 2 * var(--tt-pad) - 2px);
  border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.35),0 0 0 1px rgba(0,0,0,.06);
  transform:translate(calc(var(--toggle-x,0) * (var(--tt-w) - var(--tt-h) - 2px)), -50%);
  transition:transform .28s var(--ease);
}
@media (prefers-reduced-motion:reduce){.theme-toggle__thumb{transition:none}}

@media (max-width:60rem){
  /* The mobile nav ships COLLAPSED — `hidden` is in the markup, not added
     by JS on load. It used to be added by JS, which meant the masthead
     painted at full expanded height and then snapped shut a frame later,
     shoving the whole page up: a measured CLS of 0.42 on the home page,
     against a 0.25 "poor" threshold and a publicly refundable 95+ mobile
     guarantee. It got worse every time a nav item was added, because a
     taller expanded nav means a bigger jump.
     Each page carries a <noscript> block that un-hides this, so with JS
     off the nav is a plain visible list and the toggle button (which
     would do nothing) is removed — the no-JS path stays fully navigable.
     That's a <style> not a <script> on purpose: the CSP allows inline
     styles and forbids inline scripts, so this is the one of the two
     that can run at all. */
  #site-nav[hidden]{display:none !important}
  .nav{flex-direction:column;gap:0;padding-block:.4rem}
  .nav a{padding:.6rem .7rem;border-radius:var(--r)}
  /* Far right of the persistent top bar, not the very last thing in the
     header overall: #site-nav has order:5 and flex-basis:100%, forcing it
     onto its own full-width row whenever it's open. Giving the toggle a
     HIGHER order than that would put it below the open mobile menu instead
     of staying reachable next to the hamburger — so it sits right after
     the CTA (order:3) and before the nav's forced wrap (order:5), at 4. */
  /* order:1, right after the wordmark (default order:0) and grouped with
     the hamburger next to it — the toggle used to sit on its own second
     row next to "Get a price", which put the two least-related controls
     in the header (theme preference, primary CTA) next to each other and
     the two most-related (menu toggle, theme toggle — both persistent
     chrome, not content) on separate rows. */
  .masthead .theme-toggle{order:1}
  /* The rule the comment above has described all along, but that was
     never actually written — #site-nav sat at the default order:0 and
     auto flex-basis instead, tied with the wordmark and nav-toggle for
     first row placement. Flex-wrap fills row 1 in item order regardless
     of DOM position once items share an order value, so on a viewport
     narrow enough that the open nav's own natural width didn't fit
     alongside the wordmark, it could wrap onto row 1 ahead of (visually
     above) the wordmark/toggle row instead of dropping to its own row
     below everything else — exactly the "nav floating above the header"
     bug this was meant to prevent. flex-basis:100% forces it wide enough
     that it always needs its own row; order:5 keeps it last regardless
     of where the other items above land. */
  #site-nav{order:5;flex-basis:100%}
  /* "Get a price" moves out of the header row entirely and floats at the
     bottom-right corner instead — persistently visible rather than only
     reachable by scrolling up, and out of the way of the nav/toggle row
     it used to force onto a second line. Not solvable with position:fixed
     scoped to `.masthead .btn--primary` alone: `.masthead` sets
     backdrop-filter (line 274), and per spec any element with a filter/
     backdrop-filter/transform becomes the containing block for its
     position:fixed descendants — so a fixed child of .masthead resolves
     right/bottom against the HEADER's own small box, not the viewport,
     landing pinned near the top of the page instead of the bottom-right
     corner. (Found by actually looking at the rendered page, not just
     reading the CSS — it looks correct in isolation.) site.js moves the
     button to be a direct child of <body> at runtime and adds this class,
     sidestepping the containing-block trap entirely; see its
     "floating Get a price" section. Before that JS runs (or with JS off)
     the button just sits in its normal place in the header — a working
     fallback, not a broken one. --z-docked (see the stacking-order
     comment at the top of this file) is the same layer .mini-cta already
     uses — the two served the same job once "Get a price" is always on
     screen, so .mini-cta is switched off for this breakpoint below
     rather than risk the two overlapping. */
  .btn--primary.header-cta--floating{
    position:fixed;right:1rem;bottom:1rem;z-index:var(--z-docked);
    background:var(--grad-sage);color:#fff;
    box-shadow:0 4px 10px rgba(63,91,69,.18),0 24px 48px -20px rgba(63,91,69,.5);
  }
  /* Redundant now that "Get a price" is a persistent floating button —
     see the comment above. Desktop is unaffected: the header's CTA
     there is inline, not floating, so .mini-cta still earns its keep as
     the only reachable-while-scrolling nudge on that breakpoint. */
  .mini-cta{display:none}
}
@media (min-width:60.01rem){
  .nav-toggle{display:none}
  #site-nav{display:block !important}
  /* At this width the nav renders inline rather than wrapping, so nothing
     stops the toggle sitting after it — default order (0, same as the CTA
     and nav) would keep it in DOM order instead, which is right after the
     wordmark. order:2 puts it past both, genuinely last in the header. */
  .masthead .theme-toggle{order:2}
}

/* ---------- buttons ---------- */
.btn{
  display:inline-flex;align-items:center;gap:.45rem;font-family:var(--display);font-weight:600;font-size:.98rem;
  padding:.75rem 1.35rem;border-radius:var(--r-pill);text-decoration:none;border:2px solid transparent;
  cursor:pointer;transition:transform .25s var(--ease),box-shadow .25s var(--ease),background .25s var(--ease),border-color .25s var(--ease);
  position:relative;
}
.btn:hover{transform:translateY(-2px)}
.btn:active{transform:translateY(0)}
.btn--primary{background:var(--grad-cobalt);color:#fff;box-shadow:var(--shadow-glow)}
.btn--primary:hover{box-shadow:0 6px 14px rgba(39,67,200,.16),0 32px 56px -20px rgba(39,67,200,.48)}
.btn--ghost{border-color:var(--line);color:inherit;background:var(--card)}
.btn--ghost:hover{border-color:var(--cobalt);background:var(--cobalt-wash)}
.band--dark .btn--primary,.masthead .btn--primary{background:var(--grad-sage);color:#fff;box-shadow:0 4px 10px rgba(63,91,69,.18),0 24px 48px -20px rgba(63,91,69,.5)}
.band--dark .btn--primary:hover,.masthead .btn--primary:hover{box-shadow:0 6px 14px rgba(63,91,69,.24),0 32px 56px -20px rgba(63,91,69,.6)}
.band--dark .btn--ghost,.masthead .btn--ghost{border-color:rgba(255,255,255,.25);background:rgba(255,255,255,.04);color:#fff}
.band--dark .btn--ghost:hover,.masthead .btn--ghost:hover{border-color:rgba(255,255,255,.5);background:rgba(255,255,255,.09)}
.btn-row{display:flex;flex-wrap:wrap;gap:.7rem;margin-top:1.6rem}
.btn-row + .btn-note{margin-top:.85rem}
.btn-note{font-size:.85rem;color:var(--muted);max-width:46ch}
.band--dark .btn-note{color:var(--muted-light)}

/* ---------- hero ---------- */
.hero{padding-block:clamp(2.75rem,7.5vw,5.5rem)}
.hero-grid{display:grid;gap:clamp(2rem,4.5vw,3.25rem);align-items:center}
@media (min-width:58rem){.hero-grid{grid-template-columns:minmax(0,1.08fr) minmax(0,.92fr)}}
.hero h1{max-width:17ch}
.hero .lede{margin-top:1.25rem;color:var(--paper-band)}
.proof-strip{
  margin:2.4rem 0 0;padding-top:1.25rem;border-top:1px solid var(--line-dark);
  list-style:none;display:flex;flex-wrap:wrap;gap:.4rem .5rem;
}
.proof-strip li{
  max-width:none;font-family:var(--mono);font-size:.74rem;letter-spacing:.03em;color:var(--paper-band);
  background:var(--void-2);border:1px solid var(--line-dark);border-radius:var(--r-pill);
  padding:.3rem .8rem;display:flex;align-items:center;gap:.45rem;
}
.proof-strip li::before{content:"";width:.36rem;height:.36rem;border-radius:50%;background:var(--sage-light);flex:none}

/* ---------- signature diagram ---------- */
.diagram{
  background:rgba(27,31,39,.6);border:1px solid rgba(255,255,255,.1);border-radius:var(--r-lg);padding:1.1rem;
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);box-shadow:var(--shadow-lift);
}
@supports not (backdrop-filter:blur(1px)){.diagram{background:var(--void-2)}}
.diagram svg{display:block;width:100%;height:auto}
.diagram figcaption{
  margin-top:.85rem;font-family:var(--mono);font-size:.72rem;line-height:1.7;color:var(--muted-light);
  letter-spacing:.02em;max-width:none;
}
.d-node{fill:var(--void-3);stroke:#4A5568;stroke-width:1}
.d-node--ours{fill:#1d2b25;stroke:var(--sage);stroke-width:1.5}
.d-label{font-family:var(--display);font-size:11px;font-weight:600;fill:#fff}
.d-sub{font-family:var(--mono);font-size:9px;fill:var(--muted-light)}
.d-leak{fill:none;stroke:#E4A6A6;stroke-width:1.6;stroke-dasharray:5 4}
.d-safe{fill:none;stroke:var(--sage-light);stroke-width:2}
.d-leak-label{font-family:var(--mono);font-size:9px;fill:#E4A6A6}
/* The diagram itself holds still — no infinite loop, so it needs no pause
   control — but its connection lines draw in once, in sequence, on paint.
   This is a single, non-repeating entrance, not the kind of ongoing
   motion WCAG 2.2.2 requires a stop control for. Runs unconditionally
   rather than gated behind ".diagram.in" (an IntersectionObserver class
   that only ever got added once the diagram scrolled into view) for the
   same reliability reason as the scroll-reveal rule above — see its
   comment. */
@media (prefers-reduced-motion:no-preference){
  .d-draw{animation:d-fade-in .5s var(--ease) both}
}
@keyframes d-fade-in{from{opacity:0}to{opacity:1}}

/* ---------- hero entrance ---------- */
@media (prefers-reduced-motion:no-preference){
  [data-hero-enter]{opacity:0;transform:translateY(14px);transition:opacity .55s var(--ease),transform .55s var(--ease)}
  [data-hero-enter].go{opacity:1;transform:none}
}

/* ---------- cards ---------- */
.card{
  background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);
  padding:clamp(1.25rem,2.6vw,1.75rem);
  transition:box-shadow .3s var(--ease),transform .3s var(--ease),border-color .3s var(--ease);
}
.card h3{margin-bottom:.5rem}
.card p:last-child{margin-bottom:0}
.card--dark{background:var(--void-2);border-color:var(--line-dark);color:var(--paper-band)}
.grid--2 > .card:hover,.grid--3 > .card:hover,.grid--auto > .card:hover{box-shadow:var(--shadow-lift);transform:translateY(-3px);border-color:var(--muted-light)}
a.card{display:block;text-decoration:none;color:inherit}
a.card:hover{box-shadow:var(--shadow-glow);transform:translateY(-3px);border-color:var(--cobalt-light)}
a.card h3{color:var(--cobalt);text-decoration:underline;text-underline-offset:.2em}

/* ---------- ledger ---------- */
.ledger{border-top:1px solid var(--line);margin:0}
.ledger > div{border-bottom:1px solid var(--line);padding:.95rem 0;display:grid;gap:.2rem .75rem}
@media (min-width:44rem){.ledger > div{grid-template-columns:minmax(0,1fr) minmax(0,16rem);align-items:baseline}}
.ledger dt{font-family:var(--display);font-weight:600;font-size:1.02rem;line-height:1.35;margin:0}
.ledger dd{margin:0;font-family:var(--mono);font-size:.8rem;line-height:1.5;color:var(--sage)}
.ledger dd::before{content:"if not → ";color:var(--muted)}
/* /work/'s "Six choices, and why each one" ledger reuses this component for
   decision→reasoning pairs, not guarantee→consequence — dd.plain marks
   those so the "if not →" prefix (correct on the guarantee ledgers on
   Home, Pricing and Continuity) doesn't get prepended to a reason,
   rendering nonsense like "if not → a website form is the wrong place for
   health data". The class existed in the markup with no rule behind it,
   so the prefix was appearing anyway. */
.ledger dd.plain::before{content:none}
.band--dark .ledger,.band--dark .ledger > div{border-color:var(--line-dark)}
.band--dark .ledger dt{color:#fff}
.band--dark .ledger dd{color:var(--sage-light)}
.band--dark .ledger dd::before{color:var(--muted-light)}

/* ---------- plans ---------- */
.plans{display:grid;gap:1.15rem;margin-top:2rem}
@media (min-width:40rem){.plans{grid-template-columns:repeat(2,1fr)}}
@media (min-width:72rem){.plans{grid-template-columns:repeat(4,1fr)}}
@media (min-width:56rem){.plans--3{grid-template-columns:repeat(3,1fr)}}
@media (min-width:72rem){.plans--3{grid-template-columns:repeat(3,1fr)}}
/* Same problem as .grid--auto's fifth card (see its comment), one page
   over: between 40rem (.plans goes two columns) and 56rem (.plans--3
   goes three) the third plan — Clinic — is alone in its own row, flush
   left with a bare gap where a fourth plan would sit. Centring it and
   matching its width to a single column reads as intentional. */
@media (min-width:40rem) and (max-width:55.99rem){
  .plans--3 > .plan:last-child:nth-child(odd){
    grid-column:1 / -1;max-width:24rem;margin-inline:auto;
  }
}
.plan{
  background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);
  padding:1.5rem 1.35rem 1.6rem;display:flex;flex-direction:column;position:relative;
  overflow:hidden;transition:box-shadow .3s var(--ease),transform .3s var(--ease),border-color .3s var(--ease);
}
.plan::before{content:"";position:absolute;top:0;left:0;right:0;height:3px;background:var(--line);transition:background .3s var(--ease)}
.plan:hover{box-shadow:var(--shadow-lift);transform:translateY(-3px);border-color:var(--muted-light)}
.plan--flag{border-color:var(--cobalt);box-shadow:var(--shadow-glow)}
.plan--flag::before{background:var(--grad-cobalt)}
.plan--flag:hover{transform:translateY(-3px)}
.plan__tag{
  position:absolute;top:1.1rem;right:1.1rem;background:var(--grad-cobalt);color:#fff;
  font-family:var(--mono);font-size:.66rem;letter-spacing:.1em;text-transform:uppercase;
  padding:.28rem .7rem;border-radius:var(--r-pill);box-shadow:0 4px 10px rgba(39,67,200,.28);
}
.plan h3{margin-bottom:.15rem}
.plan__for{font-size:.86rem;color:var(--muted);margin:0 0 .9rem;max-width:none}
.plan__price{font-family:var(--display);font-weight:700;font-size:1.22rem;letter-spacing:-.02em;line-height:1.3;margin:0 0 1rem;max-width:none}
.plan__price span{display:block;font-family:var(--body);font-weight:400;font-size:.86rem;color:var(--muted);margin-top:.2rem}
.plan ul{list-style:none;padding:0;margin:0 0 1.15rem;font-size:.94rem}
.plan li{padding:.4rem 0 .4rem 1.45rem;border-bottom:1px solid var(--line);position:relative;max-width:none;line-height:1.45}
.plan li::before{position:absolute;left:0;top:.4rem;font-family:var(--mono);font-size:.82rem}
.plan li.yes::before{content:"✓";color:var(--sage)}
.plan li.no::before{content:"✕";color:var(--muted)}
.plan li.no{color:var(--muted)}
.plan__totals{margin-top:auto;padding-top:.9rem;border-top:2px solid var(--ink);font-family:var(--mono);font-size:.8rem;line-height:1.7;max-width:none}
.plan__totals b{font-size:.94rem}
.plan__save{display:inline-block;margin-left:.5rem;padding:.05rem .45rem;border-radius:1rem;background:var(--sage-wash);color:var(--sage);font-family:var(--body);font-size:.72rem;font-weight:600;vertical-align:middle}
.plan .btn{margin-top:1rem;justify-content:center}

/* ---------- tables ---------- */
.tablewrap{overflow-x:auto;border:1px solid var(--line);border-radius:var(--r-lg);background:var(--card)}
table{border-collapse:collapse;width:100%;font-family:var(--mono);font-size:.85rem;margin:0}
caption{text-align:left;padding:.85rem 1rem;font-family:var(--display);font-weight:600;font-size:.95rem;border-bottom:1px solid var(--line)}
th,td{text-align:left;padding:.7rem 1rem;border-bottom:1px solid var(--line);white-space:nowrap}
tbody tr:last-child th,tbody tr:last-child td{border-bottom:0}
thead th{font-weight:700;background:var(--card-2)}
tbody th{font-weight:500}
td.win{color:var(--sage);font-weight:700}

/* ---------- chooser ---------- */
.chooser{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);padding:clamp(1.2rem,3vw,2rem)}
.chooser fieldset{border:0;padding:0;margin:0 0 1.5rem}
.chooser legend{font-family:var(--display);font-weight:600;font-size:1.02rem;padding:0;margin-bottom:.65rem}
.opts{display:grid;gap:.5rem}
@media (min-width:34rem){.opts--2{grid-template-columns:repeat(2,1fr)}}
.opt{
  display:flex;align-items:flex-start;gap:.6rem;padding:.75rem .9rem;border:1px solid var(--line);
  border-radius:var(--r);background:var(--card-2);cursor:pointer;font-size:.95rem;line-height:1.45;
  transition:border-color .2s var(--ease),background .2s var(--ease),box-shadow .2s var(--ease),transform .2s var(--ease);
}
.opt:hover{border-color:var(--cobalt);transform:translateY(-1px)}
.opt input{margin:.25rem 0 0;accent-color:var(--cobalt);flex:none;width:1.05rem;height:1.05rem}
.opt:has(input:checked){border-color:var(--cobalt);background:var(--cobalt-wash);box-shadow:inset 0 0 0 1.5px var(--cobalt)}
.opt span{max-width:none}
.opt small{display:block;color:var(--muted);font-size:.83rem}
.result{
  margin-top:1.25rem;padding:1.15rem 1.25rem;border-radius:var(--r-lg);
  background:var(--sage-wash);border:1px solid var(--sage);
}
.result h3{margin-bottom:.4rem}
.result p{margin-bottom:.55rem}
.result p:last-child{margin-bottom:0}
.result .figures{font-family:var(--mono);font-size:.88rem;line-height:1.8}

/* ---------- faq ---------- */
details.qa{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);margin-bottom:.7rem;overflow:hidden;transition:border-color .25s var(--ease),box-shadow .25s var(--ease)}
details.qa[open]{box-shadow:var(--shadow-lift);border-color:var(--muted-light)}
details.qa > summary{
  cursor:pointer;list-style:none;padding:1rem 1.2rem;font-family:var(--display);font-weight:600;
  display:flex;justify-content:space-between;align-items:center;gap:1rem;transition:background .2s var(--ease);
}
details.qa > summary::-webkit-details-marker{display:none}
details.qa > summary::after{
  content:"+";font-family:var(--mono);font-size:1.15rem;color:var(--cobalt);flex:none;
  display:inline-grid;place-items:center;width:1.6rem;height:1.6rem;border-radius:50%;background:var(--cobalt-wash);
  transition:transform .3s var(--ease);
}
details.qa[open] > summary::after{transform:rotate(135deg)}
details.qa > summary:hover{background:var(--card-2)}
.qa__body{padding:0 1.15rem 1.1rem}
.qa__body p:last-child{margin-bottom:0}

/* ---------- steps ---------- */
.steps{list-style:none;counter-reset:s;padding:0;margin:2rem 0 0}
.steps > li{
  counter-increment:s;position:relative;padding:0 0 1.7rem 3.1rem;margin:0 0 0 1rem;max-width:none;
  border-left:1px solid var(--line);
}
.steps > li:last-child{border-left-color:transparent;padding-bottom:0}
.steps > li::before{
  content:counter(s,decimal-leading-zero);position:absolute;left:-1rem;top:-.15rem;
  width:2.1rem;height:2.1rem;display:grid;place-items:center;background:var(--grad-cobalt);color:#fff;
  font-family:var(--mono);font-size:.76rem;border-radius:50%;box-shadow:0 4px 10px rgba(39,67,200,.28);
}
.steps h3,.steps h4{margin-bottom:.3rem}
.steps p{margin-bottom:0}
.steps .cost{font-family:var(--mono);font-size:.74rem;color:var(--sage);letter-spacing:.04em;display:block;margin-top:.4rem}

/* ---------- misc ---------- */
.note{border-left:3px solid var(--sage);padding:.1rem 0 .1rem 1.05rem;margin:0 0 1.15rem}
.note--warn{border-left-color:var(--muted)}
.checklist{list-style:none;padding:0;margin:0 0 1.15rem}
.checklist li{padding:.55rem 0 .55rem 1.55rem;position:relative;border-bottom:1px solid var(--line)}
.checklist li::before{content:"✕";position:absolute;left:0;top:.55rem;font-family:var(--mono);color:var(--muted)}
.checklist--yes li::before{content:"✓";color:var(--sage)}
.band--dark .checklist li{border-color:var(--line-dark)}
.band--dark .checklist li::before{color:var(--muted-light)}
.band--dark .checklist--yes li::before{color:var(--sage-light)}
.stat{font-family:var(--display);font-weight:700;font-size:clamp(1.9rem,4vw,2.6rem);letter-spacing:-.03em;line-height:1;display:block;margin-bottom:.35rem}
.band--dark .stat{color:#fff}
.source{font-family:var(--mono);font-size:.72rem;color:var(--muted);letter-spacing:.02em}
.band--dark .source{color:var(--muted-light)}

/* ---------- footer ---------- */
.foot{background:var(--void);color:var(--muted-light);padding-block:clamp(2.25rem,5vw,3.25rem);font-size:.92rem}
.foot h2{font-family:var(--display);font-size:.76rem;letter-spacing:.14em;text-transform:uppercase;color:var(--sage-light);margin:0 0 .7rem}
.foot a{color:#fff}
.foot ul{list-style:none;padding:0;margin:0}
.foot li{max-width:none}
/* Padding lives on the link itself, not the <li> — the same fix already
   made across all five practice forks (see docs/launch-history.md): padding
   on the wrapper doesn't enlarge the actual clickable target, so the link's
   own hit area was riding on line-height alone to clear the WCAG 2.2
   24×24px minimum (axe's target-size rule) with no margin for error. */
.foot li a{display:inline-block;padding:.2rem 0}
.foot .cols{display:grid;gap:2rem}
@media (min-width:48rem){.foot .cols{grid-template-columns:1fr 1.3fr}}
.foot .fineprint{margin-top:2.25rem;padding-top:1.2rem;border-top:1px solid var(--line-dark);font-family:var(--mono);font-size:.74rem;line-height:1.85;letter-spacing:.02em}
.foot .fineprint p{max-width:none;margin:0}


/* ============ explainers ============
   Six animated figures. Rules they all follow:
   1. The FINAL frame tells the whole story on its own, so a reader with
      reduced-motion switched on loses nothing.
   2. Motion is contained inside the figure. Nothing moves under body text.
   3. A visible control pauses every animation on the page (WCAG 2.2.2).
   ==================================== */
.ex{margin:0;background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);padding:1rem 1rem 1.1rem;display:flex;flex-direction:column}
.ex--dark{background:var(--void-2);border-color:var(--line-dark);color:var(--paper-band)}
.ex svg{display:block;width:100%;height:auto}
.ex figcaption{margin-top:.9rem}
.ex figcaption b{display:block;font-family:var(--display);font-size:1.05rem;letter-spacing:-.015em;margin-bottom:.3rem}
.ex figcaption span{display:block;font-size:.95rem;line-height:1.55;color:var(--muted);max-width:46ch}
.ex--dark figcaption b{color:#fff}
.ex--dark figcaption span{color:var(--muted-light)}

.ex-t{font-family:var(--display);font-size:12px;font-weight:600;fill:var(--ink)}
.ex-t--sm{font-family:ui-monospace,monospace;font-size:9.5px;fill:var(--muted);font-weight:400}
.ex--dark .ex-t{fill:#fff}
.ex--dark .ex-t--sm{fill:var(--muted-light)}
/* Was hardcoded to the light-mode hex regardless of colour scheme, while
   .ex-t's fill uses var(--ink) — which correctly flips near-white in dark
   mode for its normal job as page text. Together: near-white text on a
   near-white box in OS dark mode, ~1.05:1, on the one diagram (pricing's
   "how can this cost so little") that never got the explicit .ex--dark
   treatment index.html's diagram has, because it sits inside .band--wash,
   whose background already tracks colour scheme via --card-2 — the SVG
   fills just weren't tracking it too. Swapped to var(--card-2)/var(--line),
   the same pair .ex--dark's own values already duplicate by hand, so this
   now adapts automatically and .ex--dark's override becomes redundant but
   harmless for the one diagram that wants to stay dark regardless of
   scheme (index.html's, sitting on an always-dark band). */
.ex-box{fill:var(--card-2);stroke:var(--line);stroke-width:1.5}
.ex--dark .ex-box{fill:#242A35;stroke:#4A5568}
.ex-good{fill:none;stroke:var(--sage);stroke-width:2.5;stroke-linecap:round}
.ex-bad{fill:none;stroke:var(--bad);stroke-width:2.5;stroke-linecap:round}
.ex--dark .ex-bad{stroke:#F0A6A0}
.ex-dash{fill:none;stroke:var(--bad);stroke-width:1.8;stroke-dasharray:5 4}
.ex--dark .ex-dash{stroke:#F0A6A0}
.ex--dark .ex-good{stroke:#C3D2C5}
.ex-fill-good{fill:var(--sage)}
.ex-fill-bad{fill:var(--bad)}
.ex--dark .ex-fill-bad{fill:#F0A6A0}
.ex-lbl-bad{font-family:ui-monospace,monospace;font-size:9.5px;fill:var(--bad)}
.ex--dark .ex-lbl-bad{fill:#F0A6A0}
.ex-lbl-good{font-family:ui-monospace,monospace;font-size:9.5px;fill:var(--sage)}
.ex--dark .ex-lbl-good{fill:#C3D2C5}

/* motion — only when the reader has not asked us not to, and pausable */
@media (prefers-reduced-motion:no-preference){
  .ex .anim{animation-duration:6s;animation-iteration-count:infinite;animation-timing-function:linear}
  .ex .a-send1{animation-name:a-send1}
  .ex .a-send2{animation-name:a-send2}
  .ex .a-alert{animation-name:a-alert}
  .ex .a-packet{animation-name:a-packet}
  .ex .a-spin{animation-name:a-spin;animation-duration:1.4s}
  .ex .a-wait{animation-name:a-wait}
  .ex .a-tap{animation-name:a-tap}
  .ex .a-strike{animation-name:a-strike}
  .ex .a-swap-in{animation-name:a-swap-in}
  .ex .a-step{animation-name:a-step;animation-duration:7s}
  .ex .a-grow{animation-name:a-grow;animation-duration:7s}
}
[data-motion="paused"] .ex *{animation-play-state:paused !important}
@keyframes a-send1{
  0%{transform:translateX(0);opacity:0}
  6%{opacity:1}
  46%{transform:translateX(112px);opacity:1}
  56%{transform:translateX(118px) translateY(14px);opacity:0}
  100%{transform:translateX(118px) translateY(14px);opacity:0}
}
@keyframes a-send2{
  0%{transform:translateX(0);opacity:0}
  6%{opacity:1}
  44%{transform:translateX(112px);opacity:1}
  52%{transform:translateX(118px);opacity:0}
  100%{transform:translateX(118px);opacity:0}
}
@keyframes a-alert{0%,50%{opacity:0;transform:scale(.7)}58%{opacity:1;transform:scale(1.08)}64%,100%{opacity:1;transform:scale(1)}}
@keyframes a-packet{0%{transform:translateX(0);opacity:0}10%{opacity:1}90%{opacity:1}100%{transform:translateX(126px);opacity:0}}
@keyframes a-spin{to{transform:rotate(360deg)}}
@keyframes a-wait{0%,55%{opacity:0}65%,100%{opacity:1}}
@keyframes a-tap{0%,20%{opacity:0;transform:scale(.4)}30%{opacity:.65;transform:scale(1)}45%,100%{opacity:0;transform:scale(1.5)}}
@keyframes a-strike{0%,15%{transform:scaleX(0)}35%,100%{transform:scaleX(1)}}
@keyframes a-swap-in{0%,40%{opacity:0;transform:translateY(6px)}55%,100%{opacity:1;transform:none}}
@keyframes a-step{0%,8%{opacity:0}16%,100%{opacity:1}}
@keyframes a-grow{0%{transform:scaleX(0)}45%,100%{transform:scaleX(1)}}

.motion-toggle{
  display:inline-flex;align-items:center;gap:.5rem;background:var(--card);color:var(--ink);
  border:1px solid var(--line);border-radius:var(--r-pill);padding:.42rem .9rem;
  font-family:var(--display);font-size:.86rem;font-weight:600;cursor:pointer;
}
.motion-toggle:hover{border-color:var(--cobalt)}
.motion-toggle svg{width:13px;height:13px}

/* ---------- guide gate ---------- */
.gate{
  background:var(--void);color:var(--paper-band);border-radius:var(--r-xl);
  padding:clamp(1.4rem,3.5vw,2.25rem);display:grid;gap:1.75rem;
  border:1px solid var(--line-dark);position:relative;isolation:isolate;overflow:hidden;
}
.gate::before{content:"";position:absolute;inset:0;background:var(--grad-mesh);opacity:.5;z-index:var(--z-below);pointer-events:none;will-change:transform}
@media (prefers-reduced-motion:no-preference){.gate::before{animation:mesh-drift 22s ease-in-out infinite alternate}}
[data-motion="paused"] .gate::before{animation-play-state:paused !important}
@media (min-width:52rem){.gate{grid-template-columns:minmax(0,1.1fr) minmax(0,.9fr);align-items:center}}
.gate h2,.gate h3{color:#fff}
.gate .lede{color:var(--muted-light)}
.gate-cover{
  background:rgba(27,31,39,.6);border:1px solid rgba(255,255,255,.12);border-radius:var(--r);
  aspect-ratio:210/297;max-width:15rem;margin:0 auto;position:relative;overflow:hidden;
  box-shadow:var(--shadow-lift);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
  transform:rotate(-2deg);transition:transform .4s var(--ease);
}
.gate-cover:hover{transform:rotate(0deg) scale(1.02)}
.gate-cover svg{display:block;width:100%;height:100%}
.gate-contents{list-style:none;margin:1.35rem 0 0;padding:0;font-size:.86rem}
.gate-contents li{
  max-width:none;padding:.4rem 0;border-bottom:1px solid var(--line-dark);
  display:flex;gap:.6rem;color:var(--muted-light);
}
.gate-contents li b{color:var(--sage-light);font-family:var(--mono);font-size:.78rem;flex:none;width:1.4rem}
.gate-form{margin-top:1.5rem}
.gate-row{display:flex;flex-wrap:wrap;gap:.6rem}
.gate-row input[type="email"]{
  flex:1 1 14rem;font:inherit;font-size:1rem;padding:.75rem .95rem;border-radius:var(--r-pill);
  border:2px solid var(--line-dark);background:var(--void-2);color:#fff;
}
.gate-row input[type="email"]::placeholder{color:var(--muted-light)}
.gate-row input[type="email"]:focus{border-color:var(--sage-light);outline:none}
.gate-hp{position:absolute;left:-9999px}
.gate-consent{margin-top:.7rem;font-size:.82rem;color:var(--muted-light);display:flex;gap:.55rem;align-items:flex-start}
.gate-consent input{margin-top:.28rem;accent-color:var(--sage-light);flex:none}
.gate-error{color:#F0A6A0;font-size:.86rem;margin-top:.6rem;display:none}
.gate-error.show{display:block}
.gate-error-fallback{font-size:.86rem;margin-top:.4rem;display:none}
.gate-error-fallback.show{display:block}
.gate-error-fallback a{color:var(--sage-light)}
.gate-row button[disabled]{opacity:.6;cursor:wait}
.gate-sent{display:none;background:var(--void-2);border:1px solid var(--sage);border-radius:var(--r);padding:1.25rem 1.35rem}
.gate-sent.show{display:block}
.gate-sent h3{margin-bottom:.4rem;font-size:1.05rem}
.gate-sent p{margin-bottom:0;color:var(--muted-light);font-size:.92rem}
.gate-sent .btn{margin-top:1rem}
.gate.sent .gate-form{display:none}
.gate-fine{margin-top:.8rem;font-size:.78rem;color:var(--muted-light)}

/* compact promo card, used on Home */
.guide-promo{
  display:grid;gap:1.25rem;align-items:center;background:var(--void);color:var(--paper-band);
  border-radius:var(--r-lg);padding:clamp(1.25rem,3vw,1.85rem);
  position:relative;isolation:isolate;overflow:hidden;
}
.guide-promo::before{content:"";position:absolute;inset:0;background:var(--grad-mesh);opacity:.45;z-index:var(--z-below);pointer-events:none;will-change:transform}
@media (prefers-reduced-motion:no-preference){.guide-promo::before{animation:mesh-drift 22s ease-in-out infinite alternate}}
[data-motion="paused"] .guide-promo::before{animation-play-state:paused !important}
@media (min-width:44rem){.guide-promo{grid-template-columns:auto 1fr auto}}
.guide-promo .gate-cover{max-width:6.5rem;margin:0;transform:none}
.guide-promo .gate-cover:hover{transform:scale(1.04)}
.guide-promo h3{color:#fff;margin-bottom:.3rem}
.guide-promo p{color:var(--muted-light);margin-bottom:0;font-size:.94rem}

/* ---------- scroll reveal ----------
   Runs automatically on paint via a CSS animation — no IntersectionObserver,
   no JS dependency, no timing race. The previous version gated visibility
   on an `.in` class added only once an element scrolled into view; that's
   the exact failure mode practice-foundation's components/reveal.tsx hit
   and rewrote away from (see its own comment): whatever triggers the
   reveal — a real scroll, a fast/keyboard jump, a bfcache restore, JS
   execution completing at all — can fail to happen, and every element
   gated behind it (.card, .plan, details.qa, .diagram, .ex and more)
   stayed permanently at opacity:0, invisible, with nothing in the markup
   to explain why. animation-fill-mode "both" means the element uses the
   0% frame before the animation starts and the 100% frame after it ends,
   so there's no flash of default styling either side of it. */
@media (prefers-reduced-motion:no-preference){
  [data-reveal]{animation:reveal-in .6s var(--ease) both}
}
@keyframes reveal-in{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}

/* ---------- scroll progress ---------- */
.scroll-progress{position:fixed;top:0;left:0;right:0;height:3px;z-index:var(--z-overlay);background:transparent;pointer-events:none;overflow:hidden}
#scroll-progress-fill{
  position:absolute;top:0;left:0;height:100%;display:block;width:0%;background:var(--grad-cobalt);
  box-shadow:0 0 8px rgba(39,67,200,.5);transition:width .1s linear;
}
@media (prefers-reduced-motion:reduce){#scroll-progress-fill{transition:none}}

/* Route-transition sweep removed: real multi-page navigation now triggers
   the browser's own loading behaviour, so faking one would be redundant. */

/* ---------- sticky mini-CTA ---------- */
.mini-cta{
  position:fixed;left:0;right:0;bottom:0;z-index:var(--z-docked);background:var(--void);color:#fff;
  border-top:1px solid var(--line-dark);transform:translateY(100%);transition:transform .35s var(--ease);
  box-shadow:0 -8px 24px rgba(0,0,0,.2);
}
.mini-cta.show{transform:translateY(0)}
.mini-cta__inner{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.75rem;padding-block:.75rem}
.mini-cta p{margin:0;max-width:none;font-size:.95rem}
.mini-cta strong{color:var(--sage-light)}
@media (prefers-reduced-motion:reduce){.mini-cta{transition:none}}

/* ---------- bento grid ---------- */
.bento{display:grid;gap:var(--gap);grid-template-columns:1fr}
@media (min-width:46rem){
  .bento{grid-template-columns:repeat(6,1fr)}
  .bento > :nth-child(1){grid-column:span 4}
  .bento > :nth-child(2){grid-column:span 2}
  .bento > :nth-child(3){grid-column:span 2}
  .bento > :nth-child(4){grid-column:span 4}
}

/* ---------- magnetic buttons (desktop, fine pointer, motion allowed) ---------- */
@media (hover:hover) and (pointer:fine) and (prefers-reduced-motion:no-preference){
  .btn[data-magnetic]{will-change:transform}
}

/* ---------- 3D tilt cards ---------- */
@media (hover:hover) and (pointer:fine) and (prefers-reduced-motion:no-preference){
  .plan[data-tilt]{transform-style:preserve-3d;transition:transform .12s ease-out,box-shadow .3s var(--ease),border-color .3s var(--ease)}
}

/* ---------- contrast checker widget ---------- */
.contrast-tool{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);padding:clamp(1.2rem,3vw,1.9rem)}
.contrast-tool__grid{display:grid;gap:1.5rem}
@media (min-width:44rem){.contrast-tool__grid{grid-template-columns:1fr 1fr}}
.swatch-field{margin-bottom:1rem}
.swatch-field label{display:block;font-family:var(--display);font-weight:600;font-size:.9rem;margin-bottom:.4rem}
.swatch-row{display:flex;align-items:center;gap:.6rem}
.swatch-row input[type="color"]{
  width:2.6rem;height:2.6rem;border-radius:var(--r);border:1px solid var(--line);padding:2px;
  background:var(--card-2);cursor:pointer;
}
.swatch-row input[type="text"]{
  flex:1;font:inherit;font-family:var(--mono);font-size:.95rem;padding:.6rem .75rem;
  border:2px solid var(--line);border-radius:var(--r);background:var(--card-2);color:var(--ink);
}
.swatch-row input[type="text"]:focus{border-color:var(--cobalt);outline:none}
.contrast-preview{
  border-radius:var(--r-lg);padding:1.75rem 1.5rem;display:flex;flex-direction:column;
  justify-content:center;min-height:9rem;transition:background .15s ease,color .15s ease;
  border:1px solid var(--line);
}
.contrast-preview .big{font-family:var(--display);font-weight:700;font-size:1.4rem;margin:0 0 .3rem}
.contrast-preview .small-sample{font-family:var(--body);font-size:.95rem;margin:0}
.contrast-result{margin-top:1.25rem;display:flex;flex-wrap:wrap;gap:.6rem;align-items:center}
.contrast-ratio{
  font-family:var(--display);font-weight:700;font-size:1.6rem;letter-spacing:-.02em;
}
.contrast-badges{display:flex;flex-wrap:wrap;gap:.5rem}
.c-badge{
  font-family:var(--mono);font-size:.74rem;letter-spacing:.03em;padding:.32rem .7rem;border-radius:var(--r-pill);
  border:1px solid var(--line);
}
.c-badge.pass{background:var(--sage-wash);border-color:var(--sage);color:var(--sage)}
.c-badge.fail{background:#F4E4E2;border-color:#8C2C24;color:#8C2C24}
.contrast-note{margin-top:.9rem}

/* ---------- URL-check preview widget ---------- */
.check-tool{background:var(--void);color:#fff;border-radius:var(--r-lg);padding:clamp(1.2rem,3vw,1.9rem);position:relative;isolation:isolate;overflow:hidden}
/* Same always-dark-surface bug as .gate (see its own comment above): a
   plain `a` inside .check-tool was inheriting var(--cobalt), which is
   correct against a light page background but ~2.4:1 against this panel's
   permanently-dark --void — below the 4.5:1 AA floor. .check-tool carries
   neither .band--dark nor .on-dark, so nothing was fixing it. Was patched
   ad hoc with an inline style="color:#9FB6FF" on the one caption link
   that had one; this makes it a real rule so every link in the widget
   (and any added later) gets it, not just the one someone happened to
   hand-fix. */
.check-tool a{color:var(--cobalt-light)}
.check-tool::before{content:"";position:absolute;inset:0;background:var(--grad-mesh);opacity:.4;z-index:var(--z-below);pointer-events:none;will-change:transform}
@media (prefers-reduced-motion:no-preference){.check-tool::before{animation:mesh-drift 22s ease-in-out infinite alternate}}
[data-motion="paused"] .check-tool::before{animation-play-state:paused !important}
.check-tool form{display:flex;flex-wrap:wrap;gap:.6rem;margin-top:1.1rem}
.check-tool input[type="text"]{
  flex:1 1 14rem;font:inherit;font-size:1rem;padding:.75rem .95rem;border-radius:var(--r-pill);
  border:2px solid var(--line-dark);background:var(--void-2);color:#fff;
}
.check-tool input[type="text"]::placeholder{color:var(--muted-light)}
.check-tool-result{
  display:none;margin-top:1.5rem;background:rgba(27,31,39,.7);border:1px solid var(--line-dark);
  border-radius:var(--r);padding:1.25rem 1.35rem;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
}
.check-tool-result.show{display:block}
.check-tool-result h3{color:#fff;font-size:1rem;margin-bottom:.9rem}
.check-row{display:flex;align-items:flex-start;gap:.6rem;padding:.5rem 0;border-bottom:1px solid var(--line-dark);font-size:.92rem}
.check-row:last-of-type{border-bottom:0}
.check-row svg{width:16px;height:16px;flex:none;margin-top:.15rem}
.check-row.ok svg{color:var(--sage-light)}
.check-row.warn svg{color:#F0A6A0}
.check-tool-caption{
  margin-top:1rem;font-family:var(--mono);font-size:.72rem;letter-spacing:.02em;color:var(--muted-light);
  padding-top:.85rem;border-top:1px solid var(--line-dark);
}

/* ---------- before/after comparison slider ----------
   The 1-4 z-indexes below are deliberately bare numbers, not --z-* tokens.
   `.ba` sets `position:relative` + `overflow:hidden`, so its children form
   their own stacking context and these values are only ever compared with
   each other, never with the masthead or the skip link. Keeping them local
   is the point: a component that orders its own internals shouldn't be
   reaching into the global scale, and shouldn't be able to escape it either. */
.ba{
  position:relative;aspect-ratio:16/10;border-radius:var(--r-lg);overflow:hidden;
  border:1px solid var(--line);box-shadow:var(--shadow-lift);margin-top:1.75rem;
  --pos:50%;user-select:none;background:var(--void);max-height:26rem;
}
.ba-panel{position:absolute;inset:0}
.ba-after{z-index:1}
.ba-before{z-index:2;clip-path:inset(0 calc(100% - var(--pos)) 0 0)}
.ba-mock{position:absolute;inset:0;padding:clamp(1.25rem,4vw,2.25rem);display:flex;flex-direction:column;gap:.6rem}
.ba-mock--after{background:var(--paper)}
.ba-mock--before{background:#EFEAE0}
.ba-mock__nav{display:flex;gap:.4rem;margin-bottom:.75rem}
.ba-mock__nav span{width:.55rem;height:.55rem;border-radius:50%;background:rgba(0,0,0,.15)}
.ba-mock__hero{max-width:70%}
.ba-mock__h1{height:1.1rem;border-radius:4px;background:var(--ink);margin-bottom:.5rem;width:85%}
.ba-mock__p{height:.55rem;border-radius:3px;background:var(--line);width:60%;margin:.6rem 0 1rem}
.ba-mock__btn{height:1.9rem;width:8rem;border-radius:var(--r-pill);background:var(--cobalt)}
.ba-tag{
  position:absolute;top:.9rem;left:.9rem;z-index:3;font-family:var(--mono);font-size:.68rem;
  letter-spacing:.06em;text-transform:uppercase;padding:.3rem .7rem;border-radius:var(--r-pill);
}
/* Each label goes on the side its own panel is actually VISIBLE on, which
   is the opposite of where they started. `.ba-before` is clipped to reveal
   its LEFT portion and sits above `.ba-after` (z-index 2 vs 1), so:
     - the "before" label belongs left, over the part of that panel on show;
     - the "after" label belongs right, where .ba-before is clipped away and
       .ba-after is what you can see.
   Previously they were the other way round, which meant NEITHER label ever
   rendered — the after label was painted underneath the before panel, and
   the before label sat in the clipped-off region. Two labels whose whole job
   is saying which side is which, invisible on every load. axe never saw it
   (the elements are present, sized and "visible" to the DOM; they are just
   covered and clipped), and neither did the pixel sweep until it was asked
   what was actually painted inside their boxes. */
.ba-tag--after{background:var(--sage-band);color:#fff;left:auto;right:.9rem}
.ba-tag--before{background:#8C2C24;color:#fff;left:.9rem;right:auto}
.ba-note{
  position:absolute;font-family:var(--mono);font-size:.68rem;line-height:1.4;max-width:11rem;
  padding:.35rem .55rem;border-radius:var(--r);background:rgba(255,255,255,.94);
  box-shadow:0 2px 8px rgba(0,0,0,.12);z-index:2;
}
.ba-note--good{border-left:3px solid var(--sage);color:#1F3A2E}
.ba-note--bad{border-left:3px solid #8C2C24;color:#5c1f19}
.ba-line{position:absolute;top:0;bottom:0;left:var(--pos);width:2px;background:#fff;box-shadow:0 0 0 1px rgba(0,0,0,.18);z-index:3;transform:translateX(-1px)}
.ba-handle{
  position:absolute;top:50%;left:var(--pos);transform:translate(-50%,-50%);
  width:2.75rem;height:2.75rem;border-radius:50%;background:#fff;border:2px solid var(--void);
  display:grid;place-items:center;cursor:ew-resize;z-index:4;box-shadow:var(--shadow-lift);
  color:var(--void);touch-action:none;padding:0;
}
.ba-handle svg{width:18px;height:18px}
.ba-handle:hover{background:var(--cobalt-wash)}
.ba-handle:active{cursor:grabbing}
@media (max-width:36rem){
  .ba-note{font-size:.62rem;max-width:8rem;padding:.28rem .45rem}
  .ba-mock__hero{max-width:88%}
}

/* ---------- print ---------- */
@media print{
  .masthead,.skip,.btn-row,.foot .cols{display:none !important}
    body{background:#fff;color:#000;font-size:11pt}
  .band--dark{background:#fff !important;color:#000 !important}
  .band--dark h1,.band--dark h2,.band--dark h3{color:#000 !important}
  a::after{content:" (" attr(href) ")";font-size:9pt;color:#444}
}