/* ==========================================================================
   EVOLUTION WEB STUDIO — design system
   --------------------------------------------------------------------------
   Brand idea: built in stages you can see.

   Everything visual carries that idea rather than decorating around it. The
   mark is an E whose arms step outward on a shared diagonal. Sections are
   separated by a stepped rule that repeats the same geometry. Stage numbers
   are set in mono so they read as record-keeping rather than ornament. The
   palette is strata: basalt ink, bone paper, clay, and a verdigris used only
   where a second signal is genuinely needed.

   Three families, each with a job and no overlap:
     Fraunces         display and headings. High contrast, editorial, warm.
     Instrument Sans  body and interface. Neutral, quiet, gets out of the way.
     IBM Plex Mono    labels, numerals, versions, anything that is a record.

   Colour pairs are verified against WCAG 2.2 AA by build/contrast_check.mjs,
   which reads THIS FILE. Change a token and the gate re-checks it.
   ========================================================================== */

/* ---------- Fonts: self-hosted, latin subset, no third-party request ------ */
@font-face {
  font-family: "Evo Display";
  src: url("/assets/fonts/fraunces-300-800-latin.woff2") format("woff2-variations");
  font-weight: 300 800;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Evo Sans";
  src: url("/assets/fonts/instrument-sans-400-700-latin.woff2") format("woff2-variations");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Evo Mono";
  src: url("/assets/fonts/plex-mono-400-latin.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Evo Mono";
  src: url("/assets/fonts/plex-mono-500-latin.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

/* ---------- Tokens ------------------------------------------------------- */
:root {
  color-scheme: light dark;

  /* Strata */
  --paper:      #F7F4EF;
  --paper-2:    #EFEAE0;
  --surface:    #FFFFFF;
  --ink:        #14161A;
  --ink-2:      #41464F;
  --ink-3:      #565C66;
  --rule:       #E3DCCF;
  --rule-strong:#7A7362;
  --accent:     #A8431A;
  --accent-hov: #86340F;
  --accent-bg:  #F8EDE5;
  --accent-2:   #0D5651;
  --accent-2-bg:#E5F0EE;
  --on-accent:  #FFFFFF;
  --inverse-bg: #14161A;
  --inverse-ink:#F3F0E9;
  --inverse-ink-2:#ADA79B;
  --inverse-rule:#2E333B;
  --inverse-accent:#EC9257;
  --inverse-accent-2:#68C9BC;
  --flag:       #7A4B00;
  --flag-bg:    #FBF0D8;
  --good:       #1D6144;
  /*
   * THE FIFTH SEVERITY, AND THE ONLY ONE THAT NEEDED A NEW COLOUR.
   *
   * A Diagnostic grades every finding Critical, High, Medium, Low or Already
   * right, and four of those already exist in this palette and are already
   * held to AA by the contrast gate: High is the accent, Medium is the flag,
   * Low is `--ink-3` and Already right is `--good`. Only Critical had no
   * colour, because nothing on this site had ever needed to say "stop".
   *
   * It is registered in `contrast_check.mjs` in both schemes like every other
   * pair. A severity scale whose top rung is not legible is a scale that
   * fails on the finding that matters most.
   */
  --critical:   #8E1B12;
  --critical-bg:#FAE6E2;
  --doc-shadow: 0 1px 2px rgba(20,22,26,.05), 0 18px 52px rgba(20,22,26,.09);

  --font-display: "Evo Display", Georgia, "Times New Roman", serif;
  --font-sans: "Evo Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: "Evo Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  --step--2: 0.75rem;
  --step--1: 0.8125rem;
  --step-0:  1.0625rem;
  --step-1:  clamp(1.125rem, 1.05rem + 0.35vw, 1.3125rem);
  --step-2:  clamp(1.4rem, 1.2rem + 0.8vw, 1.75rem);
  --step-3:  clamp(1.75rem, 1.45rem + 1.4vw, 2.4rem);
  --step-4:  clamp(2.15rem, 1.6rem + 2.5vw, 3.4rem);
  --step-5:  clamp(2.6rem, 1.6rem + 4.4vw, 5rem);

  --s-1: 0.25rem;
  --s-2: 0.5rem;
  --s-3: 0.75rem;
  --s-4: 1rem;
  --s-5: 1.5rem;
  --s-6: 2rem;
  --s-7: 3rem;
  --s-8: 4rem;
  --s-9: 6rem;
  --s-10: 8rem;

  --measure: 34rem;
  --wrap: 76rem;
  --radius: 3px;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper:      #0E1114;
    --paper-2:    #161A1F;
    --surface:    #161A1F;
    --ink:        #EFECE5;
    --ink-2:      #B3AEA4;
    --ink-3:      #9A978E;
    --rule:       #272C33;
    --rule-strong:#767C86;
    --accent:     #EC9257;
    --accent-hov: #F6AE7E;
    --accent-bg:  #241A12;
    --accent-2:   #68C9BC;
    --accent-2-bg:#0F2320;
    --on-accent:  #14161A;
    --inverse-bg: #EFECE5;
    --inverse-ink:#14161A;
    --inverse-ink-2:#4A4F58;
    --inverse-rule:#CFC9BC;
    --inverse-accent:#A8431A;
    --inverse-accent-2:#0D5651;
    --flag:       #E9BB66;
    --flag-bg:    #241E10;
    --good:       #5CC49A;
    --critical:   #F19A8E;
    --critical-bg:#2A1512;
    --doc-shadow: 0 1px 2px rgba(0,0,0,.5), 0 18px 52px rgba(0,0,0,.42);
  }
}

:root[data-theme="dark"] {
  --paper:      #0E1114;
  --paper-2:    #161A1F;
  --surface:    #161A1F;
  --ink:        #EFECE5;
  --ink-2:      #B3AEA4;
  --ink-3:      #9A978E;
  --rule:       #272C33;
  --rule-strong:#767C86;
  --accent:     #EC9257;
  --accent-hov: #F6AE7E;
  --accent-bg:  #241A12;
  --accent-2:   #68C9BC;
  --accent-2-bg:#0F2320;
  --on-accent:  #14161A;
  --inverse-bg: #EFECE5;
  --inverse-ink:#14161A;
  --inverse-ink-2:#4A4F58;
  --inverse-rule:#CFC9BC;
  --inverse-accent:#A8431A;
  --inverse-accent-2:#0D5651;
  --flag:       #E9BB66;
  --flag-bg:    #241E10;
  --good:       #5CC49A;
  --critical:   #F19A8E;
  --critical-bg:#2A1512;
  --doc-shadow: 0 1px 2px rgba(0,0,0,.5), 0 18px 52px rgba(0,0,0,.42);
}

/* ---------- Reset -------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
/* The masthead is sticky, so an in-page jump has to clear it or the heading
   lands underneath the header. */
/*
 * Anything scrolled to must clear the sticky masthead. `[id]` covered anchored
 * headings; a SECTION scrolled to by the browser, by find-in-page or by a
 * preview jump has no id and was landing under the bar, which is how the home
 * page's ownership heading came to be reported as invisible. The bar was
 * translucent at the time, so the covered half of the heading showed through as
 * a dark ghost rather than being cleanly hidden.
 */
/*
 * SC 2.4.11 Focus Not Obscured (Minimum), AA.
 *
 * This was `scroll-margin-top` on `:target, [id], .section, h1, h2, h3`, which
 * reaches headings and anchors and reaches NO link, button or summary. The
 * pre-launch audit counted 1,938 of 1,986 focusable elements getting no offset
 * at all, under a sticky masthead 4.5rem tall.
 *
 * Tab forward and the browser scrolls the target to the bottom edge, which is
 * fine. **Shift+Tab is the failure**, and Shift+Tab is how a keyboard user
 * corrects a mis-tab.
 *
 * `scroll-padding-top` on the scroll container is the fix rather than
 * `scroll-margin-top` on a list of selectors, because it applies to everything
 * scrolled into view without anybody maintaining the list, and it is the form
 * with fewer engine bugs. The `scroll-margin` line stays for the elements that
 * want a little more room than the bar.
 */
html { scroll-padding-top: 6rem; }
:target, [id], .section, h1, h2, h3 { scroll-margin-top: 6rem; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--step-0);
  line-height: 1.62;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
img, svg, video { max-width: 100%; height: auto; display: block; }
h1, h2, h3, h4 {
  margin: 0;
  font-family: var(--font-display);
  font-variation-settings: "opsz" 48;
  font-weight: 600;
  line-height: 1.08;
  letter-spacing: -0.022em;
  text-wrap: balance;
}
p, ul, ol, dl, table, figure, pre { margin: 0; }
a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 0.18em; }
a:hover { color: var(--accent-hov); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 2px; }
::selection { background: var(--accent); color: var(--on-accent); }
hr { border: 0; border-top: 1px solid var(--rule); margin: 0; }
table { border-collapse: collapse; width: 100%; }
button { font: inherit; color: inherit; }

/* ---------- Layout primitives -------------------------------------------- */
.wrap { width: min(100% - 2.5rem, var(--wrap)); margin-inline: auto; }
@media (max-width: 30rem) { .wrap { width: calc(100% - 2rem); } }

.section { padding-block: var(--s-9); border-top: 1px solid var(--rule); position: relative; }
.section--tight { padding-block: var(--s-7); }
.section--flush { border-top: 0; }
.section--fill { background: var(--paper-2); }
/*
 * A DARK BAND REMAPS THE PALETTE. IT DOES NOT CORRECT ITS CHILDREN ONE BY ONE.
 *
 * Every rule below this one that names a descendant of `.section--inverse` is
 * a correction applied BY HAND to a component somebody remembered. That list
 * is about twenty long and it was never going to be complete, because the way
 * it fails is silent: a component whose colour comes from `--ink` renders
 * #14161A text on a #14161A band and simply is not there. It still has a box,
 * it still has a hit area, it still works on hover, so it looks like an empty
 * rectangle rather than like a bug.
 *
 * Three live buttons were in that state when this was written, all of them
 * secondary calls to action on the closing band of a page: "Book 15 minutes,
 * free" on the Diagnostic, "Ask about a plan, 15 minutes" on Care, and "See
 * every price" on the Websites hub. Tony found the first one by looking at the
 * page. Nothing else could have: every class resolved, every declared token
 * pair passed the contrast gate, and the gate reads TOKEN PAIRS rather than
 * what a token resolves to inside a section that changed its meaning.
 *
 * The fix is to redefine the base palette for the duration of the band, so a
 * component dropped in here is right without anybody remembering anything.
 * The hand corrections below are now mostly no-ops resolving to the same
 * values; they are kept because removing twenty rules in the same change as
 * fixing a live defect is how a small fix becomes a large one.
 *
 * `--on-accent` is remapped too, and that fixes a second failure nobody had
 * reported: `.btn:hover` painted #FFFFFF on the #EC9257 inverse accent, which
 * is about 2.3:1 and fails AA outright. A hover state is still text.
 */
.section--inverse {
  --paper:       var(--inverse-bg);
  --paper-2:     var(--inverse-bg);
  --surface:     var(--inverse-bg);
  --ink:         var(--inverse-ink);
  --ink-2:       var(--inverse-ink-2);
  --ink-3:       var(--inverse-ink-2);
  --rule:        var(--inverse-rule);
  --rule-strong: var(--inverse-ink-2);
  --accent:      var(--inverse-accent);
  --accent-2:    var(--inverse-accent-2);
  --on-accent:   var(--inverse-bg);
  background: var(--inverse-bg); color: var(--inverse-ink); border-top: 0;
}
.section--inverse h1, .section--inverse h2, .section--inverse h3, .section--inverse h4 { color: var(--inverse-ink); }
.section--inverse a:not(.btn) { color: var(--inverse-accent); }
.section--inverse .lede, .section--inverse p { color: var(--inverse-ink-2); }
.section--inverse .label { color: var(--inverse-ink-2); }
.section--inverse .label--accent { color: var(--inverse-accent); }
.section--inverse .rule, .section--inverse hr { border-color: var(--inverse-rule); }
.section--inverse .table-scroll { border-color: var(--inverse-rule); }
.section--inverse .data th, .section--inverse .data td { border-color: var(--inverse-rule); }
.section--inverse .data thead th { background: transparent; color: var(--inverse-ink-2); }

/*
 * The stepped rule. The same 5-over-7 diagonal as the mark's arms, repeated as
 * a section divider so the geometry of the identity is present in the page
 * structure rather than only in the logo.
 */
.section--stepped::before {
  content: "";
  position: absolute; inset-inline: 0; top: -1px; height: 10px;
  background:
    linear-gradient(to right, var(--accent) 0 22%, transparent 22%) top left / 100% 2px no-repeat;
  opacity: 1;
}

.spec { display: grid; gap: var(--s-5) var(--s-7); grid-template-columns: 1fr; }
.spec > *, .grid-2 > *, .grid-3 > *, .hero__grid > *, .product-grid > *, .facts > *, .step > * { min-width: 0; }
@media (min-width: 56rem) {
  .spec { grid-template-columns: 13rem minmax(0, 1fr); }
}

.label {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  font-weight: 500;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0 0 var(--s-2);
}
.label--accent { color: var(--accent); }

.lede { font-size: var(--step-1); line-height: 1.5; color: var(--ink-2); max-width: 40rem; }
.prose { max-width: var(--measure); }
.prose p + p { margin-top: var(--s-4); }
.flow > * + * { margin-top: var(--s-4); }
.flow-lg > * + * { margin-top: var(--s-6); }

.h-display { font-size: var(--step-5); letter-spacing: -0.03em; line-height: 0.98; font-variation-settings: "opsz" 120; }
.h-1 { font-size: var(--step-4); letter-spacing: -0.026em; font-variation-settings: "opsz" 90; }
.h-2 { font-size: var(--step-3); letter-spacing: -0.022em; }
.h-3 { font-size: var(--step-2); }
.h-4 { font-size: var(--step-1); letter-spacing: -0.012em; }
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/*
 * The breadcrumb sits above the first section rather than inside it, so one
 * include in the layout serves every page shape. Padding rather than margin,
 * because the sections below set their own vertical rhythm and a margin here
 * would collapse into whichever one came first.
 */
.crumbs-bar { padding-block: var(--s-3) 0; }
/* WCAG 2.2 SC 2.5.8, Target Size (Minimum).
   The trail is set in small muted type, which made every crumb a 37 by 16
   target: under the 24 by 24 minimum on 31 pages at every mobile width, found
   by the browser pass and invisible to every gate that reads files. A
   breadcrumb is a navigation control, not a link inside a sentence, so the
   inline exception does not apply to it.
   The height comes from padding rather than from a line-height, so the text
   still sits on the same baseline as the separators and the bar does not grow
   on a page whose trail wraps. */
.crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 0 var(--s-2); }
.crumbs a,
.crumbs [aria-current="page"] { display: inline-flex; align-items: center; min-height: 24px; }
.crumbs a { color: var(--ink-2); }
.crumbs [aria-current="page"] { color: var(--ink); }
.muted { color: var(--ink-3); }
.small { font-size: var(--step--1); }
.serif { font-family: var(--font-display); font-variation-settings: "opsz" 40; }

/* ---------- Skip link, masthead, logo ------------------------------------ */
.skip {
  position: absolute; left: var(--s-4); top: 0; z-index: 100;
  transform: translateY(-200%);
  background: var(--ink); color: var(--paper); padding: var(--s-3) var(--s-4);
  text-decoration: none; border-radius: 0 0 var(--radius) var(--radius);
}
.skip:focus { transform: translateY(0); }

/*
 * THE STICKY HEADER IS OPAQUE, AND THAT IS NOT A STYLE PREFERENCE.
 *
 * It was `color-mix(in srgb, var(--paper) 88%, transparent)` with a backdrop
 * blur, which looks good over the light page and is unreadable over an inverted
 * one. Scrolling the ownership band on the home page put a near-white heading
 * under a 12%-transparent LIGHT bar with the dark section smeared through it,
 * and the second line of "Every key is in your name before anything is built"
 * became a dark ghost on black. Tony found it by scrolling.
 *
 * **No gate could see it, because the defect does not exist at rest.** axe
 * measures an element against its own background; this is a fixed overlay
 * changing what is legible at one particular scroll offset, over one particular
 * section, and only for the height of the bar. A page audited standing still is
 * a different page from one being read.
 *
 * Opaque means content passing underneath is cleanly hidden rather than
 * ghosted, in every scheme, over every section, with no dependence on
 * backdrop-filter support or on forced-colors behaviour. The frosted look was
 * the kind of thing this brand cuts rather than keeps: it made a heading
 * unreadable to buy a texture.
 */
.masthead { border-bottom: 1px solid var(--rule); background: var(--paper); position: sticky; top: 0; z-index: 40; }
.masthead__inner { display: flex; align-items: center; justify-content: space-between; gap: var(--s-5); min-height: 4.5rem; }

.logo { display: inline-flex; align-items: center; gap: 0.65rem; text-decoration: none; color: var(--ink); }
.logo:hover { color: var(--ink); }
.logo:hover .mark { color: var(--accent); }
.logo__mark { width: 1.85rem; flex: none; display: block; }
.mark { color: currentColor; transition: color .18s ease; }
.logo__type { display: block; }
.logo__name {
  display: block; font-family: var(--font-display);
  font-variation-settings: "opsz" 48;
  font-weight: 600; font-size: 1.3rem; line-height: 1; letter-spacing: -0.028em;
}
.logo__sub {
  display: block; font-family: var(--font-mono); font-weight: 500;
  font-size: 0.5625rem; letter-spacing: 0.235em; text-transform: uppercase;
  color: var(--ink-3); margin-top: 0.24rem;
}
.logo--inverse { color: var(--inverse-ink); }
.logo--inverse:hover { color: var(--inverse-ink); }
.logo--inverse .logo__sub { color: var(--inverse-ink-2); }

.nav { display: none; }
@media (min-width: 64rem) {
  .nav { display: flex; align-items: center; gap: var(--s-5); }
  .nav a:not(.btn) { color: var(--ink-2); text-decoration: none; font-size: var(--step--1); position: relative; }
  .nav a:not(.btn)::after {
    content: ""; position: absolute; left: 0; right: 0; bottom: -0.4rem; height: 1.5px;
    background: var(--accent); transform: scaleX(0); transform-origin: left; transition: transform .18s ease;
  }
  .nav a:not(.btn):hover { color: var(--ink); }
  .nav a:not(.btn):hover::after, .nav a:not(.btn)[aria-current]::after { transform: scaleX(1); }
  .nav a:not(.btn)[aria-current] { color: var(--ink); }
}

/*
 * THE MASTHEAD ACTIONS, AND WHY THEY ARE NOT IN THE NAV.
 *
 * The phone number reads as a number rather than as another nav item, because
 * a buyer scanning for "how do I ring these people" is looking for digits.
 * Mono, because the design system already sets mono for anything that is a
 * record: versions, prices, hours and stage numbers.
 *
 * It used to be `.nav__tel`, INSIDE `.nav`, which is `display: none` below
 * 64rem. **So it was invisible on every phone**, on a site that charges for the
 * observation that a tradesman's site should keep the number on screen. It now
 * sits in `.masthead__act`, which is never hidden, and the only thing that
 * disappears on a narrow screen is the estimate button, which the drawer
 * carries as its own line anyway.
 */
.masthead__act { display: flex; align-items: center; gap: var(--s-4); flex: none; }

/*
 * A ROW OF SECONDARY LINKS, WITH TARGETS BIG ENOUGH TO PRESS.
 *
 * Wraps like text, but every link gets a 44px box and the separators are drawn
 * by CSS rather than typed between the anchors. Typed separators put a stray
 * "·" in the accessibility tree between every item and, worse, made the parent
 * look like a sentence to nothing at all: the 2.5.8 inline exception needs real
 * surrounding prose, which a middle dot is not.
 */
.link-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0 var(--s-4); }
.link-row > a { display: inline-flex; align-items: center; min-height: 2.75rem; }
.link-row > a + a::before {
  content: "\00B7"; margin-inline-end: var(--s-4); color: var(--ink-3);
  speak: never; /* decoration; the gap already separates them for a reader */
}
/*
 * THE MOST IMPORTANT TAP TARGET ON THE SITE WAS 83 BY 19.
 *
 * This block argues, correctly, that for this trade the call is the conversion
 * and the number must never leave the screen. It then painted that number as a
 * bare inline link 19 CSS px tall, on a phone, which is under half of Apple's
 * 44pt and well under Android's 48dp, and under the 24px floor in WCAG 2.2
 * SC 2.5.8.
 *
 * It is arguable that 2.5.8 is satisfied anyway: the spacing exception passes
 * a small target when a 24px circle centred on it touches no other target's
 * circle, and the nearest one is the Menu button 94px away centre to centre.
 * **That argument is not worth winning.** A thumb reaching for a phone number
 * on a moving bus does not care which exception applies, and this is the one
 * element the whole masthead exists for. `browser_audit` refuses it at 320,
 * 360 and 390 and is right to, because it implements the inline exception and
 * not the spacing one, which is the conservative reading and the correct one
 * for a link that is the point of the page.
 *
 * The masthead is already 4.5rem tall, so the height is bought from padding
 * that was empty space, and nothing moves. `inline-flex` with a min-height
 * rather than padding alone, so the box is the same whatever the font does.
 */
.masthead__tel {
  font-family: var(--font-mono); font-size: var(--step--1); color: var(--ink);
  letter-spacing: -0.01em; text-decoration: none; white-space: nowrap;
  display: inline-flex; align-items: center; min-height: 2.75rem;
  padding-inline: 0.25rem; margin-inline: -0.25rem;
}
.masthead__tel:hover { color: var(--accent); }
/*
 * TWO SINGLE-CLASS SELECTORS, AND SOURCE ORDER DECIDED IT.
 *
 * This was `.masthead__cta { display: none }`, which lost to `.btn`'s
 * `display: inline-flex` because `.btn` is declared LATER in this file and both
 * are one class deep. The button stayed on screen at every width, measured on
 * the live deploy at 475px. Scoping it to the parent wins on specificity rather
 * than on position, which is the only version of this that survives somebody
 * moving a block.
 */
.masthead__act > .masthead__cta { display: none; }
@media (min-width: 64rem) {
  .masthead__act > .masthead__cta { display: inline-flex; }
}
/* A 360px phone carries a logo, a ten digit number and a Menu button on one
   line. The gap and the number both give ground before anything wraps. */
@media (max-width: 26rem) {
  .masthead__inner { gap: var(--s-3); }
  .masthead__act { gap: var(--s-3); }
  .masthead__tel { font-size: var(--step--2); }
}
/*
 * 320px, AND WHY IT IS NOT AN EDGE CASE.
 *
 * The rule above was tuned at 360 and the comment says so. At 320 the same row
 * needs 316.7px inside a `.wrap` that is `calc(100% - 2rem)`, so 288px, and the
 * document went to 333px on EVERY PAGE. That is not an old phone nobody has:
 * **320 CSS px is the normative width for WCAG 1.4.10 Reflow**, which is what a
 * sighted user with low vision gets when they zoom a 1280px window to 400%. So
 * the whole site failed 1.4.10 for the people most likely to need the zoom.
 *
 * The word "Menu" is what goes, not the phone number. The number is the one
 * thing this masthead exists to keep on screen (see `.masthead__act` above) and
 * a hamburger beside a drawer is understood without a label at this width. The
 * label is hidden rather than deleted, so the button's accessible name is still
 * "Menu" and nothing changes for a screen reader.
 */
.nav-toggle { display: inline-flex; align-items: center; gap: 0.5rem; background: none; border: 1px solid var(--rule-strong); border-radius: var(--radius); padding: 0.45rem 0.7rem; font-size: var(--step--1); cursor: pointer; }
@media (min-width: 64rem) { .nav-toggle { display: none; } }
/*
 * DECLARED AFTER `.nav-toggle`, DELIBERATELY. A media query adds no
 * specificity, so this block sitting ABOVE the base rule would lose `gap` and
 * `padding-inline` to it on source order alone. That is the same defect as the
 * `.masthead__cta` one recorded thirty lines up, and it is why this sits here
 * rather than beside the 26rem rule it continues.
 */
@media (max-width: 22rem) {
  .nav-toggle { gap: 0; padding-inline: 0.55rem; }
  /*
   * `display: none`, NOT the clip technique the rest of this site uses.
   *
   * The first version of this hid the label with the visually-hidden clip
   * pattern, which keeps the word in the accessibility tree. That was the
   * right instinct and the wrong tool: `browser_audit`'s clipped-text rule
   * reads a 1px box with `overflow: hidden` and content inside it as text the
   * layout has cut off, and it exempts `.visually-hidden` by class, which a
   * bespoke copy of the same declarations is not. It failed on all 41 pages.
   *
   * Taking the word out of the tree entirely is fine HERE because the button
   * carries `aria-label="Menu"`, so the name is stated rather than inferred
   * from the text, at every width. Simpler, and it stops a hidden element
   * having a size for a layout rule to argue with.
   */
  .nav-toggle__label { display: none; }
}
/*
 * THE OPEN DRAWER MUST NOT OUTGROW THE VIEWPORT IT HANGS FROM.
 *
 * The drawer lives inside the sticky masthead, so on a short viewport (a phone
 * in landscape, a small laptop with the browser chrome showing) its bottom
 * links sat below the fold of a container that does not scroll, and could not
 * be reached at all. The masthead is 4.5rem, so the drawer gets the rest.
 */
.nav-drawer {
  border-top: 1px solid var(--rule);
  padding-block: var(--s-4);
  display: grid;
  gap: var(--s-1);
  max-height: calc(100dvh - 4.5rem);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.nav-drawer a { display: block; padding: 0.6rem 0; color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--rule); }
@media (min-width: 64rem) { .nav-drawer { display: none !important; } }

/* ---------- Buttons ------------------------------------------------------ */
.btn {
  display: inline-flex; align-items: center; gap: 0.55rem;
  padding: 0.82rem 1.3rem;
  border: 1px solid var(--ink);
  background: var(--ink); color: var(--paper);
  text-decoration: none; font-weight: 600; font-size: var(--step--1);
  border-radius: var(--radius); cursor: pointer;
  transition: background-color .16s ease, color .16s ease, border-color .16s ease, transform .16s ease;
}
.btn:hover { background: var(--accent); border-color: var(--accent); color: var(--on-accent); transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn--ghost { background: transparent; color: var(--ink); border-color: var(--rule-strong); }
.btn--ghost:hover { background: transparent; color: var(--accent); border-color: var(--accent); }
.btn--inverse { background: var(--inverse-ink); color: var(--inverse-bg); border-color: var(--inverse-ink); }
.btn--inverse:hover { background: var(--inverse-accent); border-color: var(--inverse-accent); color: var(--paper); }
/*
 * `.btn--quiet` WAS DELETED HERE AND IT IS WORTH SAYING WHY.
 *
 * It was styled correctly for a dark band, it was never used on a single page,
 * and an identical rule called `.u-btn-on-inverse` was living in utilities.css
 * and being applied by hand to five buttons. Two names, one behaviour, and
 * three other buttons that got neither and were invisible.
 *
 * That is the fourth component on this build written, styled and called by
 * nothing: `milestoneAcceptance` at 0.23.0, `is-invalid` at 0.32.0,
 * `diagnosticPaid` at 0.37.0, this. The pattern is always the same, the right
 * thing exists and the wrong thing is what ships, because nothing checks that
 * a component has a caller.
 *
 * Both are gone. Inside `.section--inverse` the palette is remapped, so
 * `.btn` is a solid light button and `.btn--ghost` is an outlined one, with no
 * patch class to remember.
 */
.btn--lg { padding: 0.98rem 1.6rem; font-size: var(--step-0); }
.btn[disabled] { opacity: .45; cursor: not-allowed; }
.btn-row { display: flex; flex-wrap: wrap; gap: var(--s-3); align-items: center; }

/* ---------- Hero --------------------------------------------------------- */
.hero { padding-block: clamp(var(--s-7), 7vw, var(--s-9)) var(--s-8); }
.hero__grid { display: grid; gap: var(--s-7); }
@media (min-width: 64rem) {
  .hero__grid { grid-template-columns: minmax(0, 1.08fr) minmax(0, 0.92fr); gap: var(--s-8); align-items: center; }
}
.hero__claim { font-size: var(--step-5); letter-spacing: -0.034em; line-height: 0.98; margin-bottom: var(--s-5); font-variation-settings: "opsz" 120; }
.hero__claim em { font-style: normal; color: var(--accent); }
.hero__panel { border: 1px solid var(--rule); border-radius: var(--radius); background: var(--surface); padding: var(--s-5); }
.dl { display: grid; grid-template-columns: auto 1fr; gap: 0.5rem var(--s-4); font-size: var(--step--1); margin: 0; }
.dl dt { font-family: var(--font-mono); font-size: var(--step--2); text-transform: uppercase; letter-spacing: 0.09em; color: var(--ink-3); padding-top: 0.15rem; }
.dl dd { margin: 0; color: var(--ink); }
.hero__panel dl { display: grid; grid-template-columns: auto 1fr; gap: 0.5rem var(--s-4); font-size: var(--step--1); }
.hero__panel dt { font-family: var(--font-mono); font-size: var(--step--2); text-transform: uppercase; letter-spacing: 0.09em; color: var(--ink-3); padding-top: 0.15rem; }
.hero__panel dd { margin: 0; color: var(--ink); }

/* The strata diagram. An information graphic, not an ornament: each band is a
   delivery stage, widening on the same 5-over-7 diagonal as the mark's arms. */
.hero__figure { align-self: center; }
.strata { width: 100%; height: auto; display: block; }
.strata text { font-family: var(--font-mono); font-weight: 500; }
.strata__stage { font-size: 10px; letter-spacing: 0.06em; opacity: .72; }
.strata__name  { font-size: 10px; letter-spacing: 0.14em; }
.strata__note  { font-size: 10.5px; letter-spacing: 0.01em; }
.strata__foot  { font-size: 9px; letter-spacing: 0.16em; }
.js-reveal .strata__band[data-reveal] { opacity: 0; transform: translateX(-14px); }
.js-reveal .strata__band[data-reveal].is-in { opacity: 1; transform: none; transition: opacity .55s ease, transform .55s cubic-bezier(.2,.7,.3,1); }
.js-reveal .strata__band:nth-child(2) { transition-delay: .06s; }
.js-reveal .strata__band:nth-child(3) { transition-delay: .12s; }
.js-reveal .strata__band:nth-child(4) { transition-delay: .18s; }
.js-reveal .strata__band:nth-child(5) { transition-delay: .24s; }
.js-reveal .strata__band:nth-child(6) { transition-delay: .30s; }

/* ---------- Facts strip -------------------------------------------------- */
.facts { display: grid; gap: 1px; background: var(--rule); border-block: 1px solid var(--rule); }
@media (min-width: 48rem) { .facts { grid-template-columns: repeat(4, 1fr); } }
.facts__item { background: var(--paper); padding: var(--s-5) var(--s-4); }
.facts__item:first-child .facts__n, .facts__item:last-child .facts__n { color: var(--accent); }
.facts__n { font-family: var(--font-mono); font-size: var(--step-3); font-weight: 500; font-variant-numeric: tabular-nums; letter-spacing: -0.04em; display: block; line-height: 1; margin-bottom: 0.6rem; }
.facts__t { font-size: var(--step--1); color: var(--ink-3); }

/* ---------- Stages ------------------------------------------------------- */
.steps { display: grid; gap: 0; }
.step { display: grid; gap: var(--s-2) var(--s-6); padding-block: var(--s-6); border-top: 1px solid var(--rule); position: relative; }
@media (min-width: 56rem) { .step { grid-template-columns: 4.5rem 14rem minmax(0, 1fr); align-items: start; } }
.step__n { font-family: var(--font-mono); font-size: var(--step--1); color: var(--accent); font-weight: 500; padding-top: 0.35rem; }
.step__name { font-family: var(--font-display); font-variation-settings: "opsz" 40; font-size: var(--step-2); letter-spacing: -0.02em; font-weight: 600; }
.step__who { font-family: var(--font-mono); font-size: var(--step--2); text-transform: uppercase; letter-spacing: 0.09em; color: var(--ink-3); margin-top: 0.4rem; }
.step__gate { margin-top: var(--s-3); padding-left: var(--s-4); border-left: 2px solid var(--accent); color: var(--ink-2); font-size: var(--step--1); }
.section--inverse .step { border-color: var(--inverse-rule); }
.section--inverse .step__n { color: var(--inverse-accent); }
.section--inverse .step__gate { border-color: var(--inverse-accent); color: var(--inverse-ink-2); }

/* ---------- Product blocks ----------------------------------------------- */
.product { border: 1px solid var(--rule); border-radius: var(--radius); background: var(--surface); display: flex; flex-direction: column; transition: border-color .18s ease, transform .18s ease; }
.product:hover { border-color: var(--rule-strong); transform: translateY(-2px); }
.product--lead { border-color: var(--ink); }
.product--lead .product__head { position: relative; }
.product--lead .product__head::before { content: ""; position: absolute; top: -1px; left: -1px; width: 34%; height: 3px; background: var(--accent); }
.product--lead .product__price { color: var(--accent); }
/* WCAG 2.2 SC 1.4.1, Use of Colour.
   The featured card was marked by an accent rule, a heavier border and an
   accent price, and by nothing a person could read. Anyone who cannot see the
   difference between two borders was shown five identical cards. The label
   says whose recommendation it is rather than how popular it is, because this
   practice has no client count to cite and will not invent one. */
.product__flag {
  font-family: var(--font-mono); font-size: var(--step--2); text-transform: uppercase;
  letter-spacing: 0.09em; color: var(--accent); margin: 0 0 var(--s-2);
}
.product__head { padding: var(--s-5); border-bottom: 1px solid var(--rule); }
.product__body { padding: var(--s-5); display: flex; flex-direction: column; gap: var(--s-4); flex: 1; }
.product__foot { padding: var(--s-5); border-top: 1px solid var(--rule); margin-top: auto; }
.product__price { font-family: var(--font-mono); font-size: var(--step-2); font-variant-numeric: tabular-nums; letter-spacing: -0.035em; display: block; font-weight: 500; }
.product__price small { font-family: var(--font-sans); font-size: var(--step--1); letter-spacing: 0; color: var(--ink-3); display: block; font-weight: 400; margin-top: 0.35rem; }
.product-grid { display: grid; gap: var(--s-4); }
@media (min-width: 52rem) { .product-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 80rem) { .product-grid--3 { grid-template-columns: repeat(3, 1fr); } }

/* ---------- Lists -------------------------------------------------------- */
.ticks { list-style: none; padding: 0; display: grid; gap: 0.65rem; }
.ticks li { padding-left: 1.7rem; position: relative; color: var(--ink-2); }
.ticks li::before {
  content: ""; position: absolute; left: 0; top: 0.55em;
  width: 0.72rem; height: 0.38rem; border-left: 1.5px solid var(--accent); border-bottom: 1.5px solid var(--accent);
  transform: rotate(-45deg);
}
.section--inverse .ticks li { color: var(--inverse-ink-2); }
.section--inverse .ticks li::before { border-color: var(--inverse-accent); }
/*
 * `.crosses` is the site's ONLY "not included" list, and it must stay the only
 * one. Four pages used `ticks ticks--no`, which was never defined, so every
 * exclusion list rendered with the SAME tick as the inclusions: a list of
 * things you do not get, ticked. Found at 0.11.0 by `class_check.mjs`. The fix
 * was to use the component that already existed rather than build a second.
 */
.crosses { list-style: none; padding: 0; display: grid; gap: 0.65rem; }
.crosses li { padding-left: 1.7rem; position: relative; color: var(--ink-3); }
.crosses li::before { content: ""; position: absolute; left: 0; top: 0.72em; width: 0.75rem; border-top: 1.5px solid var(--rule-strong); }
.section--inverse .crosses li { color: var(--inverse-ink-2); }
.section--inverse .crosses li::before { border-color: var(--inverse-rule); }
.section--inverse .plain, .section--inverse .plain li { color: var(--inverse-ink-2); }
.section--inverse .small.muted, .section--inverse .muted { color: var(--inverse-ink-2); }
.section--inverse .note { color: var(--inverse-ink-2); border-color: var(--inverse-rule); }
.section--inverse .note--accent { border-color: var(--inverse-accent); }
.plain { list-style: none; padding: 0; display: grid; gap: 0.65rem; }

/* ---------- Data table --------------------------------------------------- */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; border: 1px solid var(--rule); border-radius: var(--radius); }
.table-scroll:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.data { font-size: var(--step--1); min-width: 34rem; }
.data th, .data td { text-align: left; padding: 0.9rem var(--s-4); border-bottom: 1px solid var(--rule); vertical-align: top; }
.data thead th { font-family: var(--font-mono); font-size: var(--step--2); text-transform: uppercase; letter-spacing: 0.09em; color: var(--ink-3); font-weight: 500; background: var(--paper-2); }
.data tbody tr:last-child td, .data tbody tr:last-child th { border-bottom: 0; }
.data td.num, .data th.num { text-align: right; font-family: var(--font-mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
.data tbody th[scope="row"] { font-weight: 600; }

/* ---------- Panel -------------------------------------------------------- */
/*
 * `.panel` was used on /kits/ and every kit page from 0.6.0 and defined ONLY in
 * pricer.css, which those pages do not load. Fourteen boxes across nine public
 * pages rendered as bare divs for five releases: no border, no background, no
 * padding. Nothing reported it because the content still reads in order, which
 * is exactly why a class that resolves to nothing is a silent defect rather
 * than a visible one. `build/class_check.mjs` now fails the build on one.
 */
.panel {
  border: 1px solid var(--rule); border-radius: var(--radius);
  background: var(--surface); padding: var(--s-5);
}
.panel > :first-child { margin-top: 0; }
.panel--accent { border-color: var(--accent); border-width: 2px; }
.panel--accent > .label { color: var(--accent); }
.section--inverse .panel { border-color: var(--inverse-rule); background: transparent; }
.section--inverse .panel--accent { border-color: var(--inverse-accent); }

/* ---------- Callouts ----------------------------------------------------- */
.note { border-left: 2px solid var(--rule-strong); padding: 0 0 0 var(--s-4); color: var(--ink-2); font-size: var(--step--1); }
.note--flag { border-color: var(--flag); background: var(--flag-bg); padding: var(--s-4); border-radius: var(--radius); color: var(--flag); }
.note--accent { border-color: var(--accent); }
.note--second { border-color: var(--accent-2); background: var(--accent-2-bg); padding: var(--s-4); border-radius: var(--radius); color: var(--accent-2); }
.pill { display: inline-block; font-family: var(--font-mono); font-size: var(--step--2); text-transform: uppercase; letter-spacing: 0.09em; padding: 0.25rem 0.6rem; border: 1px solid var(--rule-strong); border-radius: 999px; color: var(--ink-3); }
.pill--accent { color: var(--accent); border-color: var(--accent); }

/* ---------- The Diagnostic report ---------------------------------------- */
/*
 * THE DELIVERABLE, RENDERED IN THE SAME SYSTEM AS THE PAGE THAT SELLS IT.
 *
 * A Diagnostic arrives as a document, and the sample of it published here is a
 * page. Both are built from these tokens rather than from hexes typed into a
 * one-off stylesheet, for the same reason the letters generate their palette
 * out of this file: **the artefact a client holds is the brand, and a
 * deliverable that drifts from the site is a deliverable nobody checked.**
 *
 * The anatomy is the argument. Every finding separates what was OBSERVED from
 * what is an ASSESSMENT, in different colours, because a report that mixes a
 * measurement with an opinion cannot be checked by the person paying for it.
 */
.rep-doc {
  position: relative;
  max-width: 62rem; margin-inline: auto;
  background: var(--surface);
  border: 1px solid var(--rule); border-radius: var(--radius);
  box-shadow: var(--doc-shadow);
  padding: clamp(1.4rem, 4vw, 3.6rem);
  padding-top: clamp(2rem, 4vw, 3.6rem);
  overflow: hidden;
}
/* The letterhead band. A document from this practice is clay at the top edge. */
.rep-doc::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 6px; background: var(--accent); }

.rep-letterhead {
  display: flex; flex-wrap: wrap; gap: var(--s-4) var(--s-5);
  align-items: flex-end; justify-content: space-between;
  border-bottom: 2px solid var(--ink); padding-bottom: var(--s-4);
}
.rep-letterhead__id { display: flex; align-items: center; gap: 0.7rem; }
.rep-letterhead__mark { width: 2.4rem; flex: none; display: block; color: var(--ink); }
.rep-letterhead__name { display: block; font-family: var(--font-display); font-variation-settings: "opsz" 48; font-weight: 600; font-size: 1.35rem; line-height: 1; letter-spacing: -0.01em; }
.rep-letterhead__sub { display: block; font-family: var(--font-mono); font-weight: 500; font-size: 0.5625rem; letter-spacing: 0.235em; text-transform: uppercase; color: var(--ink-3); margin-top: 0.28rem; }
.rep-letterhead__contact { font-family: var(--font-mono); font-size: var(--step--2); line-height: 1.75; color: var(--ink-3); text-align: right; }
@media (max-width: 34rem) { .rep-letterhead__contact { text-align: left; } }

/* The three arms of the mark, used as the document's section divider. */
.rep-rule { display: flex; flex-direction: column; gap: 4px; margin-block: var(--s-7); }
.rep-rule i { display: block; height: 3px; background: var(--accent); }
.rep-rule i:nth-child(1) { width: 26%; }
.rep-rule i:nth-child(2) { width: 34%; }
.rep-rule i:nth-child(3) { width: 42%; }

.rep-meta {
  display: grid; gap: var(--s-3) var(--s-5); margin-top: var(--s-5);
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  border-block: 1px solid var(--rule); padding-block: var(--s-4);
}
.rep-doc__foot {
  margin-top: var(--s-7); border-top: 2px solid var(--ink); padding-top: var(--s-4);
  font-family: var(--font-mono); font-size: var(--step--2); color: var(--ink-3); line-height: 1.8;
}
.rep-meta__k { display: block; font-family: var(--font-mono); font-size: var(--step--2); text-transform: uppercase; letter-spacing: 0.09em; color: var(--ink-3); }
.rep-meta__v { display: block; font-family: var(--font-mono); font-size: var(--step--1); margin-top: 0.15rem; }

.rep-tally { display: flex; flex-wrap: wrap; gap: var(--s-2); margin: var(--s-5) 0 0; padding: 0; list-style: none; }
.rep-tally__i {
  font-family: var(--font-mono); font-size: var(--step--2); text-transform: uppercase;
  letter-spacing: 0.08em; padding: 0.35rem 0.7rem; border: 1px solid currentColor; border-radius: var(--radius);
}
.rep-tally__i--c { color: var(--critical); background: var(--critical-bg); }
.rep-tally__i--h { color: var(--accent);   background: var(--accent-bg); }
.rep-tally__i--m { color: var(--flag);     background: var(--flag-bg); }
.rep-tally__i--l { color: var(--ink-3);    background: var(--paper-2); }
.rep-tally__i--g { color: var(--good);     background: var(--accent-2-bg); }

/*
 * The verdict is not two equal columns. `.grid-2` split it 50/50, which left
 * the score marooned in a half-empty panel and wrapped the paragraph that
 * matters at forty characters. The score needs the width of a number.
 */
.rep-verdict { display: grid; gap: var(--s-5); grid-template-columns: minmax(0, 1fr) 13rem; align-items: start; }
@media (max-width: 44rem) { .rep-verdict { grid-template-columns: 1fr; } }
.rep-score { text-align: right; }
.rep-score__n { display: block; font-family: var(--font-display); font-weight: 700; font-size: var(--step-5); line-height: 0.9; color: var(--critical); font-variant-numeric: tabular-nums; }
.rep-score__band { display: inline-block; margin-top: var(--s-3); font-family: var(--font-mono); font-size: var(--step--2); text-transform: uppercase; letter-spacing: 0.09em; padding: 0.35rem 0.7rem; border-radius: var(--radius); color: var(--critical); background: var(--critical-bg); }
@media (max-width: 40rem) { .rep-score { text-align: left; } }

.finding {
  border: 1px solid var(--rule); border-left-width: 5px; border-radius: var(--radius);
  background: var(--surface); padding: var(--s-5); margin-top: var(--s-5);
}
.finding--c { border-left-color: var(--critical); }
.finding--h { border-left-color: var(--accent); }
.finding--m { border-left-color: var(--flag); }
.finding--l { border-left-color: var(--ink-3); }
.finding--g { border-left-color: var(--good); }
.finding__head { display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-3); align-items: baseline; }
.finding__sev { font-family: var(--font-mono); font-size: var(--step--2); text-transform: uppercase; letter-spacing: 0.09em; padding: 0.2rem 0.5rem; border-radius: var(--radius); white-space: nowrap; }
.finding--c .finding__sev { color: var(--critical); background: var(--critical-bg); }
.finding--h .finding__sev { color: var(--accent);   background: var(--accent-bg); }
.finding--m .finding__sev { color: var(--flag);     background: var(--flag-bg); }
.finding--l .finding__sev { color: var(--ink-3);    background: var(--paper-2); }
.finding--g .finding__sev { color: var(--good);     background: var(--accent-2-bg); }
.finding__ref { font-family: var(--font-mono); font-size: var(--step--2); color: var(--ink-3); }

.rail { margin-top: var(--s-4); padding-left: var(--s-4); border-left: 2px solid var(--rule); }
.rail--obs { border-left-color: var(--rule-strong); }
.rail--asm { border-left-color: var(--accent); }
.rail--act { border-left-color: var(--accent-2); }
.rail__lbl { display: block; font-family: var(--font-mono); font-size: var(--step--2); text-transform: uppercase; letter-spacing: 0.11em; margin-bottom: var(--s-2); }
.rail--obs .rail__lbl { color: var(--ink-3); }
.rail--asm .rail__lbl { color: var(--accent); }
.rail--act .rail__lbl { color: var(--accent-2); }

/* ---------- Details / FAQ ------------------------------------------------ */
.qa { border-top: 1px solid var(--rule); }
.qa summary {
  cursor: pointer; list-style: none; padding: var(--s-5) 2.5rem var(--s-5) 0;
  font-family: var(--font-display); font-variation-settings: "opsz" 36;
  font-size: var(--step-1); font-weight: 600; letter-spacing: -0.015em; position: relative;
}
.qa summary::-webkit-details-marker { display: none; }
.qa summary:hover { color: var(--accent); }
.qa summary::after {
  content: ""; position: absolute; right: 0.35rem; top: 50%; width: 0.7rem; height: 0.7rem;
  border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3);
  transform: translateY(-70%) rotate(45deg); transition: transform .18s ease;
}
.qa[open] summary::after { transform: translateY(-30%) rotate(-135deg); }
.qa__a { padding-bottom: var(--s-5); max-width: 44rem; color: var(--ink-2); }

/* ---------- Forms -------------------------------------------------------- */
.field { display: grid; gap: 0.4rem; margin-bottom: var(--s-4); }
.field > label, .fieldset > legend { font-weight: 600; font-size: var(--step--1); }
.field .hint { font-size: var(--step--1); color: var(--ink-3); }
/*
 * `is-invalid` was a declared runtime class with no rule behind it and nothing
 * that ever applied it, which is a validation state nobody can see. It exists
 * now because both tools stopped prefilling the page count: a question that is
 * required has to be able to say so, and both forms carry `novalidate` so the
 * browser will not say it for them.
 */
.is-invalid { border-color: var(--flag) !important; outline: 2px solid var(--flag); outline-offset: 1px; }
.field .hint.is-invalid { border: 0; outline: 0; color: var(--flag); font-weight: 500; }
input[type="text"], input[type="email"], input[type="tel"], input[type="number"], input[type="url"], select, textarea {
  /*
   * 16px MINIMUM, and it is not a typographic preference. iOS Safari zooms the
   * whole viewport when a form field smaller than 16px receives focus, and it
   * does not zoom back out. That fires on the contact form, the estimator gate
   * and the scope check, which are the three places money enters. `--step--1`
   * is 13px.
   */
  width: 100%; padding: 0.7rem 0.8rem; font: inherit; font-size: max(1rem, var(--step--1));
  color: var(--ink); background: var(--surface);
  border: 1px solid var(--rule-strong); border-radius: var(--radius);
}
select { appearance: none; background-image: linear-gradient(45deg, transparent 50%, var(--ink-3) 50%), linear-gradient(135deg, var(--ink-3) 50%, transparent 50%); background-position: calc(100% - 18px) 53%, calc(100% - 13px) 53%; background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; padding-right: 2.2rem; }
textarea { min-height: 7.5rem; resize: vertical; }
input:focus-visible, select:focus-visible, textarea:focus-visible { border-color: var(--accent); }
.fieldset { border: 0; padding: 0; margin: 0 0 var(--s-5); }
.choices { display: grid; gap: 0.5rem; margin-top: 0.5rem; }
.choice { display: flex; gap: 0.65rem; align-items: flex-start; padding: 0.7rem 0.8rem; border: 1px solid var(--rule); border-radius: var(--radius); cursor: pointer; background: var(--surface); transition: border-color .15s ease; }
.choice:hover { border-color: var(--rule-strong); }
.choice input { margin-top: 0.3rem; accent-color: var(--accent); flex: none; }
.choice__t { font-size: var(--step--1); }
.choice__d { font-size: var(--step--2); color: var(--ink-3); display: block; margin-top: 0.18rem; }
.choice:has(input:checked) { border-color: var(--accent); background: var(--accent-bg); }
.error { color: var(--accent); font-size: var(--step--1); }

/* ---------- Footer ------------------------------------------------------- */
.foot { border-top: 1px solid var(--rule); padding-block: var(--s-8) var(--s-6); background: var(--paper-2); }
.foot__cols { display: grid; gap: var(--s-6); }
@media (min-width: 48rem) { .foot__cols { grid-template-columns: 1.5fr repeat(4, 1fr); gap: var(--s-5); } }
.foot h2 { font-family: var(--font-mono); font-size: var(--step--2); text-transform: uppercase; letter-spacing: 0.09em; color: var(--ink-3); font-weight: 500; margin-bottom: var(--s-3); }
.foot ul { list-style: none; padding: 0; display: grid; gap: 0.5rem; font-size: var(--step--1); }
.foot a { color: var(--ink-2); text-decoration: none; }
.foot a:hover { color: var(--accent); text-decoration: underline; }
.foot__legal { margin-top: var(--s-7); padding-top: var(--s-5); border-top: 1px solid var(--rule); font-size: var(--step--2); color: var(--ink-3); display: flex; flex-wrap: wrap; gap: var(--s-4); justify-content: space-between; }

/* ---------- Reveal ------------------------------------------------------- *
 * Restrained: 10 pixels and an opacity fade, once, never repeated. Anything
 * that has not been revealed within a second of load is shown regardless, so a
 * failed observer can never hide content. Disabled entirely under
 * prefers-reduced-motion by the media query at the top of this file, and the
 * initial state is only applied when the class is present, which the script
 * adds. With JavaScript off, nothing is hidden.
 */
.js-reveal [data-reveal] { opacity: 0; transform: translateY(10px); }
.js-reveal [data-reveal].is-in { opacity: 1; transform: none; transition: opacity .5s ease, transform .5s ease; }
@media (prefers-reduced-motion: reduce) {
  .js-reveal [data-reveal] { opacity: 1; transform: none; }
}

/* ---------- Target size, WCAG 2.2 AA SC 2.5.8 -----------------------------
 * A link inside a sentence is exempt, because its size is constrained by the
 * line-height of the text around it. A link that IS the whole content of a list
 * item or a standalone line is not exempt, and at a 13px font size it lands at
 * about 16 pixels tall, which is under the 24 the criterion asks for.
 *
 * Padding does the work rather than a min-height, because min-height on an
 * inline element does nothing. The list gaps are pulled in by the same amount
 * so the visual rhythm does not change.
 */
li > a:only-child,
p > a:only-child,
dd > a:only-child,
th > a:only-child,
td > a:only-child,
figcaption > a:only-child,
.foot ul a,
.foot p a,
.nav-drawer a,
.sg-nav a {
  display: inline-block;
  padding-block: 4px;
}
.foot ul { gap: 0.15rem; }
.plain { gap: 0.35rem; }
.sg-nav ul { padding-block: 0.55rem; }

/* ---------- Utility ------------------------------------------------------ */
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.stack-sm > * + * { margin-top: var(--s-2); }
.center { text-align: center; }
.rule { border-top: 1px solid var(--rule); }
/*
 * The margin scale has to cover every step the templates use. `mt-2` and `mt-8`
 * were used on nine pages and defined nowhere, so those gaps simply collapsed.
 */
.mt-2 { margin-top: var(--s-2); }
.mt-8 { margin-top: var(--s-8); }

.grid-2 { display: grid; gap: var(--s-6); }
@media (min-width: 52rem) { .grid-2 { grid-template-columns: repeat(2, 1fr); gap: var(--s-7); } }
.grid-3 { display: grid; gap: var(--s-5); }
@media (min-width: 52rem) { .grid-3 { grid-template-columns: repeat(3, 1fr); } }
.mt-6 { margin-top: var(--s-6); }
.mt-5 { margin-top: var(--s-5); }
.mt-4 { margin-top: var(--s-4); }
.mb-5 { margin-bottom: var(--s-5); }
[hidden] { display: none !important; }

/* ---------- Print ---------------------------------------------------------
 * A proposal, a scope schedule or the pricing page gets printed and taken into
 * a meeting. Chrome is left to hyphenate; everything that cannot survive paper
 * is removed, and every link's destination is printed after it so a printed
 * page is still navigable.
 */
@media print {
  :root {
    --paper: #fff; --paper-2: #fff; --surface: #fff;
    --ink: #000; --ink-2: #222; --ink-3: #444;
    --rule: #bbb; --rule-strong: #888; --accent: #000; --on-accent: #fff;
    --inverse-bg: #fff; --inverse-ink: #000; --inverse-ink-2: #333; --inverse-rule: #bbb;
  }
  .masthead, .nav, .nav-toggle, .nav-drawer, .foot, .skip, .sg-nav, .btn-row, .hero__figure { display: none !important; }
  body { font-size: 11pt; line-height: 1.45; background: #fff; color: #000; }
  .wrap { width: 100%; }
  .section { padding-block: 1.2rem; border-top: 1px solid #ccc; break-inside: auto; }
  .section--inverse, .section--fill { background: #fff !important; color: #000 !important; }
  .spec { grid-template-columns: 1fr; gap: 0.5rem; }
  h1, h2, h3 { break-after: avoid; }
  .step, .product, .comp, .qa { break-inside: avoid; }
  .table-scroll { overflow: visible; border-color: #bbb; }
  .data { min-width: 0; font-size: 9.5pt; }
  details { open: open; }
  details > summary { list-style: none; }
  .qa__a { display: block !important; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-family: var(--font-mono); font-size: 8.5pt; color: #444; word-break: break-all; }
  a[href^="/"]::after { content: " (evolutionwebstudio.com" attr(href) ")"; font-family: var(--font-mono); font-size: 8.5pt; color: #444; }
  .js-reveal [data-reveal] { opacity: 1 !important; transform: none !important; }
}

/* ==========================================================================
   ART DIRECTION
   --------------------------------------------------------------------------
   The house artwork is generated at build time from the page's own URL, in the
   same 5-over-7 diagonal vocabulary as the mark. It is a geological section
   drawing, not a texture: bands of varying thickness, filled solid, hatched,
   ruled or stippled the way a section legend distinguishes one stratum from
   another.

   The reason it is drawn rather than photographed is not budget. Stock
   photography is the single clearest tell that a site came off a template,
   because every studio in the country is buying from the same three libraries.
   Something generated from the brand's own geometry cannot appear anywhere
   else, weighs nothing, themes itself, and is different on every page.
   ========================================================================== */

.art { display: block; color: var(--ink); }
/* The artwork paints with currentColor, so inside the inverted band it has to
   take the inverted ink or every stratum below full opacity disappears into the
   ground and only the accent seam survives. */
.section--inverse .art { color: var(--inverse-ink); }
.section--inverse .art--margin { opacity: .75; }
.art--band { width: 100%; height: 100%; opacity: .5; }
.art--core { width: 100%; height: 100%; }
.art--margin { width: 100%; max-width: 10.5rem; opacity: .95; margin-top: var(--s-4); }

/* A full-bleed band that sits on the join between two sections. */
.band-rule { position: relative; height: 40px; overflow: hidden; }
.band-rule .art { position: absolute; inset: 0; }
.band-rule--flip { transform: scaleY(-1); }
@media (min-width: 52rem) { .band-rule { height: 54px; } }
.section--fill + .band-rule, .band-rule + .section--fill { background: transparent; }

/* The core sample: a drill core run full bleed down an edge, so the page has a
   crafted vertical anchor rather than ending at the container's margin. */
.has-core { position: relative; overflow: hidden; }
.hero__core, .core-gutter { display: none; }
@media (min-width: 64rem) {
  .hero__core, .core-gutter {
    display: block; position: absolute; right: 0; top: 0; bottom: 0;
    width: clamp(46px, 5.5vw, 104px); opacity: .34; pointer-events: none;
  }
  .core-gutter { opacity: .22; }
  .hero__core .art, .core-gutter .art { height: 100%; }
}

/* ==========================================================================
   PAPER GRAIN, WHICH IS NOW A BACKGROUND AND NOT AN OVERLAY.
   ==========================================================================

   It used to be `body::after`: a fixed, full-viewport layer at z-index 60 with
   `mix-blend-mode: multiply`. Over EVERYTHING, which means over the type.
   A blend mode over the type changes the colour of the type, and the contrast
   gate was reading the tokens, which is the colour before the grain rather
   than the colour a person reads.

   Teaching the gate to model the blend found the size of it. At the shipped
   opacity the worst grain pixel took mono micro-labels on a filled section
   from 4.61 to 4.09, links on a filled section from 5.03 to 4.43, and on the
   kit pages it was far worse: `multiply` over an INVERTED section darkens pale
   text a great deal and an already dark background hardly at all, so light on
   dark collapsed. Forty pairs across the eight kits failed, on palettes that
   had been tuned to clear 3.0 and 4.5 with almost no headroom.

   Shaving the opacity does not fix that; it only moves which pair fails first.
   The defect is structural: **a blended layer over the content cannot be
   reconciled with palettes that have no contrast headroom**, and the kit
   palettes are deliberately tight because a kit is a design, not a safety
   margin.

   So the grain is composited into the PAGE BACKGROUND instead, with
   `background-blend-mode`, which blends an element's own background layers
   with each other and never touches a glyph. Anything that paints its own
   background covers it: filled sections, panels, cards, inverse sections and
   every kit surface. What is left is texture on the open paper, which is where
   paper grain belongs anyway, and exactly one set of pairs to check: text
   sitting on `--paper`. `build/contrast_check.mjs` reads this rule and checks
   them.

   Still off entirely for reduced motion, high contrast and print. */
body {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='0.06'/%3E%3C/svg%3E");
  background-blend-mode: multiply;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) body { background-blend-mode: screen; }
}
:root[data-theme="dark"] body { background-blend-mode: screen; }
@media (prefers-reduced-motion: reduce), (prefers-contrast: more) {
  body { background-image: none; }
}
@media print { body { background-image: none; } }

/* ---------- Editorial ----------------------------------------------------- */
.dropcap::first-letter {
  font-family: var(--font-display);
  font-variation-settings: "opsz" 120;
  font-weight: 600;
  float: left;
  font-size: 3.9em;
  line-height: 0.78;
  padding: 0.06em 0.11em 0 0;
  color: var(--accent);
}

.pull {
  margin-block: var(--s-7);
  padding-left: var(--s-5);
  border-left: 3px solid var(--accent);
  max-width: 40rem;
}
.pull p {
  font-family: var(--font-display);
  font-variation-settings: "opsz" 90;
  font-weight: 500;
  font-size: var(--step-3);
  line-height: 1.14;
  letter-spacing: -0.022em;
  color: var(--ink);
}
.pull cite { display: block; margin-top: var(--s-3); font-family: var(--font-mono); font-size: var(--step--2); letter-spacing: .09em; text-transform: uppercase; color: var(--ink-3); font-style: normal; }
.section--inverse .pull p { color: var(--inverse-ink); }
.section--inverse .pull { border-color: var(--inverse-accent); }
.section--inverse .pull cite { color: var(--inverse-ink-2); }

/* A figure that earns its place: a drawn diagram with a real caption. */
.figure { margin: 0; }
.figure svg { width: 100%; height: auto; display: block; }
.figure figcaption { margin-top: var(--s-3); font-size: var(--step--1); color: var(--ink-3); max-width: 34rem; }
.section--inverse .figure figcaption { color: var(--inverse-ink-2); }

/* ==========================================================================
   KIT CARDS
   The index of the eight kits. Each card is rendered IN its kit, so the reader
   is looking at the real typeface and the real palette rather than a swatch
   chart describing them. That is why these live here and not in kit-layout.css:
   the card is studio furniture, the contents are the kit.
   ========================================================================== */
.kit-cards {
  display: grid;
  /*
   * This read `var(--space-6)` for seven releases and there is no `--space-6`
   * on this estate. An unresolvable custom property with no fallback makes the
   * whole declaration invalid, so `gap` fell back to its initial value and the
   * eight kit cards on /kits/ sat flush against each other, on the page the
   * express tier is sold from. `class_check.mjs` checks CLASSES; nothing
   * checked custom properties. `check:comms` now does.
   */
  gap: 2rem;
  grid-template-columns: repeat(auto-fit, minmax(min(21rem, 100%), 1fr));
}
.kit-card {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--k-rule-strong);
  border-radius: var(--k-radius);
  overflow: hidden;
  min-width: 0;
}
.kit-card__fig { aspect-ratio: 12 / 5; background: var(--k-panel); }
.kit-card__fig .kfig { display: block; width: 100%; height: 100%; }
.kit-card__body { padding: 1.6rem; display: flex; flex-direction: column; flex: 1; }
.kit-card__body > .k-btns { margin-top: auto; padding-top: 1.4rem; }
.kit-card .k-h1 a { color: inherit; text-decoration: none; }
.kit-card .k-h1 a:hover { text-decoration: underline; text-underline-offset: .12em; }
.k-swatches { display: flex; gap: .4rem; margin: 1.1rem 0 0; padding: 0; list-style: none; }
.k-swatches li {
  width: 2.1rem; height: 2.1rem;
  background: var(--s);
  border: 1px solid var(--k-rule-strong);
  border-radius: var(--k-radius);
}


/* ---------- The kit bar -------------------------------------------------- */
/*
 * THE ONLY STUDIO CHROME ALLOWED ABOVE A KIT DEMONSTRATION.
 *
 * A kit page used to open with 662 words explaining the kit, in the studio's
 * own typeface, with the demonstration starting a third of the way down. A
 * buyer choosing between eight layouts is doing one thing, which is looking,
 * so the demonstration is now first and this strip is everything that sits
 * above it: which kit, the two prices, and a way to buy without scrolling.
 *
 * It is deliberately a strip and not a hero. Anything taller competes with
 * the thing it is introducing, which is the defect it was written to fix.
 */
.section--strip {
  padding-block: var(--s-5);
  border-bottom: 1px solid var(--rule);
}

.kitbar {
  display: grid;
  gap: var(--s-4) var(--s-6);
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
}
.kitbar__id { min-width: 0; }
.kitbar__act { min-width: 0; }

/*
 * Below 46rem the actions stack under the name rather than squeezing beside
 * it, because two payment buttons and a row of links do not fit a phone at
 * half width and a wrapped button row reads as a mistake.
 */
@media (min-width: 46.01rem) {
  .kitbar__act { text-align: right; }
  .kitbar__act .btn-row { justify-content: flex-end; }
}
@media (max-width: 46rem) {
  .kitbar { grid-template-columns: minmax(0, 1fr); }
}


/* ---------- The small button --------------------------------------------- */
/*
 * FOR AN ACTION INSIDE A TABLE ROW, WHICH IS WHERE THE PRICING PAGE NEEDED ONE.
 *
 * The catalogue table had a price in every row and no way to act on it, so a
 * reader who had just decided went back to a product page to find the button.
 * Seven full sized buttons down a table make the table unreadable, so this is
 * the same button one step down: same colours, same focus ring, same wording
 * discipline, smaller box.
 *
 * It is not a ghost by default. The row's action is the primary thing in that
 * cell and a table full of outlines reads as a table full of maybes.
 */
.btn--sm {
  padding: 0.42rem 0.72rem;
  font-size: var(--step--1);
  white-space: nowrap;
}
