/* ============================================================================
   Launch Social v17 : "Amplemarket system, Launch Social content"
   Warm off-white canvas, a phoenix wash behind the hero, five flat pastel
   surfaces that code the five service pillars, and charcoal drops for rhythm.

   Reading order: fonts, tokens (primitive, semantic, component), base, type,
   layout, controls, chrome, devices, sections, then state and motion.

   Three devices carry the site:
     .reel + .slate   client footage as the object, metadata ruled beneath it
     .tile            flat pastel taxonomy surface, one per service pillar
     .hero::before    the phoenix wash, used twice per page and nowhere else

   Retired from v16: the seaglass signal line, and the entire coastal palette
   (pacific, seaglass, ocean ink, sea mist, warm sand).
   ========================================================================= */

/* ---------------------------------------------------------------- fonts -- */

/* DM Sans: geometric grotesque standing in for Labil Grotesk. Variable, and
   the opsz axis has been instanced out (pinned at 14) because the design uses
   weight only. That takes it from 62,556 to 35,876 bytes, which is smaller
   than the Schibsted Grotesk it replaces. */
@font-face {
  font-family: "Manrope";
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("/assets/fonts/manrope.woff2") format("woff2");
  /* Google's own latin subset, self-hosted. Declares only what the file holds. */
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Clash Display";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/assets/fonts/clash-display.woff2") format("woff2");
}

/* ------------------------------------------------- tokens: layer 1, primitive */

:root {
  /* Taken from the Amplemarket reference unchanged. Raw values live here and
     nowhere else; nothing outside this block should reference a hex. */
  --p-ink: #111111;
  /* The dark ground. The Amplemarket reference doc lists Charcoal #272625, but
     their site's dark sections run far darker, and #272625 is 5.2x the relative
     luminance of a warm near-black: it reads as dark grey rather than black,
     which is why type and footage sat flat on it. Kept warm rather than pure
     #000 so it still feels designed. */
  --p-obsidian: #111111;   /* their --am--primary-dark */
  --p-charcoal: #272625;   /* their --am--secondary-dark */
  --p-canvas: #ffffff;
  --p-cream: #f6f5f3;
  --p-cream-2: #fbfaf9;    /* their --am--secondary-light, between white and cream */
  --p-pearl: #ecebea;
  --p-stone: #b1b1af;

  /* Their seven-step neutral ramp. We had three, which is why secondary text
     was either muted or nothing. --text-muted (#5a5958) already sits within
     four points of their --n-04, so it stays and the ramp is built around it. */
  --n-02: #373634;
  --n-03: #494846;
  --n-04: #5e5c5a;
  --n-05: #787673;
  --n-06: #a7a6a4;
  --n-07: #dcdbda;
  --n-08: #ecebea;

  --p-phoenix: #e8400d;
  --p-phoenix-wash: radial-gradient(386.06% 162.79% at 3.77% -12.1%,
    rgb(232, 64, 13) 0%, rgb(255, 238, 216) 31.14%, rgb(208, 178, 255) 81.98%);

  /* The taxonomy. Five pillars, five colours, flat fills only. */
  --p-petal: #ffd7f0;
  --p-mint: #b7efb2;
  --p-canary: #ffef99;
  --p-violet: #e2ddfd;
  --p-aqua: #99fff9;

  --p-alert: #a3231d;

  /* The phoenix wash composited at 0.55 over white, at its hottest point. Not
     used for painting: it exists so tools/contrast.mjs can assert hero text
     against the worst ground it actually sits on rather than against white. */
  --wash-hot: #f2967a;

  /* ---------------------------------------------- tokens: layer 2, semantic */

  /* Grounds. Four near-identical v16 tints collapse to two, and the rhythm
     now comes from dropping to charcoal: canvas to charcoal is 15.11:1,
     where v16's widest light pair was 1.14:1 and therefore invisible. */
  --ground: var(--p-canvas);
  --ground-soft: var(--p-cream);
  --ground-dark: var(--p-obsidian);
  --ground-dark-2: var(--p-charcoal);  /* raised card on the dark ground */

  --text: var(--p-ink);
  /* Darkened from the reference's #6d6c6b, which fails AA on Soft Violet
     (3.99:1) and Petal Pink (4.05:1). This clears 4.5:1 on all five pastels
     plus both light grounds, worst case 5.32:1. Asserted in contrast.mjs. */
  --text-muted: #5a5958;
  --text-on-dark: var(--p-canvas);
  /* Stone, not ash. The reference specifies ash on charcoal, which is 2.88:1
     and fails outright. Stone is 7.03:1. */
  --text-muted-dark: var(--p-stone);
  /* Warm brown for the eyebrow over the phoenix wash. The wash composites to
     rgb(242,150,122) at its hottest, where #5f4034 is only 4.15:1. */
  --text-on-wash: #4f3228;
  --text-wash-body: #3d3a37;

  --brand: var(--p-phoenix);
  --brand-wash: var(--p-phoenix-wash);

  /* Theirs are 5% on both grounds. Ours were 8% and 14%, which read as rules
     rather than as the suggestion of an edge. --hair-strong is their
     #1111114d, kept for the few places that need a real division. */
  /* Three weights, because they ship three and each does a different job.
     5% is a card edge. 8% is a structural divider that has to be seen. 30% is
     a real division. Collapsing all three to 5% dropped the dark section rules
     from 1.47 to 1.12 against their ground, which is the edge of perceptible. */
  --hair: rgba(17, 17, 17, 0.05);          /* their --am--border-black */
  --hair-medium: rgba(17, 17, 17, 0.08);   /* their --am--borders--black-medium */
  --hair-strong: rgba(17, 17, 17, 0.3);    /* their --am--borders--black-stronger */
  --hair-dark: rgba(255, 255, 255, 0.05);        /* their --am--border-white */
  --hair-dark-medium: rgba(255, 255, 255, 0.08); /* their --white-medium */
  --hair-dark-strong: rgba(255, 255, 255, 0.3);  /* their --white-stronger */

  --focus-ring: var(--p-ink);
  --alert: var(--p-alert);

  /* Type */
  --font: "DM Sans", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  --serif: "Instrument Serif", Georgia, "Times New Roman", serif;

  /* Clamps anchored to the reference's fixed sizes at the top end. The display
     floor is 40px, not 44px: at 44px an 11-character uppercase word overflows
     a 335px container, and /work's "EVERYWHERE." clears it by only 17px. */
  --t-display: clamp(40px, 7vw, 84px);
  --t-h2: clamp(32px, 4.2vw, 56px);
  --t-h3: clamp(26px, 2.8vw, 44px);
  --t-h4: clamp(22px, 2vw, 36px);
  --t-sub: 28px;
  --t-figure: clamp(32px, 4vw, 56px);
  --t-lede: 20px;
  --t-body: 16px;
  --t-small: 14px;
  --t-slate: 12px;
  --t-eyebrow: 10px;

  --w-regular: 400;
  --w-medium: 500;
  --w-black: 900;

  /* Space: 8pt scale, unchanged from v16. It was never the problem. */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 16px;
  --s-4: 24px;
  --s-5: 32px;
  --s-6: 48px;
  --s-7: 64px;
  --s-8: 96px;
  --section-y: clamp(64px, 8vw, 112px);
  --section-y-tight: clamp(44px, 5vw, 72px);

  /* Shape. Exactly two radii do the work, plus pills on badges. This reverses
     v16's no-pill rule deliberately. */
  --r-button: 8px;
  --r-card: 12px;
  --r-input: 12px;
  --r-badge: 9999px;
  /* v16 aliases, kept so nothing inherits a broken radius mid-migration. */
  --r-sm: 8px;
  --r-md: 12px;

  /* Their `.am-container` is 83rem of CONTENT with `.am-section` padding of
     3.5rem outside it, so the content column is a true 1328px. Because
     box-sizing is border-box here, the max-width has to carry the gutters or
     the padding would eat into the column. At a 1440 viewport the two add up
     to exactly 1440. */
  --container: 1328px;
  --gutter: clamp(20px, 4vw, 56px);
  --header-h: 68px;

  /* Motion. Exits faster than entrances, only transform and opacity ever
     animate, and `transition: all` stays banned (check-site enforces it). */
  --dur-press: 120ms;
  --dur-hover: 180ms;
  --dur-reveal: 250ms;
  /* One step above hover, for the two things that are neither a hover nor an
     entrance: the mobile nav panel opening and the cross-document page
     crossfade. Both are a surface arriving, both want to feel deliberate
     without making anyone wait. */
  --dur-enter: 200ms;
  /* The second reveal tier. Section heads and full-width blocks travel further
     (16px rather than 10px) and take longer, so the page has two speeds instead
     of one: the block announces itself, the items inside it just appear. */
  --dur-reveal-lg: 560ms;
  /* The gap between one staggered item and the next. STAGGER_STEP in main.js
     MUST match this value: the JS ramp on [data-stagger] and the CSS ramp on
     the split hero headline are the same rhythm and would read as two different
     systems if they drifted. */
  --stagger-step: 60ms;
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-exit: cubic-bezier(0.4, 0, 1, 1);

  --reveal-delay: 0ms;

  /* Elevation. Cards on white take a hairline, never a drop shadow. */
  /* They light cards from below. This is the --shadow-sm that has been sitting
     in the token block unused since v16, from the same source, never applied. */
  --shadow-card: 0 -6px 6px rgba(17, 17, 17, 0.02),
    0 -23px 9px rgba(17, 17, 17, 0.012);
  --shadow-hairline: rgba(17, 17, 17, 0.05) 0 0 0 1px inset;
  --shadow-lift: rgba(17, 17, 17, 0.12) 0 26px 60px -6px,
    rgba(17, 17, 17, 0.04) 0 6px 6px -3px;
}

/* ------------------------------------------------------------------ base -- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  /* Sticky header plus a breath, so an anchored heading is never tucked under it. */
  scroll-padding-top: calc(var(--header-h) + var(--s-4));
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--ground);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Belt and braces against horizontal overflow from any full-bleed child. */
  overflow-x: hidden;
}

img,
svg,
video {
  display: block;
  max-width: 100%;
}

/* width/height attributes reserve space without freezing the rendered box.
   iframes are deliberately excluded: the booking widget sizes itself by
   postMessage and any height rule here would fight it. */
img {
  height: auto;
}

::selection {
  background: rgba(22, 116, 155, 0.20);
}

:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 3px;
  border-radius: 1px;
}

.on-dark :focus-visible {
  outline-color: var(--text-on-dark);
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--p-ink);
  color: var(--text-on-dark);
  padding: 12px 20px;
  font-weight: var(--w-medium);
}

.skip-link:focus {
  left: 8px;
  top: 8px;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ------------------------------------------------------------------ type -- */

/* The signature of the whole system, and the single biggest change from v16:
   weight 400 at every heading size up to 56px. v16 set all four to 600, which
   is the most common tell that a page came from a template. Authority here
   comes from restraint, not from weight. 900 exists only on .display. */
h1,
h2,
h3,
h4 {
  margin: 0 0 0.5em;
  color: var(--text);
  font-weight: var(--w-regular);
  text-wrap: balance;
}

h1 {
  font-size: var(--t-h2);
  line-height: 1;
  letter-spacing: -0.05em;
}

h2 {
  font-size: var(--t-h2);
  line-height: 1;
  letter-spacing: -0.05em;
}

h3 {
  font-size: var(--t-h4);
  line-height: 1.1;
  letter-spacing: -0.03em;
}

h4 {
  font-size: var(--t-sub);
  line-height: 1.1;
  letter-spacing: -0.017em;
}

/* The one display moment: 84px, weight 900, uppercase, once per page and only
   on the page's own H1. Tracking is negative and the leading is tighter than
   the cap height, so it sets as a poster rather than as a heading.
   The 40px floor is load-bearing: at 44px an 11-character uppercase word
   overflows a 335px container at 375px. */
.display {
  font-size: var(--t-display);
  font-weight: var(--w-black);
  text-transform: uppercase;
  line-height: 0.8;
  letter-spacing: -0.03em;
}

p {
  margin: 0 0 1em;
  /* 55 to 72 characters. Longer than this and the eye loses the line. */
  max-width: 68ch;
}

a {
  color: var(--text);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

strong,
b {
  font-weight: var(--w-medium);
  color: var(--text);
}

.lede {
  font-size: var(--t-lede);
  line-height: 1.4;
  letter-spacing: -0.02em;
  max-width: 52ch;
  color: var(--text-muted);
}

/* 10px uppercase with positive tracking. The reference inverts to positive
   letter-spacing below 12px, which is what keeps small caps legible. */
.eyebrow {
  margin: 0 0 var(--s-3);
  font-size: var(--t-eyebrow);
  font-weight: var(--w-medium);
  line-height: 1;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  color: var(--text-muted);
}

.small {
  font-size: var(--t-small);
  line-height: 1.55;
}

.muted {
  color: var(--text-muted);
}

/* The production slate. Small, technical, legible. Deliberately 12px rather
   than the 10px caps that made the old site squint. */
.slate {
  font-size: var(--t-slate);
  line-height: 1.45;
  font-weight: var(--w-medium);
  letter-spacing: 0.075em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.slate-accent {
  color: var(--text);
}

/* Result numerals. Big, tight, and set against the thing that produced them. */
.figure {
  display: block;
  font-size: var(--t-figure);
  font-weight: var(--w-regular);
  line-height: 0.98;
  letter-spacing: -0.035em;
  color: var(--text);
  font-feature-settings: "ss01";
}

.figure-sm {
  font-size: clamp(28px, 2.6vw, 38px);
}

.pull {
  font-family: var(--serif);
  font-size: clamp(23px, 2.5vw, 34px);
  line-height: 1.28;
  color: var(--text);
  margin: 0;
  max-width: 24ch;
}

.pull-wide {
  max-width: 34ch;
}

/* ---------------------------------------------------------------- layout -- */

.container {
  width: 100%;
  max-width: calc(var(--container) + var(--gutter) * 2);
  margin-inline: auto;
  padding-inline: var(--gutter);
  /* Distance from the container's content edge out to the viewport edge.
     Lets a single child bleed to the edge without leaving the grid. */
  --bleed: calc((100vw - min(100vw, calc(var(--container) + var(--gutter) * 2))) / 2 + var(--gutter));
}

.section {
  padding-block: var(--section-y);
  /* Sections are explicit about their canvas so a bare section can never
     inherit a dark ground and keep light-palette text. */
  background: var(--ground);
}

.section.tight {
  padding-block: var(--section-y-tight);
}

/* One soft ground, not four. v16 had paper, paper-2, mist and sand spanning a
   total of 1.14:1, so the alternation it was built around could not be seen. */
.section.on-soft {
  background: var(--ground-soft);
}

/* The taxonomy colours, available as full-bleed section grounds. Used sparingly:
   canary carries the lead magnet, aqua carries the distribution claim. */
.section.on-canary { background: var(--p-canary); }
.section.on-aqua { background: var(--p-aqua); }

/* ------------------------------------------------------ dark ground mode -- */

.on-dark {
  background: var(--ground-dark);
  color: var(--text-muted-dark);
  /* Focus rings must invert here or they vanish against charcoal. */
  --focus-ring: var(--p-canvas);
}

.on-dark h1,
.on-dark h2,
.on-dark h3,
.on-dark h4,
.on-dark strong,
.on-dark b,
.on-dark .figure,
.on-dark .pull {
  color: var(--text-on-dark);
}

.on-dark p,
.on-dark li,
.on-dark .lede {
  color: var(--text-muted-dark);
}

.on-dark .slate {
  color: var(--text-muted-dark);
}

.on-dark .slate-accent {
  color: var(--text-on-dark);
}

.on-dark a:not(.btn) {
  color: var(--text-on-dark);
}

.on-dark hr,
.on-dark .rule {
  border-color: var(--hair-dark);
}

/* -------------------------------------------------------------- controls -- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  min-height: 48px;
  padding: 12px 22px;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  font: inherit;
  font-size: 16px;
  font-weight: var(--w-medium);
  letter-spacing: -0.004em;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--dur-hover) var(--ease-out),
    border-color var(--dur-hover) var(--ease-out), color var(--dur-hover) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

.btn:active {
  transform: scale(0.97);
}

.btn-primary {
  background: var(--p-ink);
  color: var(--text-on-dark);
}

.btn-secondary {
  background: transparent;
  color: var(--text);
  border-color: var(--hair-strong);
}

/* The primary button is an Ink fill, which is 1.25:1 against the charcoal
   ground: on every dark CTA on the site it was rendering as a near-invisible
   black slab. On dark it inverts to a canvas fill with an Ink label, which is
   15.11:1 against the ground and 18.88:1 for the label. */
.on-dark .btn-primary {
  background: var(--p-canvas);
  color: var(--p-ink);
}

@media (hover: hover) and (pointer: fine) {
  .on-dark .btn-primary:hover {
    background: var(--p-pearl);
  }
}

.on-dark .btn-secondary {
  color: var(--text-on-dark);
  border-color: var(--hair-dark-strong);
}

.btn-lg {
  min-height: 54px;
  padding: 15px 28px;
  font-size: 17px;
}

@media (hover: hover) and (pointer: fine) {
  .btn-primary:hover {
    background: #2b2b2b;
  }

  .btn-secondary:hover {
    border-color: var(--text);
    background: rgba(17, 26, 34, 0.04);
  }

  .on-dark .btn-secondary:hover {
    border-color: var(--text-on-dark);
    background: rgba(244, 240, 231, 0.07);
  }
}

/* The text link with an arrow. Underline grows from the left on hover, which
   is the one link flourish the site allows itself. */
.tlink {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-weight: var(--w-medium);
  color: var(--text);
  text-decoration: none;
  position: relative;
  /* 44px when the link stands alone. Inline in a sentence it is exempt under
     WCAG 2.5.5 and 2.5.8, and there the surrounding line-height governs. */
  min-height: 44px;
  padding-block: 4px;
}

/* Inline in prose, drop back to the text baseline so the sentence does not
   gain 14px of leading around the link. */
p .tlink,
li .tlink {
  min-height: 0;
  display: inline;
}

/* Unless the link IS the paragraph, in which case it is a standalone target
   again and the inline exemption does not apply. */
p > .tlink:only-child,
li > .tlink:only-child {
  display: inline-flex;
  min-height: 44px;
}

/* Same rule for a bare link that is the whole list item: the contact page's
   email and phone are standalone targets, not links inside a sentence. */
.check-list > li > a:only-child {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

p .tlink .arw,
li .tlink .arw {
  margin-left: 4px;
}

.tlink::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: currentColor;
  transform: scaleX(1);
  transform-origin: left;
  transition: transform var(--dur-hover) var(--ease-out);
}

/* An inline SVG, not the &rarr; character. Neither self-hosted font contains
   U+2192, so the text glyph had been silently falling back to a system font on
   every one of the site's ten link arrows. */
.arw {
  width: 14px;
  height: 14px;
  flex: none;
  display: inline-block;
  vertical-align: -2px;
}

.tlink .arw {
  transition: transform var(--dur-hover) var(--ease-out);
}

.on-dark .tlink {
  color: var(--text-on-dark);
}

@media (hover: hover) and (pointer: fine) {
  .tlink:hover .arw {
    transform: translateX(3px);
  }
}

/* Standalone text links in prose keep a normal underline. */
.section a:not(.btn):not(.tlink):not(.logo):not(.reel) {
  text-decoration: underline;
}

/* ---------------------------------------------------------------- header -- */

/* Transparent at rest so the phoenix wash runs up underneath it, then frosted
   glass once the page moves. main.js already toggles .scrolled, so this needed
   no JavaScript change. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: background-color var(--dur-hover) var(--ease-out),
    backdrop-filter var(--dur-hover) var(--ease-out),
    border-color var(--dur-hover) var(--ease-out);
}

.site-header.scrolled {
  background: rgba(255, 255, 255, 0.72);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border-bottom-color: var(--hair);
}

.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  min-height: var(--header-h);
}

.logo {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  font-size: 18px;
  font-weight: var(--w-medium);
  letter-spacing: -0.02em;
  color: var(--text);
  text-decoration: none;
  padding-block: 12px;
  /* Without this the lockup breaks to two lines at 375px. */
  white-space: nowrap;
}

/* The one piece of brand furniture: a small seaglass signal mark. */
/* The one orange moment in the chrome. A non-text mark, so the 4.06:1 it gets
   on canvas clears the 3:1 that non-text UI requires. */
.logo .mark {
  width: 12px;
  height: 12px;
  background: var(--brand);
  border-radius: 3px;
  flex: none;
}

.on-dark .logo,
.site-footer .logo {
  color: var(--text-on-dark);
}

.nav-links {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  list-style: none;
  margin: 0;
  padding: 0;
}

.nav-links > li > a:not(.btn) {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 10px 14px;
  font-size: 15.5px;
  font-weight: 500;
  color: var(--text);
  text-decoration: none;
  border-radius: var(--r-sm);
}

.nav-links a[aria-current="page"]:not(.btn) {
  color: var(--text);
  font-weight: var(--w-medium);
}

.nav-links .btn {
  margin-left: var(--s-2);
  min-height: 44px;
  padding: 10px 18px;
  font-size: 15.5px;
}

@media (hover: hover) and (pointer: fine) {
  .nav-links > li > a:not(.btn):hover {
    color: var(--text);
  }
}

.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin-right: -10px;
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--text);
  cursor: pointer;
}

.nav-toggle .icon-close {
  display: none;
}

.nav-toggle.is-open .icon-open {
  display: none;
}

.nav-toggle.is-open .icon-close {
  display: block;
}

@media (max-width: 900px) {
  .nav-toggle {
    display: inline-flex;
  }

  /* The panel stays in the layout and is hidden by visibility rather than by
     `display`, because a display flip cannot be transitioned: the menu used to
     appear in one frame, fully formed, which is the one interaction on a phone
     that every visitor performs and the only one with no motion at all.

     Nothing is lost by the swap. A visibility-hidden subtree is out of the
     accessibility tree and its links are not focusable, so a closed panel is
     still unreachable by keyboard and invisible to a screen reader. It is also
     inert to the pointer, so the full-screen fixed box cannot swallow taps
     meant for the page.

     Opacity and transform only. Nothing here touches layout, so an open or shut
     menu moves nothing else on the page. */
  .nav-links {
    position: fixed;
    inset: var(--header-h) 0 0 0;
    z-index: 49;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    gap: 0;
    padding: var(--s-4) var(--gutter) var(--s-6);
    background: var(--ground);
    border-top: 1px solid var(--hair);
    overflow-y: auto;
    visibility: hidden;
    opacity: 0;
    transform: translateY(-8px);
    /* Pointer events go the instant the class does, while visibility waits for
       the fade. They are two different questions. A panel that is still on
       screen should still be focusable, so that a keyboard user is not thrown
       out of it mid-transition, but a panel on its way out must never eat the
       tap that comes next: at opacity 0 it is still a full-screen fixed box,
       and without this it swallows everything for 200ms after it closes. */
    pointer-events: none;
    /* Closing: the fade runs, and visibility only flips at the very end. Doing
       it the other way round makes the panel vanish in one frame and there is
       no exit to see. */
    transition: opacity var(--dur-enter) var(--ease-exit),
      transform var(--dur-enter) var(--ease-exit),
      visibility 0s linear var(--dur-enter);
  }

  .nav-links.open {
    visibility: visible;
    opacity: 1;
    transform: none;
    pointer-events: auto;
    /* Opening: visible first, then the fade, so the first focusable link is
       already focusable when main.js focuses it. */
    transition: opacity var(--dur-enter) var(--ease-out),
      transform var(--dur-enter) var(--ease-out),
      visibility 0s;
  }

  .nav-links > li > a:not(.btn) {
    min-height: 56px;
    padding: 14px 0;
    font-size: 21px;
    font-weight: 500;
    border-bottom: 1px solid var(--hair);
    border-radius: 0;
  }

  .nav-links > li:last-child {
    margin-top: auto;
    padding-top: var(--s-5);
  }

  .nav-links .btn {
    width: 100%;
    margin-left: 0;
    min-height: 52px;
    font-size: 17px;
  }
}

/* ============================================================================
   Signature device 1 : the signal line
   A hairline with seaglass nodes threading a sequence of proof points. Vertical
   on narrow screens, horizontal when there is room. Replaces every icon grid.
   ========================================================================= */

.signal {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--s-5);
}

.signal > li {
  position: relative;
  padding-left: var(--s-5);
}

/* The line itself. */
.signal > li::before {
  content: "";
  position: absolute;
  left: 4px;
  top: 10px;
  bottom: calc(var(--s-5) * -1);
  width: 1px;
  background: var(--hair-strong);
}

.signal > li:last-child::before {
  display: none;
}

/* The node. */
.signal > li::after {
  content: "";
  position: absolute;
  left: 0;
  top: 6px;
  width: 9px;
  height: 9px;
  border-radius: 1px;
  background: var(--brand);
}

.on-dark .signal > li::before {
  background: var(--hair-dark-strong);
}

.signal-title {
  display: block;
  font-weight: var(--w-medium);
  color: var(--text);
  margin-bottom: 2px;
}

.on-dark .signal-title {
  color: var(--text-on-dark);
}

.signal p {
  margin: 0;
  max-width: 46ch;
}

/* Horizontal variant: the five process steps. */
@media (min-width: 900px) {
  .signal-row {
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: var(--s-4);
    padding-top: var(--s-5);
  }

  .signal-row > li {
    padding-left: 0;
    padding-top: var(--s-5);
  }

  .signal-row > li::before {
    left: 0;
    right: calc(var(--s-4) * -1);
    top: 4px;
    bottom: auto;
    width: auto;
    height: 1px;
  }

  .signal-row > li:last-child::before {
    display: block;
    right: 0;
  }

  .signal-row > li::after {
    top: 0;
    left: 0;
  }
}

/* Three-column variant: the roadmap phases and the about lessons. Gated at the
   same 900px breakpoint as the five-column row. In v16 these two lists carried
   an inline grid-template-columns with no media query at all, which applied at
   every width and put 28px of horizontal overflow on /roadmap at 375px. */
@media (min-width: 900px) {
  .signal-row.cols-3 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* ============================================================================
   Signature device 2 : the production slate
   Metadata under every piece of work. This is what makes casual vertical phone
   footage read as a studio's output rather than as someone's camera roll.

   The multi-line block form (.slate-block, .slate-client, .slate-result) was
   the caption under each figure in the scattered /work rail. That rail is
   gone, and the stage states the same four facts in the track row and the
   stage caption instead, so the block form went with it. `.slate` itself, the
   single uppercase metadata line, is used across every page and stays.
   ========================================================================= */

/* ============================================================================
   Signature device 3 : the reel
   Client footage, poster first, with no card around it. The clip is the
   object.

   A <button class="reel"> is now always the whole hover target: the
   <figure class="reel-fig"> wrapper it used to sit inside belonged to the
   scattered rail on /work, and both went when /work moved onto the stage
   (device 3c). Every remaining reel lives in a stage phone, where the caption
   is outside the frame and cannot be hovered on the way past.
   ========================================================================= */

.reel {
  position: relative;
  display: block;
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  overflow: hidden;
  aspect-ratio: 9 / 16;
  background: var(--ground-dark-2);
  cursor: pointer;
  color: inherit;
  text-align: left;
  isolation: isolate;
  /* A hairline instead of a shadow: depth without the SaaS card look. */
  box-shadow: 0 0 0 1px var(--hair);
  /* No transform transition here any more. Its only trigger was the rail's
     `.reel-fig:hover .reel` lift, which went with the rail, and a transition
     with nothing to transition is a promise the stylesheet does not keep. */
}

.on-dark .reel {
  box-shadow: 0 0 0 1px var(--hair-dark);
}

.reel img,
.reel video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.reel video {
  opacity: 0;
  transition: opacity var(--dur-reveal) var(--ease-out);
}

.reel.is-playing video {
  opacity: 1;
}

/* Play affordance. A small square mark, not a blurred glass circle. */
.reel-play {
  position: absolute;
  z-index: 2;
  left: 12px;
  bottom: 12px;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border-radius: var(--r-sm);
  background: rgba(17, 26, 34, 0.66);
  color: #fff;
  transition: opacity var(--dur-hover) var(--ease-out), background-color var(--dur-hover) var(--ease-out);
}

.reel-play svg {
  width: 15px;
  height: 15px;
  fill: currentColor;
  margin-left: 2px;
}

.reel.is-playing .reel-play {
  opacity: 0;
}

@media (hover: hover) and (pointer: fine) {
  /* The 3px lift went with the rail. Every reel now sits inside a stage phone,
     and a phone that floats when the pointer crosses it reads as a bug rather
     than as an affordance: the frame is a fixed object on the page. The play
     glyph darkening is the whole hover state now, and it is enough, because
     the clip itself starts looping under the pointer. */
  .reel:hover .reel-play {
    background: var(--p-ink);
  }
}

.reel:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 3px;
}

.on-dark .reel:focus-visible {
  outline-color: var(--text-on-dark);
}

/* The rail and its phone contact-sheet variant lived here. Both were the
   scattered /work layout: twelve-column spans with varying top margins so the
   eye travelled instead of scanning. It was rejected twice, and /work now runs
   the stage (device 3c), which is the only layout that shows client reels on
   this site. Nothing referenced .reel-rail outside that one page. */

/* Lightbox. Native <dialog>, so focus and Escape are handled by the platform;
   main.js adds focus restoration on close. */
.reel-modal {
  /* Sized to actually watch a reel through, not just to preview it. */
  width: min(460px, 92vw, calc((92vh - 76px) * 0.5625));
  max-width: 92vw;
  max-height: 92vh;
  padding: 0;
  border: 0;
  border-radius: var(--r-md);
  background: var(--p-ink);
  color: var(--text-on-dark);
  overflow: hidden;
  box-shadow: var(--shadow-lift);
}

.reel-modal::backdrop {
  background: rgba(10, 15, 20, 0.82);
}

/* A 9:16 clip at 460px wide is 818px tall, which overflows a 800px laptop
   viewport once the controls and caption are added. Cap the video by height
   and let the dialog shrink to fit rather than running off the screen. */
.reel-modal video {
  width: 100%;
  height: auto;
  max-height: calc(92vh - 76px);
  aspect-ratio: 9 / 16;
  object-fit: contain;
  background: #000;
  display: block;
}

.reel-modal-caption {
  margin: 0;
  padding: 14px 18px 18px;
  display: grid;
  gap: 3px;
}

.reel-modal-client {
  font-size: 16px;
  font-weight: var(--w-medium);
  color: var(--text-on-dark);
}

.reel-modal-result {
  font-size: 14px;
  color: var(--text-on-dark);
  font-weight: var(--w-medium);
}

.reel-modal-close {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 3;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: var(--r-sm);
  background: rgba(17, 26, 34, 0.7);
  color: #fff;
  cursor: pointer;
}

/* --------------------------------------------------------------- section -- */

/* Section heads sit on a left axis by default. Centring is reserved for the
   final call to action, which is the only place it earns its keep. */
.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-3) var(--s-5);
  margin-bottom: var(--s-5);
}

/* Both, so a section headed by an h1 (the inner-page mastheads) keeps the
   supporting copy beside it instead of pushing it onto its own row. */
.section-head h1,
.section-head h2 {
  margin: 0;
  max-width: 18ch;
}

.section-head .slate {
  margin-bottom: 10px;
}

.section-head-copy {
  max-width: 44ch;
}

.rule {
  border: 0;
  border-top: 1px solid var(--hair);
  margin: 0;
}

/* ------------------------------------------------------------------ hero -- */

/* The hero is full width and centred, not the v16 asymmetric split. Measured in
   DM Sans 900, the headline at 84px in v16's 560px copy column ran to 5 lines
   and 336px; full width at 1200px it is 3 lines and 202px. The reels move to a
   strip directly beneath, which is how the reference places its video too. */
.hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  /* Danny sized this against production's 48px by eye: roughly an extra
     half inch to an inch of air above the headline and below the button.
     The taller wash also seats the larger display clamp below. */
  padding-top: clamp(56px, 6.5vw, 112px);
  padding-bottom: clamp(56px, 6.5vw, 112px);
  text-align: center;
  background: var(--ground);
}

/* The phoenix wash. The hot orange sits in the top-left, well clear of the
   centred text, which reads against the cream-to-violet body of the gradient.
   Composited at 0.55 over white the hottest point is rgb(242,150,122). */
.hero::before {
  content: "";
  position: absolute;
  inset: -20% -10% auto -10%;
  height: 150%;
  z-index: -1;
  background: var(--brand-wash);
  opacity: 0.55;
  animation: wash 24s ease-in-out infinite alternate;
}

@keyframes wash {
  from { transform: scale(1); }
  to { transform: scale(1.06); }
}

.hero .container {
  position: relative;
}

.hero .eyebrow {
  color: var(--text-on-wash);
}

/* Their hero content is capped at 34.75rem and centred, not run to the
   container edge. Ours cannot go that narrow (this headline is 50 characters,
   which is 5 lines at 556px), but capping it keeps the contained composition
   and holds the 3-line set that was already signed off. Without this the wider
   1328px container pulls it to 2 lines spanning the full width. */
.hero .display {
  max-width: 1120px;
  margin: 0 auto var(--s-4);
  /* Sized so each sentence holds one line at the cap (Danny read the
     three-line balance as bunched): the longer sentence must clear the
     1120px column. The floor holds the 375px rule (longest word is nine
     characters, inside the eleven-char limit). The authored <br> owns the
     break; the splitter passes it through. */
  font-size: clamp(44px, 7vw, 76px);
  line-height: 0.95;
}

.hero .lede {
  margin: 0 auto var(--s-5);
  color: var(--text-wash-body);
  max-width: 58ch;
  text-wrap: balance;
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  justify-content: center;
}

/* The reassurance note under the hero CTA. It sits on the wash, where
   --text-muted is only 3.13:1; --text-wash-body is the wash's muted tier,
   5.07:1 at the hottest point, asserted by the contrast gate. */
.hero-note {
  margin: var(--s-3) auto 0;
  font-size: var(--t-small);
  line-height: 1.55;
  color: var(--text-wash-body);
  max-width: 46ch;
}

/* The reel strip that now sits under the hero rather than beside it. At
   1440x900 it starts around 568px, leaving ~300px of clip visible at the fold,
   so the page still shows motion without scrolling. */
.reel-strip {
  padding-top: var(--s-2);
  padding-bottom: clamp(48px, 6vw, 80px);
  background: var(--ground);
}

.reel-grid {
  display: grid;
  gap: clamp(12px, 1.5vw, 20px);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

@media (min-width: 768px) {
  .reel-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* ---------------------------------------------------- the taxonomy tiles -- */

/* The five How it works steps, and nothing else on the site uses these classes.

   They were five flat pastel slabs in a row. The section is a charcoal stage
   now, and a pastel fill on charcoal is a lightbox: five of them side by side
   is a paint chart, not a taxonomy. So the pillar colour stops being the fill
   and becomes a 24 by 3 accent tick per step, which is enough to keep the
   mapping to the services cards, the comparison table and the FAQ groups
   legible without lighting the section up.

   This block is now the ONLY layout for the section: a stacked list of five
   cards on the dark ground, all five visible, on every viewport. It used to be
   the resting state under a desktop stage; that stage was removed on
   2026-08-31 and the note further down says why. Nothing here is gated on
   script, motion preference or viewport width. */
.tiles {
  display: grid;
  gap: var(--s-2);
  /* The steps are an <ol>, so the order is carried semantically. The visible
     numbering is .tile-step, so the markers and their indent are reset. */
  list-style: none;
  margin: 0;
  padding: 0;
}

/* No transition and no hover lift, deliberately, and this is a change from the
   pastel row. A 2px lift under the pointer is a claim that something can be
   clicked, and these are five list items with no link and no control in them.
   On the row it was at least a playful accent on a small card; on a full-width
   stacked card it is a wobble that promises an action that does not exist. It
   also has nowhere left to fire: above 900px the stage takes the pointer out of
   play entirely, so the lift only ever reached a desktop window under 900px
   wide. Deleting it also means the scrub owns `transform` on these outright,
   with no easing curve left running underneath it to fight the scroll. */
.tile {
  border-radius: var(--r-card);
  background: var(--ground-dark-2);
  padding: var(--s-4);
}

/* The pillar tick. Non-text and decorative, so the five pastels only have to be
   visible against the card, which they clear by a wide margin: the tightest of
   them, Soft Violet and Mint, are 11.51:1 on the raised charcoal card. */
.tile::before {
  content: "";
  display: block;
  width: 24px;
  height: 3px;
  margin-bottom: var(--s-3);
  border-radius: 2px;
  background: var(--pillar);
}

.tile-step {
  margin: 0 0 var(--s-3);
  font-size: var(--t-slate);
  font-weight: var(--w-medium);
  letter-spacing: 0.3px;
  text-transform: uppercase;
  /* Stone, not --text-muted: these cards only ever sit on the dark ground now,
     and #5a5958 on charcoal is 1.9:1. Asserted in tools/contrast.mjs. */
  color: var(--text-muted-dark);
}

.tile h3 {
  margin-bottom: 10px;
  font-size: var(--t-sub);
  color: var(--text-on-dark);
}

.tile p {
  margin: 0;
  max-width: none;
  font-size: var(--t-small);
  line-height: 1.45;
  color: var(--text-muted-dark);
}

/* The charcoal stage is gone. `.howto-scrub` and `.howto-rail` used to live
   here: a desktop-only stage that pinned the How it works section and showed
   one step at a time, driven by ScrollTrigger in js/motion.js.

   Removed 2026-08-31. It held the viewport for 2,700px of scroll, 29% of the
   page, to present five short paragraphs that already read completely as the
   stacked list below, which is what every phone was getting. The client read
   the hold as the page being stuck, and it was: the stillness bought no
   information. The five steps and their pillar ticks are unchanged; only the
   stage that sat on top of them is gone.

   `.tiles` and `.tile` above are now the only rendering of this section, on
   every viewport. */

/* Pillar to colour. This mapping is the taxonomy and it must stay consistent
   wherever a pillar appears: tiles, services cards, comparison rows, FAQs. It
   is a custom property rather than a background now, because on the charcoal
   ground the pillar colour is carried by the tick and not by a fill. */
/* The one line that names the product and reaches /services, sitting under the
   five steps. Body register, not a heading: it is a footnote to the process,
   not a sixth step. */
.howto-more {
  margin: clamp(32px, 4vw, 48px) 0 0;
  max-width: 62ch;
  color: var(--text-muted-dark);
}

.t-strategy { --pillar: var(--p-violet); }
.t-shoot { --pillar: var(--p-canary); }
.t-edit { --pillar: var(--p-petal); }
.t-publish { --pillar: var(--p-aqua); }
.t-measure { --pillar: var(--p-mint); }

/* v16's split-hero layout (.hero-grid, .hero-copy, .hero-media, .hero-reels)
   is retired with the restructure. The hero is now a single centred column and
   the clips live in .reel-strip beneath it. */

/* ----------------------------------------------------------- proof strip -- */

/* The margin-top here was spacing the strip off the reels that used to sit
   above it. With those cut, the section's own padding does the work and the
   margin just left a dead band under the hero. */
.proof {
  border-top: 1px solid var(--hair-medium);
  border-bottom: 1px solid var(--hair-medium);
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

@media (min-width: 900px) {
  .proof {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

.proof > * {
  padding: var(--s-4) var(--s-4) var(--s-4) 0;
  border-right: 1px solid var(--hair);
}

.proof > *:nth-child(2n) {
  border-right: 0;
  padding-left: var(--s-4);
}

@media (min-width: 900px) {
  .proof > *:nth-child(2n) {
    border-right: 1px solid var(--hair);
    padding-left: var(--s-4);
  }

  .proof > *:not(:first-child) {
    padding-left: var(--s-4);
  }

  .proof > *:last-child {
    border-right: 0;
  }
}

.proof .figure {
  font-size: clamp(26px, 2.6vw, 36px);
  margin-bottom: 6px;
}

.proof .slate {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 500;
  font-size: 14px;
  color: var(--text-muted);
  max-width: 22ch;
}

/* ----------------------------------------------------------- trust strip -- */

/* ------------------------------------------------------------- quote cards -- */

/* Two equal cards on the charcoal ground, one raised step above it so the card
   edge is visible without a border. Both quotes carry the same weight: the old
   split set one at 34px serif and the other at small grey sans, which read as
   one quote being deliberately de-emphasised. */
.quotes {
  display: grid;
  gap: clamp(16px, 2vw, 24px);
}

@media (min-width: 860px) {
  .quotes {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* #272625 on the #111 ground is only 1.25:1, so the card is not defined by
   being lighter. The 1px 5% white border is what draws the edge. That is their
   mechanism and it is why their dark sections read layered where a luminance
   step reads flat. Do not "fix" the low ratio by lightening the card. */
.quote-card {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  padding: clamp(24px, 3vw, 32px);
  border: 1px solid var(--hair-dark);
  border-radius: var(--r-card);
  background: var(--ground-dark-2);
}

.quote-card blockquote {
  margin: 0;
  flex: 1;
  font-family: var(--serif);
  font-size: clamp(20px, 1.9vw, 26px);
  line-height: 1.35;
  letter-spacing: -0.01em;
  color: var(--text-on-dark);
}

.quote-card .byline {
  margin: 0;
}

.quotes-cta {
  margin: var(--s-6) 0 0;
  max-width: none;
  text-align: center;
}

/* ------------------------------------------------- testimonial marquee --
   The Amplemarket-style rotating wall, adapted honestly. Theirs is three
   vertical columns scrolling continuously behind a fade mask, which works
   because they have dozens of quotes. Launch Social has three published
   testimonials, so a three-column wall would visibly repeat itself every few
   seconds. This is a single horizontal track instead: it loops seamlessly, it
   reads as deliberate motion rather than as a thin wall, and it scales to more
   quotes later by adding cards. */
.marquee {
  position: relative;
  overflow: hidden;
  /* Fades the track into the ground at both edges, the way the reference does
     vertically. */
  -webkit-mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
  mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
}

.marquee-track {
  display: flex;
  gap: var(--s-3);
  width: max-content;
  /* Scales with the number of cards: the track translates half its own width,
     so a longer track at a fixed duration would scroll faster. Roughly 16s per
     card keeps the reading pace constant. Five reviews, so 80s. */
  animation: marquee 80s linear infinite;
}

.marquee:hover .marquee-track,
.marquee:focus-within .marquee-track {
  animation-play-state: paused;
}

@keyframes marquee {
  /* The track holds the set twice, so translating exactly half is seamless. */
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

.marquee .quote-card {
  width: min(78vw, 30rem);
  flex: none;
}

/* Motion that runs forever must stop for anyone who asks it to. Stopping it is
   only half the job though: a still track leaves quotes three, four and five
   sitting off the right edge of a container with `overflow: hidden`, so the
   accommodation for reduced motion was quietly hiding two thirds of the
   testimonials from the people who asked for it.

   So the row becomes a real horizontal scroller instead: the track keeps its
   max-content width, the mask comes off so nothing looks clipped, and the
   duplicate half is dropped. The duplicate exists purely so translateX(-50%)
   lands on its twin, and with the animation gone it is five identical quotes
   the visitor has to scroll past twice. */
@media (prefers-reduced-motion: reduce) {
  .marquee-track {
    animation: none;
    width: max-content;
  }

  .marquee {
    overflow-x: auto;
    -webkit-mask-image: none;
    mask-image: none;
  }

  .marquee-track .quote-card[aria-hidden="true"] {
    display: none;
  }
}

/* Phones get their hands on the wheel: below the desktop breakpoint the same
   conversion the reduced-motion block makes, made for a different reason.
   The drift's pause lives on hover, and a phone has no hover, so a mobile
   visitor could never read a review at their own pace; Danny asked for a
   swipeable row. The peek of the next card is the whole affordance, the
   snap is proximity so a long flick still travels, and the scrollbar chrome
   stays hidden. Inline padding puts the resting cards on the site grid, and
   containing overscroll keeps a hard swipe from triggering the browser's
   back gesture. Also contains the track's width on phones, which is where
   the page's scrollWidth inflation came from. */
@media (max-width: 899px) {
  .marquee {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    padding-inline: var(--gutter);
    scroll-padding-inline: var(--gutter);
    -webkit-mask-image: none;
    mask-image: none;
    scrollbar-width: none;
  }

  .marquee::-webkit-scrollbar {
    display: none;
  }

  .marquee-track {
    animation: none;
    width: max-content;
  }

  .marquee-track .quote-card {
    scroll-snap-align: center;
  }

  .marquee-track .quote-card[aria-hidden="true"] {
    display: none;
  }
}

/* ------------------------------------------------------------ case study -- */

/* Option F, "the ledger". The transformation still carries the section, but
   the strata are gone: a ruled masthead names the case, the story column
   reads outcome, meaning, voice, action in one pass, and the evidence rail
   beside it holds the reserved film slot and the three numbers we can verify
   ourselves. The client-attributed $100k stays in the story sentence with
   its caveat two elements away, mirroring how /work separates his count from
   our screenshots. */

.case-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2) var(--s-4);
  padding-bottom: var(--s-3);
  border-bottom: 1px solid var(--hair-dark-medium);
  margin-bottom: clamp(40px, 5vw, 64px);
}

/* The masthead pair: the label at the section-title register (Danny asked
   for it above the eyebrow tier, and the h4 clamp keeps it subordinate to
   the 92px transformation below), the client line staying micro meta on the
   shared baseline. `.on-dark h2` supplies the label's canvas white; the
   client drops to stone so size and colour carry the same hierarchy. */
.case-head h2 {
  margin: 0;
  font-size: var(--t-h4);
  line-height: 1;
  /* Danny's call: bold, white, no period. The black weight is otherwise
     reserved for .display, but the masthead now speaks in the same voice
     as the 900-weight numerals it introduces. */
  font-weight: var(--w-black);
}

.case-head .eyebrow {
  margin: 0;
  color: var(--text-muted-dark);
}

.case-grid {
  display: grid;
  gap: clamp(40px, 5vw, 64px) clamp(40px, 5vw, 72px);
  grid-template-areas:
    "lead"
    "rail"
    "close";
}

@media (min-width: 900px) {
  .case-grid {
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
    grid-template-areas:
      "lead rail"
      "close rail";
    /* The rail spans both rows and is taller than the two left-hand blocks
       put together, so with two auto rows the browser split its surplus
       height between them and injected ~175px of dead space between the stat
       line and the quote. align-items: start could not help: the items were
       already at the top of rows that were themselves too tall. Row one is
       now exactly its content and row two absorbs the remainder. */
    grid-template-rows: auto 1fr;
    align-items: start;
  }
}

.case-lead { grid-area: lead; }
.case-close { grid-area: close; }

/* `.case-move`, the 3,000 -> 7,500+ display counter with the arrow between the
   two figures, used to open this section. Removed 2026-08-31: it made a
   follower count the first thing the reader met, and a follower count is not
   what this buyer is shopping for. The same growth is still here and still
   counts up, one column right, as a `.case-stat` among the other figures.
   What leads now is `.case-outcome` below.

   The setup, the outcome, then the evidence. `.case-setup` is the situation the
   reader is meant to recognise in themselves, so it is quiet and it comes
   first. `.case-outcome` is the display register for a sentence rather than a
   numeral: the heading face at h2, one step under the old counter, capped
   narrow so it stays a statement and never becomes a paragraph. */
.case-setup {
  margin: 0 0 var(--s-3);
  font-size: var(--t-lede);
  line-height: 1.5;
  color: var(--text-muted-dark);
  max-width: 34ch;
}

.on-dark .case-outcome,
.case-outcome {
  margin: 0;
  font-family: var(--serif);
  font-weight: var(--w-black);
  font-size: var(--t-h2);
  line-height: 1.06;
  letter-spacing: -0.03em;
  color: var(--text-on-dark);
  max-width: 20ch;
}

.case-sub {
  margin: var(--s-4) 0 0;
  font-size: var(--t-lede);
  line-height: 1.5;
  color: var(--text-muted-dark);
  max-width: 42ch;
}

.case-quote {
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(22px, 2vw, 30px);
  line-height: 1.32;
  letter-spacing: -0.01em;
  color: var(--text-on-dark);
  max-width: 44ch;
}

.case-close .byline { margin-top: var(--s-4); }

.case-cta {
  margin-top: var(--s-5);
  display: grid;
  gap: 12px;
  justify-items: start;
}

.case-caveat {
  margin: 0;
  font-size: var(--t-slate);
  color: var(--text-muted-dark);
  max-width: 52ch;
}

.case-caveat a { color: inherit; }

/* The evidence rail: the reserved film slot, then the numbers as a ledger.
   The area is named explicitly: the two placed items either side of it leave
   no free cell in row one, and an auto-placed rail would take a single row
   instead of running the height of the story column. */
.case-rail {
  grid-area: rail;
  display: grid;
  align-content: start;
}

.case-media { margin: 0 0 var(--s-4); }

.case-stat {
  padding: var(--s-4) 0;
  border-top: 1px solid var(--hair-dark-medium);
}

.case-stat:last-child { padding-bottom: 0; }

/* Ink, not Phoenix. These figures were set in the brand orange briefly on
   2026-08-31 and taken straight back to ink: three orange numerals stacked in
   a rail read as a pricing table rather than as evidence, and the accent stops
   meaning "the thing to press" once numbers wear it too. The colour comes from
   `.on-dark .figure` above, which resolves to Ink on this band. Do not
   reintroduce `color: var(--brand)` here without re-running contrast.mjs: it
   passes at 3.50:1 only because these are large, and it would fail the moment
   the clamp floor drops under 24px.

   The size and weight stay where the accent left them. That is what carries
   the emphasis now, and it costs no contrast to do it.

   Each figure counts up from data-count-from to data-count-to the first time
   it scrolls into view. Both values are in the markup: with JS off or reduced
   motion on the final figure is simply there, and nothing is hidden. */
.case-stat .figure {
  display: block;
  font-size: clamp(30px, 2.9vw, 40px);
  line-height: 1;
  letter-spacing: -0.04em;
  font-weight: var(--w-black);
  font-variant-numeric: tabular-nums;
}

.case-stat .slate {
  display: block;
  margin-top: 8px;
  color: var(--text-muted-dark);
}

/* The testimonial, live: a portrait reel at rail width, driven entirely by
   the shared .reel module. Centred and capped so the 9:16 card reads as an
   object in the rail rather than stretching the grid, which was the disease
   the ledger redesign cured. */
.case-testi {
  width: min(260px, 100%);
  margin: 0 auto var(--s-4);
}

.case-film-caption {
  margin-top: 12px;
  font-size: var(--t-slate);
  color: var(--text-muted-dark);
}

/* -------------------------------------------------------------------- VSL -- */

/* The reference's Hero Video Container: black ground, 12px radius, 16:9, full
   container width, centered play button. Sits between the hero and the proof
   strip, in the slot the duplicate reel strip used to occupy. */
.vsl-section {
  padding-top: 0;
  background: var(--ground);
}

.vsl-frame {
  margin: 0;
}

.vsl {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  padding: 0;
  border: 0;
  border-radius: var(--r-card);
  overflow: hidden;
  background: #000;
  cursor: default;
}

/* The whole poster is a click target for pointer users; the button inside is
   the accessible control. Once the film is playing, clicks belong to the
   native video controls and the module ignores the frame. */
.vsl[data-src] {
  cursor: pointer;
}

.vsl.is-playing {
  cursor: default;
}

.vsl img,
.vsl video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* The film sits over the poster, silent about its existence until played:
   no pointer events, no paint. First activation fades it in over the poster,
   the same handoff the reels use, and hands the pointer to the native
   controls. */
.vsl video {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-reveal) var(--ease-out);
}

.vsl.is-playing video {
  opacity: 1;
  pointer-events: auto;
}

/* Rendered only under `.js` and only while a data-src exists, so the page can
   never promise a film that is not there or a control that cannot act. That
   was the second finding of the v14 audit. */
.vsl-play {
  display: none;
}

.js .vsl[data-src] .vsl-play {
  display: grid;
  place-items: center;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 72px;
  height: 72px;
  padding: 0;
  border: 0;
  border-radius: var(--r-badge);
  background: rgba(255, 255, 255, 0.92);
  color: var(--p-ink);
  font: inherit;
  cursor: pointer;
  transition: transform var(--dur-hover) var(--ease-out);
}

.js .vsl.is-playing .vsl-play {
  display: none;
}

.vsl-play svg {
  width: 28px;
  height: 28px;
  fill: currentColor;
  margin-left: 3px;
}

@media (hover: hover) and (pointer: fine) {
  .js .vsl[data-src]:not(.is-playing):hover .vsl-play {
    transform: translate(-50%, -50%) scale(1.06);
  }
}

/* ------------------------------------------------------------ trust strip -- */

/* Was a single cramped flex row with 18px logos that read as an afterthought.
   Now a hairline-ruled grid on the cream ground, which also supplies the
   light-to-soft step in the homepage rhythm before the first charcoal drop. */
.trust {
  padding-block: var(--section-y-tight);
  background: var(--ground-soft);
}

/* Not an eyebrow any more: at 10px it read as a caption apologising for the
   logos underneath it. `max-width: none` matters, because the base p rule caps
   paragraphs at 68ch and text-align:center was centring inside that box rather
   than across the container, which is why it looked left aligned. */
.trust-label {
  max-width: none;
  text-align: center;
  margin-bottom: var(--s-5);
  font-size: var(--t-small);
  letter-spacing: 0.1em;
  color: var(--text);
}

/* 1px gaps over a hairline ground draw the rules for free, the same technique
   the proof strip uses, so the two sections read as one family. */
.trust-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1px;
  background: var(--hair-medium);
  border-block: 1px solid var(--hair-medium);
}

@media (min-width: 600px) {
  .trust-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@media (min-width: 1024px) {
  .trust-grid {
    grid-template-columns: repeat(5, minmax(0, 1fr));
  }
}

.trust-grid > li {
  display: grid;
  place-items: center;
  min-height: 132px;
  padding: var(--s-4) var(--s-3);
  background: var(--ground-soft);
}

/* An odd count leaves a gap in the last row on the two and three column
   layouts. Stretching the final cell keeps the block rectangular. */
@media (max-width: 599px) {
  .trust-grid > li:last-child {
    grid-column: span 2;
  }
}

.trust-grid img {
  width: auto;
  /* Roughly 1.6x the old sizes. These are proof, so they get to be legible. */
  opacity: 0.78;
  transition: opacity var(--dur-hover) var(--ease-out);
}

.trust-grid:hover img {
  opacity: 0.5;
}

.trust-grid li:hover img {
  opacity: 1;
}

/* The two stacked marks need more height than the horizontal ones: their
   wordmark is only about a fifth of the artwork, so matching Ticor's height
   would render them unreadable. */
.trust-grid .tl-mm { height: 56px; }
.trust-grid .tl-ticor { height: 28px; }
.trust-grid .tl-randa { height: 62px; }
.trust-grid .tl-elegant { height: 38px; }
.trust-grid .tl-clear { height: 42px; }

@media (max-width: 599px) {
  .trust-grid > li { min-height: 104px; }
  .trust-grid .tl-mm { height: 44px; }
  .trust-grid .tl-ticor { height: 22px; }
  .trust-grid .tl-randa { height: 48px; }
  .trust-grid .tl-elegant { height: 30px; }
  .trust-grid .tl-clear { height: 34px; }
}

/* ------------------------------------------------------------ case study -- */

.case-figures {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-4);
  padding-bottom: var(--s-6);
  margin-bottom: var(--s-6);
  border-bottom: 1px solid var(--hair-dark);
}

@media (min-width: 760px) {
  .case-figures {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

.case-body {
  display: grid;
  gap: var(--s-6);
}

@media (min-width: 1000px) {
  .case-body {
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: var(--s-6) var(--s-5);
    align-items: start;
  }

  .case-narrative {
    grid-column: 1 / span 5;
  }

  .case-visual {
    grid-column: 7 / span 6;
  }
}

/* The contact sheet: the reel plus the receipts, hairline framed and captioned
   rather than blown up. The screenshots are genuinely low resolution and the
   honest treatment is to present them at the size they actually are. */
.contact-sheet {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-3);
}

.contact-sheet figure {
  margin: 0;
  display: grid;
  gap: 8px;
  align-content: start;
}

.contact-sheet img {
  width: 100%;
  height: auto;
  border-radius: var(--r-sm);
  box-shadow: 0 0 0 1px var(--hair-dark);
  background: var(--ground-dark-2);
}

.contact-sheet figcaption {
  font-size: 12px;
  line-height: 1.45;
  color: var(--text-muted-dark);
}

.attribution-note {
  margin-top: var(--s-4);
  padding: var(--s-3) var(--s-4);
  border-left: 2px solid var(--brand);
  background: rgba(244, 240, 231, 0.05);
  font-size: 14px;
  line-height: 1.55;
  color: var(--text-muted-dark);
}

.attribution-note p {
  margin: 0;
  max-width: 58ch;
}

/* ---------------------------------------------------------- quote blocks -- */

.quote-lead {
  display: grid;
  gap: var(--s-4);
  padding-top: var(--s-6);
  margin-top: var(--s-6);
  border-top: 1px solid var(--hair);
}

.on-dark .quote-lead {
  border-top-color: var(--hair-dark);
}

.byline {
  display: flex;
  align-items: center;
  gap: 12px;
}

.byline img,
.byline .monogram {
  width: 52px;
  height: 52px;
  flex: none;
  border-radius: var(--r-sm);
  object-fit: cover;
}

/* Randa has no headshot. Rather than mix photos with a bare initial, the
   monogram is designed as a deliberate tile at exactly the portrait size, so
   the row reads as one system instead of an unfinished one. */
.byline .monogram {
  display: grid;
  place-items: center;
  background: var(--p-ink);
  color: var(--text-on-dark);
  font-size: 20px;
  font-weight: var(--w-medium);
  letter-spacing: -0.02em;
}

.on-dark .byline .monogram {
  background: var(--ground);
  color: var(--text);
}

.byline-name {
  font-weight: var(--w-medium);
  color: var(--text);
  line-height: 1.3;
}

.on-dark .byline-name {
  color: var(--text-on-dark);
}

.byline-role {
  font-size: 13.5px;
  line-height: 1.4;
  color: var(--text-muted);
}

.on-dark .byline-role {
  color: var(--text-muted-dark);
}

/* An editorial aside: the one serif statement per section, set against a
   photograph rather than against empty space. */
.aside-split {
  display: grid;
  gap: var(--s-5);
  align-items: center;
  margin-top: var(--s-6);
  padding-top: var(--s-5);
  border-top: 1px solid var(--hair);
}

@media (min-width: 860px) {
  .aside-split {
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: var(--s-6);
  }

  .aside-split > *:first-child {
    grid-column: 1 / span 6;
  }

  .aside-split > *:last-child {
    grid-column: 8 / span 5;
  }
}

.aside-split figure {
  margin: 0;
  display: grid;
  gap: 10px;
}

/* A tall behind-the-scenes frame beside a quote would otherwise set the height
   of the whole band. Cropped from the top, which is where the action is.
   Scoped to the figure on purpose: a bare `.aside-split img` has the same
   specificity as `.byline img` and, being later in the file, would win and
   blow a 52px portrait up to full column width. */
.aside-split figure img {
  width: 100%;
  height: auto;
  max-height: 420px;
  object-fit: cover;
  object-position: 50% 30%;
  border-radius: var(--r-sm);
}

/* One quote set large in the serif, one at reading size beside it. Two voices
   at two weights, rather than two identical cards. */

@media (min-width: 860px) {
  .quote-split {
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: var(--s-6);
    align-items: start;
  }

  .quote-split > *:first-child {
    grid-column: 1 / span 7;
  }

  .quote-split > *:last-child {
    grid-column: 9 / span 4;
    padding-left: var(--s-5);
    border-left: 1px solid var(--hair);
  }
}

.quote-split blockquote:not(.pull) {
  margin: 0 0 var(--s-4);
  font-size: 16px;
  line-height: 1.55;
  color: var(--text);
  max-width: 44ch;
}

/* -------------------------------------------------------------- packages -- */

/* A typographic price list. No cards, no borders around each tier: the
   difference between them is stated in words and set in type. */
.tiers {
  border-top: 1px solid var(--hair);
}

.tier {
  display: grid;
  gap: 6px var(--s-5);
  padding: var(--s-5) 0;
  border-bottom: 1px solid var(--hair);
  align-items: baseline;
}

@media (min-width: 760px) {
  .tier {
    grid-template-columns: minmax(0, 15rem) minmax(0, 1fr) auto;
    align-items: center;
  }
}

.tier-name {
  font-size: clamp(20px, 1.8vw, 25px);
  font-weight: var(--w-medium);
  letter-spacing: -0.02em;
  color: var(--text);
  line-height: 1.2;
}

.tier-what {
  margin: 0;
  max-width: 42ch;
  color: var(--text);
}

.tier-price {
  font-size: clamp(24px, 2.2vw, 32px);
  font-weight: var(--w-medium);
  letter-spacing: -0.03em;
  color: var(--text);
  white-space: nowrap;
}

.tier-price .per {
  font-size: 14px;
  font-weight: 500;
  color: var(--text-muted);
  letter-spacing: 0;
}

/* The recommended tier is marked, not boxed. The bleed is tied to the gutter
   rather than to a fixed value: at 375px the gutter is 20px, so a hardcoded
   24px pushed 4px of horizontal overflow across the whole document. */
.tier.is-pick {
  background: var(--ground-soft);
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
}

.tier-mark {
  display: inline-block;
  margin-bottom: 6px;
  padding: 3px 8px;
  border-radius: var(--r-sm);
  background: var(--brand);
  color: #fff;
  font-size: 11.5px;
  font-weight: var(--w-medium);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* The terms sit in two columns beside the price list rather than as one narrow
   paragraph with an empty half page next to it. */
.terms {
  display: grid;
  gap: var(--s-3) var(--s-6);
  margin-top: var(--s-5);
}

@media (min-width: 860px) {
  .terms {
    grid-template-columns: repeat(12, minmax(0, 1fr));
  }

  .terms > *:first-child {
    grid-column: 1 / span 6;
  }

  .terms > *:last-child {
    grid-column: 8 / span 5;
  }
}

.terms-line {
  margin: 0;
  max-width: 54ch;
  color: var(--text);
}

/* --------------------------------------------------------------- founder -- */

.founder {
  display: grid;
  gap: var(--s-6);
  align-items: center;
}

@media (min-width: 900px) {
  .founder {
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: var(--s-6);
  }

  .founder-photo {
    grid-column: 1 / span 5;
  }

  .founder-photo img {
    border-radius: 0 var(--r-sm) var(--r-sm) 0;
  }

  .founder-copy {
    grid-column: 7 / span 5;
  }
}

/* The portrait runs off the left edge of the page, but only while the bleed is
   small enough to stay a portrait. --bleed grows with the viewport, so past
   ~1400px an uncapped bleed turns a 3:4 photograph into a wide letterbox with a
   small person in it, and a capped bleed leaves the image floating in the
   margin, which reads as an avatar rather than a photograph. So: it either
   touches the edge or it aligns to the grid, never in between. */
@media (min-width: 900px) and (max-width: 1400px) {
  .founder-photo {
    margin-left: calc(var(--bleed) * -1);
  }
}

/* Height is set, not derived from the width, so the portrait stays a band of a
   known size at every viewport instead of growing with its container. Cropped
   from just above centre to keep the face and the bay behind it. */
.founder-photo img {
  width: 100%;
  height: clamp(420px, 40vw, 620px);
  object-fit: cover;
  object-position: 50% 25%;
  border-radius: var(--r-sm);
}

/* Beside a portrait the headline has roughly half the measure, so the full
   display size runs to five lines and swallows the screen. */
.founder-copy h1 {
  font-size: clamp(30px, 3.2vw, 44px);
  max-width: 16ch;
}

/* Four proof cells in a half-width column wrap every label to four lines.
   Two by two, with the divider only between columns. */
.founder-copy .proof {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.founder-copy .proof > * {
  padding: var(--s-3) var(--s-4) var(--s-3) 0;
}

.founder-copy .proof > *:nth-child(2n) {
  border-right: 0;
  padding-left: var(--s-4);
}

.founder-copy .proof > *:nth-child(2n + 1) {
  border-right: 1px solid var(--hair);
  padding-left: 0;
}

/* On a phone the portrait breaks the container and bleeds edge to edge. */
@media (max-width: 899px) {
  .founder-photo {
    width: 100vw;
    margin-inline: calc(50% - 50vw);
  }

  .founder-photo img {
    border-radius: 0;
  }
}

/* ----------------------------------------------------------- lead magnet -- */

/* On the full-bleed canary band the hairlines and the tight padding made this
   read as a strip wedged between two sections rather than its own offer. The
   band supplies the separation, so the rules come off and it gets real height. */
.lead-magnet {
  display: grid;
  gap: clamp(28px, 4vw, 56px);
  align-items: center;
  padding: 0;
}

@media (min-width: 880px) {
  .lead-magnet {
    grid-template-columns: 1.05fr 0.95fr;
  }
}

.lead-magnet h2 {
  margin-bottom: var(--s-3);
  font-size: var(--t-h3);
}

.lead-magnet p {
  margin: 0;
  color: var(--text-muted);
}

@media (min-width: 860px) {
  .lead-magnet {
    grid-template-columns: minmax(0, 1fr) minmax(0, 26rem);
    gap: var(--s-6);
  }
}

.lead-magnet h2 {
  font-size: clamp(21px, 1.9vw, 26px);
  margin-bottom: 6px;
}

.lead-magnet p {
  margin: 0;
  max-width: 54ch;
}

/* Joined inline: the field fills the row and the button sits at the right, the
   way the reference builds its email capture. */
.lead-form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}

.lead-form input[type="email"] {
  flex: 1 1 15rem;
  min-height: 52px;
  padding: 0 16px;
  border: 1px solid rgba(17, 17, 17, 0.14);
  border-radius: var(--r-input);
  background: var(--p-canvas);
  color: var(--text);
  font: inherit;
  font-size: var(--t-body);
}

.lead-form input[type="email"]::placeholder {
  color: var(--text-muted);
}

.lead-form .btn {
  min-height: 52px;
  flex: 0 0 auto;
}

.lead-form input {
  flex: 1 1 14rem;
  min-height: 48px;
  padding: 12px 14px;
  border: 1px solid var(--hair-strong);
  border-radius: var(--r-md);
  background: #fff;
  color: var(--text);
  font: inherit;
  font-size: 16px;
}

.lead-form input::placeholder {
  color: var(--text-muted);
}

.lead-form input:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 1px;
  border-color: var(--text);
}

.lead-success {
  margin-top: var(--s-3);
  padding: 12px 14px;
  border-left: 2px solid var(--brand);
  background: var(--ground-soft);
  font-size: var(--t-small);
  opacity: 0;
  transition: opacity var(--dur-hover) var(--ease-out);
}

.lead-success.visible {
  opacity: 1;
}

.lead-success.is-error {
  border-left-color: var(--alert);
}

/* --------------------------------------------------------------- the CTA -- */

.cta {
  text-align: center;
}

.cta h2 {
  max-width: 20ch;
  margin-inline: auto;
}

.cta .lede {
  margin-inline: auto;
  margin-bottom: var(--s-5);
}

/* Was a 30rem left-aligned list sitting under centred copy, which read as two
   layouts fighting. Now a three-column row that shares the section's centre. */
.cta-signal {
  max-width: none;
  margin: clamp(48px, 6vw, 72px) 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: var(--s-4);
  text-align: left;
}

@media (min-width: 820px) {
  .cta-signal {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--s-5);
  }
}

/* The vertical seam and node the .signal device draws do not belong in a
   horizontal row: a rule per column, above each beat, reads as a sequence. */
.cta-signal > li {
  position: relative;
  padding: var(--s-4) 0 0;
  border-top: 1px solid var(--hair-dark-strong);
}

.cta-signal > li::before,
.cta-signal > li::after {
  display: none;
}

.cta-signal .signal-title {
  display: block;
  margin-bottom: 6px;
}

.cta-signal p {
  margin: 0;
  max-width: none;
}

.cta-note {
  margin: var(--s-4) auto 0;
  font-size: var(--t-small);
  color: var(--text-muted-dark);
  max-width: 46ch;
}

/* --------------------------------------------- problem, terms and thanks -- */

/* The problem beat's three recognitions use the same horizontal-signal
   pattern as .cta-signal, on the light ground: a rule per column, the
   vertical spine and node switched off. */
.problem-signal {
  max-width: none;
  margin: clamp(48px, 6vw, 72px) 0 0;
}

@media (min-width: 900px) {
  .problem-signal {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--s-5);
  }
}

.problem-signal > li {
  position: relative;
  padding: var(--s-4) 0 0;
  border-top: 1px solid var(--hair-strong);
}

.problem-signal > li::before,
.problem-signal > li::after {
  display: none;
}

.problem-signal .signal-title {
  margin-bottom: 6px;
}

.problem-signal p {
  margin: 0;
  max-width: none;
}

.problem-close {
  margin: clamp(48px, 6vw, 72px) 0 0;
}

/* flex-end, not center: when the slate wraps to two lines the arrow should
   sit beside the last line, not float between them. */
.problem-pivot {
  display: inline-flex;
  align-items: flex-end;
  gap: var(--s-2);
  margin: var(--s-5) 0 0;
}

/* The terms ledger: the tier-detail row idiom carrying five policies, with
   the fit rail beside it. Verified terms only; nothing joins these rows
   without an approved offer change. */
.terms-grid {
  display: grid;
  gap: var(--s-6);
  margin-top: var(--s-5);
}

@media (min-width: 900px) {
  .terms-grid {
    grid-template-columns: repeat(12, minmax(0, 1fr));
  }

  .terms-list {
    grid-column: 1 / span 7;
  }

  .fit-aside {
    grid-column: 9 / span 4;
  }
}

.term {
  padding-block: var(--s-4);
  border-bottom: 1px solid var(--hair-medium);
}

.terms-list .term:first-child {
  border-top: 1px solid var(--hair-medium);
}

.term h3 {
  margin: 0 0 var(--s-1);
  font-size: var(--t-lede);
  font-weight: var(--w-medium);
  letter-spacing: -0.02em;
  line-height: 1.3;
}

.term p {
  margin: 0;
  color: var(--text-muted);
  max-width: 52ch;
}

.fit-aside {
  align-content: start;
}

.fit-block {
  padding-block: var(--s-4);
  border-top: 1px solid var(--hair-medium);
}

.fit-block:last-child {
  border-bottom: 1px solid var(--hair-medium);
}

.fit-block .slate {
  margin: 0 0 var(--s-2);
}

.fit-block p {
  margin: 0;
  color: var(--text-muted);
}

.terms-close {
  margin-top: var(--s-6);
  display: grid;
  gap: var(--s-4);
  justify-items: start;
}

.terms-close .lede {
  margin: 0;
}

/* /thanks: the booking receipt. */
.thanks-signal {
  margin: clamp(48px, 6vw, 72px) 0 0;
}

.thanks-more {
  margin: var(--s-6) 0 0;
}

/* ---------------------------------------------------------------- footer -- */

/* Must be the same ground as the CTA above it, or the two dark blocks meet at
   a visible seam. This was inheriting Ink from v16, where the footer and the
   dark sections were different colours by design. */
.site-footer {
  background: var(--ground-dark);
  color: var(--text-muted-dark);
  padding-block: var(--s-7) var(--s-5);
}

/* The footer runs wider than the 1200px content column, the way the reference
   does: the columns spread rather than sitting in a narrow block with dead
   space either side. */
.site-footer > .container {
  max-width: 1440px;
}

/* strong inherits --text (#111111) from the base rule, which on the near-black
   footer is 1.03:1. The legal entity name was rendering invisible. */
.site-footer strong,
.site-footer b {
  color: var(--text-on-dark);
}

.footer-grid {
  display: grid;
  gap: var(--s-6) var(--s-5);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

@media (min-width: 860px) {
  .footer-grid {
    /* A wide brand column, a deliberate gap, then the link columns pushed out
       to the right edge, which is the reference's arrangement. */
    grid-template-columns: 1.9fr 0.4fr 1fr 1fr 1.2fr;
    gap: var(--s-7) var(--s-5);
  }

  /* The spacer column. */
  .footer-grid > .footer-about { grid-column: 1; }
  .footer-grid > div:nth-of-type(2) { grid-column: 3; }
  .footer-grid > div:nth-of-type(3) { grid-column: 4; }
  .footer-grid > div:nth-of-type(4) { grid-column: 5; }
}

@media (max-width: 520px) {
  .footer-grid {
    grid-template-columns: 1fr;
    gap: var(--s-5);
  }
}

/* These stay <h2> for the heading-order gate, but they read as eyebrows, not as
   headings: 10px, weight 500, positive tracking, stone on charcoal. The last
   three headings on the site still set at 600. */
.site-footer h2 {
  font-size: var(--t-eyebrow);
  font-weight: var(--w-medium);
  letter-spacing: 0.3px;
  text-transform: uppercase;
  color: var(--text-muted-dark);
  margin-bottom: var(--s-3);
}

.site-footer ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 2px;
}

/* Footer links are standalone targets, not links in a sentence, so they take
   the full 44px rather than the inline-link exemption. */
.site-footer a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-block: 7px;
  color: var(--text-on-dark);
  text-decoration: none;
  font-size: 15px;
}

@media (hover: hover) and (pointer: fine) {
  .site-footer a:hover {
    color: var(--text-on-dark);
    text-decoration: underline;
  }
}

.footer-about p {
  font-size: 15px;
  line-height: 1.55;
  max-width: 34ch;
  margin: var(--s-3) 0 0;
}

/* The legal relationship, stated plainly rather than buried in a copyright. */
.footer-legal {
  margin-top: var(--s-7);
  padding-top: var(--s-4);
  border-top: 1px solid var(--hair-dark-medium);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-5);
  justify-content: space-between;
  font-size: 13.5px;
  line-height: 1.6;
}

.footer-legal p {
  margin: 0;
  max-width: 62ch;
}

.footer-legal a {
  font-size: inherit;
  padding-block: 0;
  text-decoration: underline;
}

/* ------------------------------------------------------------- page hero -- */

/* Inner pages open on a compact masthead rather than a second full hero:
   the visitor already chose to be here, so the job is orientation, not
   persuasion. */
.page-hero {
  padding-block: clamp(32px, 4vw, 56px) var(--section-y-tight);
  background: var(--ground);
}

.page-hero h1 {
  max-width: 17ch;
  margin: 12px 0 16px;
}

.page-hero .lede {
  max-width: 56ch;
}

.page-hero-grid {
  display: grid;
  gap: var(--s-5);
}

@media (min-width: 960px) {
  .page-hero-grid {
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: var(--s-6);
    /* Top aligned. Bottom aligning the shorter column leaves a hole under it
       that reads as a mistake rather than as negative space. */
    align-items: start;
  }

  .page-hero-grid > *:first-child {
    grid-column: 1 / span 7;
  }

  .page-hero-grid > *:last-child {
    grid-column: 9 / span 4;
    /* Optically aligns the aside with the headline rather than the eyebrow. */
    padding-top: 10px;
  }
}

/* ------------------------------------------------------- package detail -- */

/* The services page shows the same price list as the homepage, opened up:
   name and price hold the left, the contents sit on the right. Still a list,
   still no cards. */
.tier-detail {
  display: grid;
  gap: var(--s-4);
  padding-block: var(--s-5);
  border-bottom: 1px solid var(--hair);
}

.tier-detail:first-of-type {
  border-top: 1px solid var(--hair);
}

@media (min-width: 900px) {
  .tier-detail {
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: var(--s-6);
  }

  .tier-detail > *:first-child {
    grid-column: 1 / span 4;
  }

  .tier-detail > *:last-child {
    grid-column: 6 / span 7;
  }
}

.tier-detail.is-pick {
  background: var(--ground-soft);
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
}

.tier-tagline {
  margin: 4px 0 12px;
  color: var(--text-muted);
  font-size: var(--t-small);
}

.check-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 9px;
}

@media (min-width: 620px) {
  .check-list.two-up {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 9px var(--s-5);
  }
}

.check-list li {
  position: relative;
  padding-left: 24px;
  font-size: 16px;
  line-height: 1.45;
}

.check-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 8px;
  width: 8px;
  height: 8px;
  border-radius: 1px;
  background: var(--p-ink);
}

.on-dark .check-list li::before {
  background: var(--text-on-dark);
}

/* --------------------------------------------------------- entry offer -- */

.entry-offer {
  display: grid;
  gap: var(--s-4) var(--s-6);
  align-items: center;
  padding: var(--s-5) 0;
  border-top: 1px solid var(--hair);
  border-bottom: 1px solid var(--hair);
}

@media (min-width: 820px) {
  .entry-offer {
    grid-template-columns: minmax(0, 1fr) auto;
  }
}

/* ------------------------------------------------------ comparison table -- */

.compare-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.compare {
  width: 100%;
  min-width: 620px;
  border-collapse: collapse;
  font-size: 15px;
  text-align: left;
}

.compare caption {
  caption-side: bottom;
  padding-top: 12px;
  font-size: 13px;
  color: var(--text-muted);
  text-align: left;
}

.compare th,
.compare td {
  padding: 13px 14px 13px 0;
  border-bottom: 1px solid var(--hair);
  vertical-align: top;
}

.compare thead th {
  font-size: var(--t-slate);
  font-weight: var(--w-medium);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
  border-bottom-color: var(--hair-strong);
}

.compare th[scope="row"] {
  font-weight: 500;
  color: var(--text);
  padding-right: var(--s-4);
}

.compare td {
  color: var(--text);
}

/* Our column is tinted rather than boxed, and carries the only accent. */
.compare .col-us {
  background: var(--ground-soft);
  padding-inline: 14px;
  color: var(--text);
  font-weight: 500;
}

.compare thead .col-us {
  color: var(--text);
}

.compare .yes {
  color: var(--text);
  font-weight: var(--w-medium);
}

.compare .no {
  color: var(--text-muted);
}

/* ------------------------------------------------------------------ FAQ -- */

.faq-list {
  border-top: 1px solid var(--hair);
  max-width: 62rem;
}

.faq-item {
  border-bottom: 1px solid var(--hair);
}

.faq-question {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  width: 100%;
  min-height: 60px;
  padding: 16px 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 17px;
  font-weight: var(--w-medium);
  color: var(--text);
  text-align: left;
  cursor: pointer;
}

.faq-chevron {
  flex: none;
  width: 18px;
  height: 18px;
  color: var(--text-muted);
  transition: transform var(--dur-hover) var(--ease-out);
}

.faq-item.open .faq-chevron {
  transform: rotate(180deg);
}

.faq-answer {
  display: grid;
  grid-template-rows: 0fr;
}

.faq-item.open .faq-answer {
  grid-template-rows: 1fr;
}

/* overflow:hidden clips visually but leaves the text in the accessibility
   tree, which is what lets a screen reader read a closed answer on request;
   opacity keeps it there too, unlike visibility. The row itself snaps: height
   is a layout property, so the motion lives on the content instead. */
.faq-answer-inner {
  overflow: hidden;
  opacity: 0;
  transform: translateY(-6px);
  transition:
    opacity var(--dur-reveal) var(--ease-out),
    transform var(--dur-reveal) var(--ease-out);
}

.faq-item.open .faq-answer-inner {
  opacity: 1;
  transform: translateY(0);
}

.faq-answer-inner p {
  margin: 0 0 18px;
  max-width: 68ch;
  color: var(--text);
}

@media (hover: hover) and (pointer: fine) {
  .faq-question:hover .faq-chevron {
    color: var(--text);
  }
}

/* ----------------------------------------------------------------- forms -- */

.contact-layout {
  display: grid;
  gap: var(--s-6);
}

@media (min-width: 900px) {
  .contact-layout {
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: var(--s-6);
    align-items: start;
  }

  .contact-layout > *:first-child {
    grid-column: 1 / span 7;
  }

  .contact-layout > *:last-child {
    grid-column: 9 / span 4;
  }
}

.form-row {
  display: grid;
  gap: var(--s-3);
}

@media (min-width: 560px) {
  .form-row {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.field {
  display: grid;
  gap: 6px;
  margin-bottom: var(--s-3);
}

.field label {
  font-size: 14px;
  font-weight: var(--w-medium);
  color: var(--text);
}

.field input,
.field select,
.field textarea {
  width: 100%;
  min-height: 48px;
  padding: 12px 14px;
  border: 1px solid var(--hair-strong);
  border-radius: var(--r-md);
  background: #fff;
  color: var(--text);
  font: inherit;
  font-size: 16px;
}

.field textarea {
  min-height: 120px;
  resize: vertical;
}

.field input:focus-visible,
.field select:focus-visible,
.field textarea:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 1px;
  border-color: var(--text);
}

.field-error {
  margin: 6px 0 0;
  font-size: 14px;
  color: var(--alert);
  font-weight: 500;
}

.field.has-error input,
.field.has-error select,
.field.has-error textarea,
.lead-form.has-error input[type="email"] {
  border-color: var(--alert);
}

/* Required consent stays fully visible and legible. It is set quietly, at a
   size that still passes AA, rather than hidden behind a disclosure. */
.consent-group {
  display: grid;
  gap: 12px;
  margin: var(--s-4) 0;
  padding-top: var(--s-4);
  border-top: 1px solid var(--hair);
}

.consent {
  display: grid;
  grid-template-columns: 20px 1fr;
  gap: 10px;
  align-items: start;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-muted);
  cursor: pointer;
}

.consent input {
  width: 20px;
  height: 20px;
  margin: 1px 0 0;
  accent-color: var(--text);
}

.form-success {
  margin-top: var(--s-3);
  padding: 14px 16px;
  border-left: 2px solid var(--brand);
  background: var(--ground-soft);
  font-size: var(--t-small);
  opacity: 0;
  transition: opacity var(--dur-hover) var(--ease-out);
}

.form-success.visible {
  opacity: 1;
}

.form-success.is-error {
  border-left-color: var(--alert);
}

/* --------------------------------------------------------- booking embed -- */

/* The GHL widget sizes itself by postMessage against its own id. Do not give
   the iframe a height or an aspect-ratio here: both fight the injected value.
   min-height reserves space so the page does not jump when it measures. */
.booking-embed {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--hair);
  border-radius: var(--r-md);
  background: #fff;
  min-height: 700px;
}

.booking-embed iframe {
  width: 100%;
  min-height: 700px;
  border: 0;
  display: block;
}

@media (max-width: 640px) {
  .booking-embed,
  .booking-embed iframe {
    min-height: 820px;
  }
}

/* ----------------------------------------------------------- legal pages -- */

.legal-banner {
  margin-bottom: var(--s-5);
  padding: 14px 16px;
  border-left: 2px solid var(--brand);
  background: var(--ground-soft);
  font-size: var(--t-small);
}

.legal-banner p {
  margin: 0;
  max-width: 68ch;
}

.legal-body {
  max-width: 68ch;
}

.legal-body h2 {
  font-size: clamp(20px, 1.8vw, 24px);
  margin-top: var(--s-6);
}

.legal-body ul {
  padding-left: 20px;
}

.legal-body li {
  margin-bottom: 8px;
}

/* ------------------------------------------------------------ sticky CTA -- */

.sticky-cta {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 45;
  padding: 10px var(--gutter) calc(10px + env(safe-area-inset-bottom));
  background: var(--ground);
  border-top: 1px solid var(--hair);
  transform: translateY(101%);
  transition: transform var(--dur-reveal) var(--ease-out);
}

.sticky-cta.visible {
  transform: translateY(0);
}

.sticky-cta .btn {
  width: 100%;
}

@media (min-width: 901px) {
  .sticky-cta {
    left: auto;
    right: var(--gutter);
    bottom: var(--gutter);
    width: auto;
    padding: 0;
    background: transparent;
    border: 0;
    border-radius: var(--r-md);
    box-shadow: var(--shadow-lift);
    /* Off the bottom of the viewport, not just off its own height: at
       `bottom: var(--gutter)` a 101% translate still leaves it showing. */
    transform: translateY(calc(100% + var(--gutter) + 8px));
  }

  .sticky-cta.visible {
    transform: translateY(0);
  }

  .sticky-cta .btn {
    width: auto;
  }
}

/* ------------------------------------------------------------------ misc -- */

.reveal {
  opacity: 1;
}

/* Only animate when the script is alive. If main.js never runs, the inline
   head guard drops the .js class and everything is visible and still. */
.js .reveal {
  opacity: 0;
  transform: translateY(10px);
}

.js .reveal.in-view {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-reveal) var(--ease-out) var(--reveal-delay),
    transform var(--dur-reveal) var(--ease-out) var(--reveal-delay);
}

/* The second tier. `.reveal-lg` is a modifier, never a replacement: an element
   carries both classes and inherits the whole `.reveal` mechanism (the .js
   gate, .in-view, --reveal-delay), then overrides the distance and the
   duration. One tier for the block, one for the items inside it, which is what
   stops a page of identical 250ms fades reading as a single wipe.

   Reserved for section heads and for full-width blocks that own their own row.
   Grid items, tiles, cards, proof figures and the trust marks stay on the small
   tier: 16px of travel on a five-across grid is a wave, not a reveal. */
.js .reveal-lg {
  transform: translateY(16px);
}

.js .reveal-lg.in-view {
  transform: none;
  transition: opacity var(--dur-reveal-lg) var(--ease-out) var(--reveal-delay),
    transform var(--dur-reveal-lg) var(--ease-out) var(--reveal-delay);
}

/* ------------------------------------------------------- split headline --
   The hero headline arrives a word at a time. motion.js does the splitting and
   removes `.reveal` from the h1 as it does, so the headline is only ever driven
   by one system: word spans OR the block reveal, never both.

   Word level, not character level. Fifty characters counting up one at a time
   is a slot machine, and a screen reader reading a string broken into character
   spans is a disaster. Words keep the sentence intact for assistive tech and
   keep the entrance under a second.

   With JS absent the split never happens and the h1 is plain static text, which
   is why this whole block is gated on `.js` and scoped to `.w`. */
.js .hero h1 .w {
  /* The word index, overwritten inline by motion.js. Declared here so the rule
     is complete on its own: a span that somehow missed its inline value gets a
     zero delay rather than an invalid calc() that drops the whole rule. */
  --wi: 0;
  /* Transform does not apply to a non-replaced inline box. */
  display: inline-block;
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--dur-reveal-lg) var(--ease-out),
    transform var(--dur-reveal-lg) var(--ease-out);
  /* --wi is the word index, written onto each span by motion.js. */
  transition-delay: calc(var(--wi) * var(--stagger-step));
}

.js .hero h1.split-in .w {
  opacity: 1;
  transform: none;
}

/* ------------------------------------------------------ page transitions --
   Cross-document view transitions. The whole feature is four declarations and
   no JavaScript: the browser snapshots the outgoing page, snapshots the
   incoming one, and crossfades between them. A browser that does not implement
   it does exactly what this site did yesterday, an instant swap, so there is
   nothing to feature-detect and nothing to shim.

   200ms and --ease-out, matching the mobile nav panel. Longer than that on a
   navigation and the site feels like it is loading rather than moving.

   `navigation: auto` covers same-origin navigations only, which on this site
   means the eight internal pages and nothing else. */
@view-transition {
  navigation: auto;
}

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: var(--dur-enter);
  animation-timing-function: var(--ease-out);
}

/* The header is identical on every page, so naming it hands the browser a
   matched pair and it stays put instead of crossfading with the body under it.
   That single stable element is the difference between a page transition and a
   page flash. */
.site-header {
  view-transition-name: site-header;
}

/* ---------------------------------------------------------------- motion -- */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  /* No page crossfade either. A navigation is the largest piece of motion on
     the site and it is not one the visitor asked for. */
  @view-transition {
    navigation: none;
  }

  /* Belt and braces: the universal rule below matches elements, not the view
     transition pseudo-elements, so an engine that honours `navigation: auto`
     from an earlier rule still gets a zero-length crossfade here. */
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 0.001ms !important;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  /* Reduce, do not delete. The rule above kills every duration on the page,
     which stops the movement this setting is actually about, but it also takes
     out the 120ms colour change under a cursor and the fade on a focus ring.
     Those are feedback, not decoration: without them a button gives no sign it
     was pressed, and a control that responds in zero frames reads as a
     mis-render rather than as a response.

     So the paint properties come back at press speed for the things a person
     interacts with. `transition-property` is restated rather than inherited so
     that transform stays dead no matter what the element declared: the movement
     is what was asked to stop. */
  .btn,
  a,
  button,
  input,
  select,
  textarea,
  summary {
    transition-property: color, background-color, border-color, opacity,
      box-shadow !important;
    transition-duration: var(--dur-press) !important;
    transition-timing-function: var(--ease-out) !important;
  }

  .js .reveal,
  .js .reveal.in-view,
  .js .reveal-lg,
  .js .reveal-lg.in-view {
    opacity: 1;
    transform: none;
  }

  /* motion.js never splits the headline under reduced motion, so these spans
     normally do not exist. They do if the setting is switched on while the page
     is open, and a headline stuck at opacity 0 is not an accommodation. */
  .js .hero h1 .w {
    opacity: 1;
    transform: none;
  }

  .btn:active {
    transform: none;
  }

  .sticky-cta {
    transition: none;
  }
}

/* ----------------------------------------------------------------- print -- */

@media print {
  /* Dark grounds print as either a black slab or white-on-white depending on
     the browser. Force everything back to paper. */
  .on-dark,
  .site-footer,
  .section.on-soft,
  .section.on-soft-x {
    background: #fff !important;
    color: #000 !important;
  }

  .on-dark h1,
  .on-dark h2,
  .on-dark h3,
  .on-dark p,
  .on-dark li,
  .site-footer a,
  .on-dark .figure,
  .on-dark .slate {
    color: #000 !important;
  }

  .site-header,
  .sticky-cta,
  .nav-toggle,
  .reel-play,
  .reel video,
  .skip-link {
    display: none !important;
  }

  body {
    background: #fff;
    color: #000;
    font-size: 11pt;
  }

  .section {
    padding-block: 12pt;
    background: #fff;
  }

  /* Pricing and services are the pages people actually print. */
  a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-size: 9pt;
    color: #444;
  }

  .tier,
  .stage,
  .track,
  .case-body {
    break-inside: avoid;
  }
}


/* ============================================================================
   Signature device 3c : the stage
   The homepage work section as a playlist. One reel plays large inside a
   phone frame; the other five wait as tracks in an Apple Music register:
   art, title, meta, result where a duration would sit. Hovering a track
   surfaces a play glyph on its art; the active track shows equalizer bars
   while the stage is genuinely playing (.stage-section.is-live).
   ========================================================================= */

.stage-head {
  text-align: center;
  max-width: 62ch;
  margin: 0 auto var(--s-6);
}

/* h1 as well as h2: /work leads with the stage, so the stage head carries the
   page's only h1 there. Same block, same rhythm, different level. */
.stage-head h1,
.stage-head h2 {
  margin-bottom: var(--s-3);
}

.stage-head .slate {
  margin-bottom: var(--s-2);
}

.stage-head p {
  font-size: 17px;
  line-height: 1.6;
  color: var(--n-04);
}

.stage-grid {
  display: grid;
  grid-template-columns: minmax(280px, 380px) minmax(0, 1fr);
  gap: clamp(40px, 6vw, 88px);
  align-items: center;
  max-width: 1100px;
  margin-inline: auto;
}

@media (max-width: 899px) {
  .stage-grid {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s-5);
  }

  /* Two classes on purpose: the base `.stage { margin: 0 }` sits later in
     the file at equal specificity, so a bare `.stage` here loses its
     margin-inline and the phone rides the left edge on phones. That
     shipped; Danny caught it on his own device. */
  .stage-grid .stage {
    max-width: 300px;
    margin-inline: auto;
  }
}

/* The phone. A slim consistent bezel is what lets six visually unrelated
   posters read as one system: everything becomes content on a device. */
.stage {
  margin: 0;
}

.stage-phone {
  border-radius: 40px;
  background: var(--p-ink);
  padding: 9px;
  box-shadow: 0 36px 64px -32px rgba(17, 17, 17, 0.4);
  position: relative;
}

.stage-phone::after {
  /* The speaker pill. Sits above the reel, purely cosmetic. */
  content: "";
  position: absolute;
  top: 19px;
  left: 50%;
  transform: translateX(-50%);
  width: 76px;
  height: 19px;
  border-radius: 11px;
  background: var(--p-ink);
  z-index: 3;
  pointer-events: none;
}

.stage-phone .reel {
  border-radius: 31px;
  box-shadow: none;
  aspect-ratio: 9 / 16.4;
}

.stage-cap {
  margin-top: var(--s-3);
  text-align: center;
}

.stage-cap-title {
  display: block;
  font-weight: 600;
  font-size: 16px;
}

.stage-cap-meta {
  display: block;
  margin-top: 3px;
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* n-04, not n-05: n-05 is 4.16:1 on cream, under AA for 12px text. */
  color: var(--n-04);
}

.stage-cap-meta em {
  font-style: normal;
  font-weight: 600;
  /* Ink, not orange: Phoenix is 3.7:1 on cream, a wash color, never text. */
  color: var(--p-ink);
}

/* The objective line. Optional, and only /work carries it: the homepage cut
   has no room for a third line under the phone and does not need one. It is
   the sentence the old scatter kept in its slate block, which is the one piece
   of information the tracklist has nowhere to put. n-04, not n-05: n-05 is
   4.16:1 on cream and fails AA at this size. */
.stage-cap-note {
  display: block;
  margin-top: 10px;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--n-04);
  max-width: 32ch;
  margin-inline: auto;
}

/* ---------------------------------------------------------- the tracklist -- */
.tracklist {
  display: grid;
  gap: 6px;
  align-content: center;
}

/* Grouped tracks. The homepage runs one flat list of six; /work splits the
   same six under two labels, the way an album splits into sides, because
   "we were paid to make this" and "this is our own account" are two different
   claims and a flat list states them as one.

   The group gap is larger than the 6px between rows, so the split reads
   without a rule across the column. */
.tracklist > .track-group + .track-group {
  margin-top: var(--s-4);
}

.track-group {
  display: grid;
  gap: 6px;
}

.tracklist-label {
  margin: 0 0 4px;
  padding-inline: 14px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  /* n-04, not n-05: n-05 is 4.16:1 on cream and fails AA at 11px. */
  color: var(--n-04);
}

.track {
  display: grid;
  grid-template-columns: 48px minmax(0, 1fr) auto;
  align-items: center;
  gap: 14px;
  min-height: 64px;
  padding: 8px 14px;
  border: 1px solid transparent;
  border-radius: 14px;
  background: transparent;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  transition: background-color var(--dur-hover) var(--ease-out), border-color var(--dur-hover) var(--ease-out),
    box-shadow var(--dur-hover) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .track:not(.is-active):hover {
    background: rgba(255, 255, 255, 0.65);
  }
}

.track.is-active {
  background: #ffffff;
  border-color: rgba(17, 17, 17, 0.09);
  box-shadow: 0 10px 28px -18px rgba(17, 17, 17, 0.28);
}

.track:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.track-art {
  position: relative;
  width: 48px;
  height: 64px;
  border-radius: 8px;
  overflow: hidden;
}

.track-art img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Scrim behind the glyph and the equalizer so they read on any poster. */
.track-art::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(17, 17, 17, 0.45);
  opacity: 0;
  transition: opacity var(--dur-hover) var(--ease-out);
}

.track-glyph,
.track-eq {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: grid;
  place-items: center;
  opacity: 0;
  transition: opacity var(--dur-hover) var(--ease-out);
}

.track-glyph svg {
  width: 16px;
  height: 16px;
  fill: #ffffff;
  margin-left: 2px;
}

@media (hover: hover) and (pointer: fine) {
  .track:not(.is-active):hover .track-art::after,
  .track:not(.is-active):focus-visible .track-art::after,
  .track:not(.is-active):hover .track-glyph,
  .track:not(.is-active):focus-visible .track-glyph {
    opacity: 1;
  }
}

/* The equalizer. Three bars, visible on the active track only while the stage
   is genuinely playing, exactly like a now-playing row. */
.track-eq {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: 3px;
  padding-bottom: 24px;
}

.track-eq i {
  width: 3px;
  height: 15px;
  border-radius: 2px;
  background: #ffffff;
  transform-origin: bottom;
  transform: scaleY(0.4);
}

.stage-section.is-live .track.is-active .track-art::after,
.stage-section.is-live .track.is-active .track-eq {
  opacity: 1;
}

.stage-section.is-live .track.is-active .track-eq i {
  animation: track-eq 1.05s ease-in-out infinite;
}

.stage-section.is-live .track.is-active .track-eq i:nth-child(2) {
  animation-delay: 0.22s;
}

.stage-section.is-live .track.is-active .track-eq i:nth-child(3) {
  animation-delay: 0.44s;
}

@keyframes track-eq {
  0%, 100% { transform: scaleY(0.35); }
  50% { transform: scaleY(1); }
}

.track-title {
  display: block;
  font-weight: 600;
  font-size: 15px;
  line-height: 1.3;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.track-meta {
  display: block;
  margin-top: 2px;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* n-04, not n-05: AA on the cream ground behind inactive rows. */
  color: var(--n-04);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.track-result {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--n-04);
  white-space: nowrap;
}

.track.is-active .track-result {
  /* Ink, not orange: 4.06:1 on the active row's white. The active state is
     already carried by the row fill, border and equalizer. */
  color: var(--p-ink);
}

@media (max-width: 479px) {
  .track {
    gap: 12px;
    padding: 8px 10px;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* Keep the now-playing state, drop the movement: bars hold three static
     heights instead of animating. */
  .stage-section.is-live .track.is-active .track-eq i {
    animation: none;
    transform: scaleY(0.85);
  }

  .stage-section.is-live .track.is-active .track-eq i:nth-child(2) {
    transform: scaleY(0.55);
  }

  .stage-section.is-live .track.is-active .track-eq i:nth-child(3) {
    transform: scaleY(0.7);
  }
}


/* ==========================================================================
   v19 "Frame". Neutral grounds, one accent, one display face.

   This block redefines the tokens declared at the top of the file and changes
   the handful of rules that could not be expressed as a token swap. It is
   appended rather than merged into the original :root so the change is
   reviewable as one unit and revertible by deleting one block. Fold it into
   the token block once it has been live long enough to trust.

   The ground flip now happens exactly twice per page: the homepage hero and
   every page's closing CTA. `.on-dark` itself is a LIGHT ground now, which is
   why the other eight pages needed no markup change at all.
   ========================================================================== */

:root {
  /* ---- light palette, neutral ---- */
  --p-ink: #111111;
  --p-canvas: #fafafa;
  --p-cream: #eeeeee;
  --p-cream-2: #fafafa;
  --p-pearl: #eeeeee;
  --p-stone: #5a5a5a;

  /* ---- the dark bands ---- */
  --d-ground: #101010;
  --d-ground-2: #1c1c1c;
  --d-text: #f2f2f2;
  --d-text-muted: #a6a6a6;

  /* One accent. Measured: 3.89:1 on --p-canvas and 3.50:1 on --p-cream, so it
     clears the 3:1 floor for the record light and large display type. It is
     4.06:1 on white, so it never carries body text or links. */
  --p-phoenix: #e8400d;
  --p-phoenix-wash: none;
  --brand: var(--p-phoenix);
  --brand-wash: none;

  /* The five pillar pastels collapse into the neutral ramp: one colour only. */
  --p-petal: #eeeeee;
  --p-mint: #eeeeee;
  --p-canary: #eeeeee;
  --p-violet: #eeeeee;
  --p-aqua: #eeeeee;

  --ground: var(--p-canvas);
  --ground-soft: var(--p-cream);
  /* No longer dark. Only .on-dark.cta and the hero opt back in. */
  --ground-dark: var(--p-cream);
  --ground-dark-2: var(--p-canvas);

  --text: var(--p-ink);
  --text-muted: #5a5a5a;
  --text-on-dark: var(--p-ink);
  --text-muted-dark: #5a5a5a;
  --text-on-wash: var(--p-ink);
  --text-wash-body: #5a5a5a;

  --hair: rgba(17, 17, 17, 0.10);
  --hair-medium: rgba(17, 17, 17, 0.10);
  --hair-strong: rgba(17, 17, 17, 0.10);
  --hair-dark: rgba(17, 17, 17, 0.10);
  --hair-dark-medium: rgba(17, 17, 17, 0.10);
  --hair-dark-strong: rgba(17, 17, 17, 0.10);
  --focus-ring: var(--p-ink);

  --font: "Manrope", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  --serif: "Clash Display", "Manrope", system-ui, sans-serif;
  --display-font: "Clash Display", "Manrope", system-ui, sans-serif;

  --t-display: clamp(2.75rem, 5.4vw, 4.9rem);
  --t-h2: clamp(2rem, 3.6vw, 3.25rem);
  --t-h3: clamp(1.5rem, 2.2vw, 2rem);
  --t-h4: clamp(1.3rem, 1.7vw, 1.6rem);
  --t-sub: clamp(1.3rem, 1.7vw, 1.6rem);
  --t-figure: clamp(2rem, 3.6vw, 3.25rem);
  --t-lede: clamp(1.0625rem, 1.2vw, 1.1875rem);
  --t-body: 1.0625rem;
  --t-small: 0.9375rem;
  --t-slate: 0.8125rem;
  --t-eyebrow: 0.8125rem;
  --w-black: 700;

  /* Hover lightens rather than darkens. Darkening the orange drops the ink
     label under 4.5 (a 6% darker fill measures 4.17); lightening 6% toward
     white takes it to 4.93 and keeps the fill at 3.67 against the light
     ground. */
  --brand-hover: #e94b1c;
  /* The button's highlight. A short ramp: it resolves to the base by 58%, so
     the light sits at the top like a light source rather than washing the
     whole button into a different colour from the record dot. */
  --brand-lift: #f18969;
  --brand-lift-hover: #f3987c;
  --btn-fill: linear-gradient(180deg, var(--brand-lift) 0%, var(--brand) 58%);
  --btn-fill-hover: linear-gradient(180deg, var(--brand-lift-hover) 0%, var(--brand-hover) 58%);

  --peek: 115px;
  --vsl-gap: 20px;
  --hero-h: calc(100svh - var(--header-h) - var(--peek) - var(--vsl-gap));
}

body { font-weight: 400; line-height: 1.6; letter-spacing: -0.005em; }

h1, h2, h3, h4, .display, .figure, .pull {
  font-family: var(--display-font);
  font-weight: 700;
}
h2 { font-size: var(--t-h2); line-height: 1; letter-spacing: -0.03em; }
h3 { font-size: var(--t-h4); line-height: 1.1; letter-spacing: -0.02em; }
h4 { font-size: var(--t-sub); line-height: 1.15; letter-spacing: -0.02em; }
.figure { font-size: var(--t-figure); letter-spacing: -0.035em; line-height: 0.98; }
.pull { font-size: var(--t-h3); line-height: 1.2; letter-spacing: -0.02em; }
.lede { font-size: var(--t-lede); line-height: 1.5; }
.eyebrow, .slate { font-size: var(--t-slate); letter-spacing: 0.1em; font-weight: 600; }
/* The display face is set in sentence case. At this scale uppercase was the
   single thing making the headline read as a slab. */
.display { font-size: var(--t-display); text-transform: none; line-height: 1; letter-spacing: -0.035em; }

.btn { border-radius: var(--r-button); font-weight: 650; letter-spacing: -0.005em; min-height: 50px; padding: 13px 24px; }
.btn-lg { min-height: 54px; padding: 15px 30px; font-size: 16px; }
/* The primary button carries the accent, the same colour as the record light.
   The label is ink, not white: ink on the orange measures 4.65:1 and clears
   the 4.5 floor, where white measures 4.06 and does not. The fill itself is a
   component fill, judged at 3:1, and measures 3.89 on canvas, 3.50 on the
   secondary light ground and 4.68 on the dark bands. One rule, every ground,
   so there is no ground-specific inversion left to get wrong. */
/* The 1px border is load-bearing, not decoration. It is what separates the
   button from the page, measuring 3.89 on canvas, 3.50 on cream and 4.68 on
   the dark band. With the boundary carried by the border, the fill only has to
   keep the label readable, and a lighter fill makes the label easier to read,
   not harder: ink measures 4.65 against the base and 7.66 against the top of
   the ramp. Without the border the light end was 2.68 against cream and the
   top edge dissolved into it. */
.btn-primary { background: var(--btn-fill); border-color: var(--brand); color: var(--p-ink); }
@media (hover: hover) and (pointer: fine) {
  .btn-primary:hover { background: var(--btn-fill-hover); border-color: var(--brand-hover); }
}

/* The phoenix wash is retired. */
.hero::before { display: none; }

/* ---- the record light -------------------------------------------------- */
/* Round, and it blinks the way a record light does. The only ambient loop on
   the site, and semantic rather than decorative: an indicator that never
   blinks is not an indicator. */
.logo .mark {
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: var(--brand);
  animation: rec-blink 2.6s steps(1, end) infinite;
}
@keyframes rec-blink { 0%, 64% { opacity: 1; } 76%, 100% { opacity: 0.25; } }

/* ---- the two dark bands ------------------------------------------------ */
/* .hero-frame belongs in this group. Left out of it, the hero's CTA fell back
   to the light-ground button and rendered an ink fill on the ink ground at
   1.01:1: a button with no visible shape. */
.hero-frame,
.on-ink,
.on-dark.cta {
  background: var(--d-ground);
  color: var(--d-text-muted);
  --ground: var(--d-ground);
  --ground-soft: var(--d-ground);
  --ground-dark: var(--d-ground);
  --ground-dark-2: var(--d-ground-2);
  --text: var(--d-text);
  --text-muted: var(--d-text-muted);
  --text-on-dark: var(--d-text);
  --text-muted-dark: var(--d-text-muted);
  --hair: rgba(242, 242, 242, 0.14);
  --hair-medium: rgba(242, 242, 242, 0.14);
  --hair-strong: rgba(242, 242, 242, 0.14);
  --hair-dark: rgba(242, 242, 242, 0.14);
  --hair-dark-medium: rgba(242, 242, 242, 0.14);
  --hair-dark-strong: rgba(242, 242, 242, 0.14);
  --focus-ring: var(--d-text);
}
.hero-frame h1, .hero-frame h2, .hero-frame h3, .hero-frame h4, .hero-frame strong,
.on-ink h1, .on-ink h2, .on-ink h3, .on-ink h4, .on-ink strong,
.on-dark.cta h1, .on-dark.cta h2, .on-dark.cta h3, .on-dark.cta h4, .on-dark.cta strong { color: var(--d-text); }
.hero-frame p, .hero-frame li, .hero-frame .lede, .hero-frame .slate,
.on-ink p, .on-ink li, .on-ink .lede, .on-ink .slate,
.on-dark.cta p, .on-dark.cta li, .on-dark.cta .lede, .on-dark.cta .slate { color: var(--d-text-muted); }
.hero-frame a:not(.btn), .on-ink a:not(.btn), .on-dark.cta a:not(.btn) { color: var(--d-text); }
/* No inversion on the dark bands any more: the accent reads on both grounds,
   so the button is one colour everywhere. */
.hero-frame .btn-primary, .on-ink .btn-primary, .on-dark.cta .btn-primary,
/* .on-dark is a LIGHT ground now, but v17's inversion rule for it is (0,2,0)
   and still outranked the flat .btn-primary above, painting a near-white fill
   on a light ground at 1.11:1. Same invisible-button failure, third variation. */
.on-dark .btn-primary { background: var(--btn-fill); border-color: var(--brand); color: var(--p-ink); }
@media (hover: hover) and (pointer: fine) {
  .hero-frame .btn-primary:hover, .on-ink .btn-primary:hover,
  .on-dark.cta .btn-primary:hover, .on-dark .btn-primary:hover { background: var(--btn-fill-hover); border-color: var(--brand-hover); }
}

/* ---- homepage hero: the viewfinder ------------------------------------- */
/* Geometry measured from the clip by sampling pixels, not estimated:
   1272 x 662, inner brackets x 172 to 1092, y 219 to 512. Those brackets
   animate: over 61 frames they sweep 59px inward on the left and 57px on the
   right, which is why the copy is not pinned to their coordinates and why the
   clip is zoomed past the edges of the hero. */
.hero-frame {
  position: relative;
  overflow: hidden;
  min-height: var(--hero-h);
  container-type: inline-size;
  background: var(--d-ground);
}
.hero-frame-media {
  --zoom: 140%;
  position: absolute;
  left: calc((100% - var(--zoom)) / 2);
  top: 50%;
  /* Not -50%: the clip's bracket box centre sits at 55.2% of the clip, not
     50%, so centring the clip put the box low and the copy read high in it. */
  transform: translateY(-55.2%);
  width: var(--zoom);
  aspect-ratio: 1272 / 662;
}
.hero-frame-media img,
.hero-frame-media video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: fill;
  display: block;
  pointer-events: none;
  /* screen drops the clip's black out entirely, so the marks sit on the ground
     rather than on a black rectangle. The encode clamps the blacks to true
     zero, so no edge row or column paints a line onto the ground. */
  mix-blend-mode: screen;
  opacity: 0.9;
}
.hero-frame-media video { z-index: 1; }

.hero-frame .hero-copy {
  position: absolute;
  left: 8.5%;
  top: 50%;
  transform: translateY(-50%);
  width: 80%;
  z-index: 2;
}
.hero-frame h1 {
  margin: 0;
  font-size: 5.4cqw;
  line-height: 1;
  letter-spacing: -0.035em;
  color: var(--d-text);
  text-transform: none;
}
.hero-frame .lede { margin: 1.5cqw 0 0; max-width: 46ch; color: var(--d-text-muted); }
.hero-frame .hero-actions { margin-top: 1.5cqw; display: flex; gap: var(--s-2); justify-content: flex-start; }
.hero-frame .hero-note { margin: 1.1cqw 0 0; font-size: var(--t-small); color: var(--d-text-muted); max-width: 46ch; }

/* The film sits immediately below and crests the fold. */
.section.tight.vsl-section { padding-top: var(--vsl-gap); }

@media (max-width: 900px) {
  :root { --peek: 40px; --vsl-gap: 16px; }
  /* The clip is far too small here for its brackets to hold anything, so the
     frame becomes a header and the copy runs underneath it. */
  .hero-frame { display: flex; flex-direction: column; padding-block: var(--s-3) var(--s-5); }
  .hero-frame-media { position: relative; top: auto; left: 0; transform: none; width: 100%; flex: none; }
  .hero-frame .hero-copy {
    position: static; transform: none; width: auto;
    margin-top: var(--s-4); padding-inline: var(--gutter);
  }
  .hero-frame h1 { font-size: clamp(2.25rem, 8.5vw, 3rem); }
  .hero-frame .lede, .hero-frame .hero-note { margin-top: var(--s-3); }
  .hero-frame .hero-actions { margin-top: var(--s-4); }
}

@media (prefers-reduced-motion: reduce) {
  .logo .mark { animation: none; opacity: 1; }
  /* The frame stops moving; its poster still shows. No script involved. */
  .hero-frame-media video { display: none; }
}

/* The rail's active-step contrast fix lived here. The rail went with the stage
   on 2026-08-31, so the rule has nothing left to select. */

/* ============================================================ mobile density
   Added 2026-09-01. The homepage runs 12 sections on a phone and measured 17.3
   viewports, which the v20 audit named as the largest structural problem left
   on the page. This block is the layout half of the answer. No copy was cut and
   nothing is hidden: every element a phone rendered before, it still renders.

   Only spacing changes here, and only below 600px. Desktop is untouched, which
   is deliberate: desktop already came down 12% when the pinned stage went, and
   the v19 composition was approved at those rhythms.

   Where the height actually was, measured at 375x812 before this block:

     section padding   1,536px   12 sections x 64px top and bottom
     terms list          725px   five terms at 145px each
     case rail           833px   a 516px testimonial card plus three stats

   The section padding was the single biggest line and the least interesting:
   `--section-y` floors at 64px, and at 375px wide the clamp's 8vw term resolves
   to 30px, so every phone was getting the floor twice per section. 48px is the
   house `--s-6`, not a new value.

   The testimonial card is deliberately NOT touched. It is capped at 260px
   already and it is the best proof asset on the page; 516px of a phone spent on
   Zane talking is 516px well spent. */
@media (max-width: 599px) {
  :root {
    --section-y: clamp(48px, 8vw, 64px);
  }

  /* Five short terms do not need 24px of air above and below each one when the
     rule between them is already doing the separating. */
  .term {
    padding-block: var(--s-3);
  }

  /* Same reasoning, three figures instead of five terms. */
  .case-stat {
    padding-block: var(--s-3);
  }

  /* The signal device stacks on a phone, so its 32px gap and the 24px top
     padding on each item compound into 56px between two short paragraphs. */
  .signal {
    gap: var(--s-4);
  }
}

/* ==========================================================================
   v20 "the Halcyon shape". Homepage only, every selector is `hs-` prefixed.
   Plan and reasoning: docs/redesign-plan.md.

   No new tokens. Every duration, curve, radius, colour and type size below is
   one the file already defines. Appended as one block, like v19, so it reads
   as one change and reverts as one change.
   ========================================================================== */

/* ---- centred section heads ---------------------------------------------- */
/* The house rule keeps heads on a left axis. The reference centres them, and
   this page takes the reference's structure, so its new sections centre. */
.hs-head {
  max-width: 46rem;
  margin: 0 auto var(--s-6);
  text-align: center;
}
.hs-head .slate { margin: 0 0 10px; max-width: none; }
.hs-head h2 { margin: 0 auto; max-width: 20ch; }
.hs-head p:not(.slate) { margin: var(--s-3) auto 0; max-width: 52ch; color: var(--text-muted); }

/* ---- hero ---------------------------------------------------------------- */
.hs-hero { padding-bottom: var(--s-6); }
.hs-hero .hero-note { color: var(--text-muted); }

/* The reel row bleeds to both viewport edges. Items are spaced with a right
   margin, not a flex gap, so the track's half-width is exactly one set: with
   a gap the twin sits half a gap off and the loop jumps. */
.hs-reels {
  margin-top: clamp(40px, 5vw, 64px);
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 6%, #000 94%, transparent);
  mask-image: linear-gradient(to right, transparent, #000 6%, #000 94%, transparent);
}
.hs-reels-track {
  display: flex;
  width: max-content;
  margin: 0;
  padding: 0;
  list-style: none;
  animation: marquee 60s linear infinite;
}
.hs-reels-track > li {
  flex: none;
  width: clamp(150px, 14vw, 208px);
  margin-right: var(--s-3);
}
.hs-reels-track .reel { border-radius: var(--r-card); }
.hs-reels:hover .hs-reels-track,
.hs-reels:focus-within .hs-reels-track { animation-play-state: paused; }

/* Phones and reduced motion: the row becomes a native swipeable scroller, the
   same conversion the testimonial marquee made. A phone has no hover to pause
   on, and a still track would hide half the reels off the right edge. */
@media (max-width: 899px), (prefers-reduced-motion: reduce) {
  .hs-reels {
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    scroll-padding-inline: var(--gutter);
    scrollbar-width: none;
    -webkit-mask-image: none;
    mask-image: none;
  }
  .hs-reels::-webkit-scrollbar { display: none; }
  .hs-reels-track { animation: none; padding-inline: var(--gutter); }
  .hs-reels-track > li { scroll-snap-align: start; }
  .hs-reels-track > li[aria-hidden="true"] { display: none; }
}

.hs-stats {
  list-style: none;
  margin: clamp(32px, 4vw, 48px) auto 0;
  padding: var(--s-4) var(--s-2);
  max-width: 46rem;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  border: 1px solid var(--n-07);
  border-radius: var(--r-card);
}
.hs-stats > li { padding-inline: var(--s-2); text-align: center; }
.hs-stats > li + li { border-left: 1px solid var(--n-07); }
.hs-stat-figure {
  display: block;
  font-family: var(--display-font);
  font-weight: 700;
  font-size: var(--t-h4);
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--text);
}
.hs-stat-label {
  display: block;
  margin-top: var(--s-2);
  font-size: var(--t-small);
  line-height: 1.35;
  color: var(--text-muted);
}

/* ---- three photo cards ---------------------------------------------------- */
.hs-cards {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--s-3);
}
@media (min-width: 760px) {
  .hs-cards { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.hs-card {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  aspect-ratio: 4 / 5;
  border-radius: var(--r-card);
  background: var(--d-ground);
}
.hs-card img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* The scrim is what the text reads against, so it is solid by the time the
   copy starts: the body occupies at most the bottom 35% of the card, where the
   scrim is at least 78% of the dark ground. Over a pure white photo that
   composites to about #404040, which holds the near-white label at 8.8:1. */
.hs-card::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(180deg, rgba(16, 16, 16, 0) 30%, rgba(16, 16, 16, 0.78) 62%, rgba(16, 16, 16, 0.9) 100%);
}
.hs-card-body {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  padding: clamp(20px, 2.4vw, 28px);
}
.hs-card h3 { margin: 0; color: var(--d-text); }
.hs-card p { margin: var(--s-2) 0 0; max-width: 34ch; font-size: var(--t-small); line-height: 1.5; color: var(--d-text); }

/* Phones: square cards, and a scrim that goes solid higher up to match, since
   the copy now starts at about 58% of the card's height. */
@media (max-width: 599px) {
  .hs-card { aspect-ratio: 1 / 1; }
  .hs-card::after {
    background: linear-gradient(180deg, rgba(16, 16, 16, 0) 15%, rgba(16, 16, 16, 0.8) 50%, rgba(16, 16, 16, 0.9) 100%);
  }
  .hs-stats > li { padding-inline: var(--s-1); }
  .hs-stat-figure { font-size: var(--t-lede); white-space: nowrap; }
}

/* ---- how it works: the rail ------------------------------------------------ */
.hs-steps {
  list-style: none;
  margin: 0 auto;
  padding: 0;
  max-width: 56rem;
}
.hs-step {
  position: relative;
  display: grid;
  gap: var(--s-4);
  padding: 0 0 clamp(32px, 4vw, 48px) clamp(32px, 4vw, 48px);
}
.hs-step:last-child { padding-bottom: 0; }
/* The node, then the rail segment down to the next node. The orange is a
   component colour here, 3.50:1 on cream, never text. */
.hs-step::after {
  content: "";
  position: absolute;
  left: 0;
  top: 6px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--brand);
}
.hs-step::before {
  content: "";
  position: absolute;
  left: 5px;
  top: 18px;
  bottom: -6px;
  width: 2px;
  background: var(--brand);
}
.hs-step:last-child::before { display: none; }
.hs-step .slate { margin: 0 0 6px; }
.hs-step h3 { margin: 0 0 10px; }
.hs-step p:not(.slate) { margin: 0; max-width: 46ch; }
.hs-step-media {
  margin: 0;
  width: 100%;
  max-width: 200px;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  border-radius: var(--r-card);
  background: var(--ground);
  box-shadow: 0 0 0 1px var(--n-07);
}
.hs-step-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The views counter sits at the foot of that screenshot. */
.hs-step-media-base img { object-position: center bottom; }
/* Phones: the photo rides beside the copy as a thumbnail rather than below it
   at full width, which was 1,300px of a phone for five pictures. */
@media (max-width: 759px) {
  .hs-step { grid-template-columns: minmax(0, 1fr) 88px; gap: var(--s-3); }
  .hs-step-media { max-width: none; }
}
@media (min-width: 760px) {
  .hs-step {
    grid-template-columns: minmax(0, 1fr) 200px;
    gap: var(--s-6);
    align-items: start;
  }
  .hs-step-media { max-width: none; }
}

/* ---- where it goes: one shoot day, four destinations ------------------- */
/* Plan: docs/hero-and-distribution-plan.md, section D. The lines are one
   inline SVG in a fixed 1000x120 box that scales with the column, so the
   curves never distort. The four destination centres sit at 11.75%, 37.25%,
   62.75% and 88.25% of the row because the grid gap is 2%, a percentage, not
   a spacing token: that is what keeps each line landing on its card's centre
   at every width. */
.hs-dist { max-width: 64rem; margin: 0 auto; }
.hs-dist-source {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  max-width: 22rem;
  margin: 0 auto;
  padding: var(--s-3);
  border: 1px solid var(--brand);
  border-radius: var(--r-card);
  background: var(--ground);
}
.hs-dist-source img {
  flex: none;
  width: 56px;
  height: 56px;
  border-radius: var(--r-sm);
  object-fit: cover;
}
.hs-dist-source h3 { margin: 0; font-size: var(--t-sub); }
.hs-dist-source p { margin: var(--s-1) 0 0; font-size: var(--t-small); color: var(--text-muted); }

.hs-dist-lines { display: none; }
.hs-dist-targets {
  list-style: none;
  margin: var(--s-5) 0 0;
  padding: 0;
  display: grid;
  gap: var(--s-3);
}
.hs-dist-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: var(--s-4);
  border: 1px solid var(--n-07);
  border-radius: var(--r-card);
  background: var(--ground);
}
/* Instagram is the main line, so its card carries the accent edge, the
   same way the source does. 3.89:1 on canvas, a boundary, not text. */
.hs-dist-card.is-main { border-color: var(--brand); }
.hs-dist-icons { display: flex; gap: var(--s-2); margin-bottom: var(--s-3); }
.hs-picon { display: block; width: 28px; height: 28px; }
.hs-dist-mark { display: block; width: 28px; height: 28px; border-radius: var(--r-badge); background: var(--brand); }
.hs-dist-card h3 { margin: 0; font-size: var(--t-lede); font-weight: 700; }
.hs-dist-card p:not(.hs-tag) { margin: var(--s-2) 0 var(--s-4); font-size: var(--t-small); line-height: 1.5; color: var(--text-muted); }
.hs-dist-card .hs-tag { margin: auto 0 0; border-radius: var(--r-sm); }
/* Below 960 the curves are not drawn, so one short rule joins the shoot day
   to the cards under it. */
.hs-dist-source::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 100%;
  width: 1px;
  height: var(--s-5);
  background: var(--n-06);
}

@media (min-width: 600px) and (max-width: 959px) {
  .hs-dist-targets { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* 960 and up: four across, and the lines appear between source and cards. */
@media (min-width: 960px) {
  .hs-dist-lines {
    display: block;
    width: 100%;
    height: auto;
    overflow: visible;
    margin-top: -1px;
  }
  .hs-dist-targets {
    margin-top: 0;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    column-gap: 2%;
  }
  .hs-dist-source::after { display: none; }
  .hs-dist.is-live .hs-beam {
    animation: hs-beam 2.6s linear infinite;
    animation-delay: calc(var(--i) * var(--stagger-step) * 5);
  }
}
.hs-line { fill: none; stroke: var(--n-06); stroke-width: 1.5; }
.hs-line.is-main { stroke: var(--n-05); stroke-width: 2.5; }

/* The pulse: one short dash per line, travelling from the shoot day to its
   card. pathLength="1" makes the dash a fraction of the line, so every line
   runs at the same pace whatever its drawn length. It animates
   stroke-dashoffset, which repaints the stroke but never touches layout; the
   loop runs only while the section is on screen (.is-live, main.js) and
   never under reduced motion. The 2.6s cycle is the record light's, the
   site's only other ambient loop. */
.hs-beam {
  /* The line's index, set inline per path. Declared so a path without one
     starts at zero delay rather than dropping the whole rule. */
  --i: 0;
  fill: none;
  stroke: var(--brand);
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-dasharray: 0.14 2;
  stroke-dashoffset: 0.14;
  opacity: 0;
}
@keyframes hs-beam {
  0% { stroke-dashoffset: 0.14; opacity: 0; }
  10% { opacity: 1; }
  85% { opacity: 1; }
  100% { stroke-dashoffset: -1; opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .hs-dist.is-live .hs-beam { animation: none; }
}

.hs-tag {
  display: inline-block;
  margin: 0 0 var(--s-3);
  padding: 3px 10px;
  border: 1px solid var(--n-07);
  border-radius: var(--r-badge);
  font-size: var(--t-slate);
  font-weight: 600;
  line-height: 1.5;
  color: var(--text);
}

/* ---- packages ------------------------------------------------------------- */
.hs-plans {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--s-3);
}
@media (min-width: 900px) {
  .hs-plans { grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: stretch; }
}
.hs-plan {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: clamp(24px, 2.6vw, 32px);
  border: 1px solid var(--n-07);
  border-radius: var(--r-card);
  background: var(--ground);
}
.hs-plan.is-pick { border-color: var(--brand); }
.hs-plan h3 { margin: 0; }
.hs-plan-price { margin: var(--s-2) 0 0; font-weight: 650; color: var(--text); }
.hs-plan-lede { margin: var(--s-2) 0 0; color: var(--text-muted); }
.hs-plan .check-list { margin: var(--s-4) 0 var(--s-5); flex: 1; }
.hs-plan .btn { width: 100%; margin-top: auto; }

.hs-terms {
  list-style: none;
  margin: var(--s-5) 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s-2) var(--s-5);
  font-size: var(--t-small);
  color: var(--text-muted);
}

/* ---- case study on canvas -------------------------------------------------- */
.section.hs-canvas { background: var(--ground); }

/* ---- reviews ---------------------------------------------------------------- */
/* Two-column masonry from 760px. Below that the wall becomes a swipeable row,
   the way the old review strip behaved on phones: five long quotes stacked
   were 2,600px of a phone, and the peek of the next card is the affordance. */
.hs-reviews {
  display: flex;
  gap: var(--s-3);
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scroll-padding-inline: var(--gutter);
  scrollbar-width: none;
}
.hs-reviews::-webkit-scrollbar { display: none; }
.hs-reviews > * { flex: none; width: min(84vw, 26rem); scroll-snap-align: start; }
@media (min-width: 760px) {
  .hs-reviews {
    display: block;
    columns: 2;
    column-gap: var(--s-3);
    margin-inline: 0;
    padding-inline: 0;
    overflow: visible;
  }
  .hs-reviews > * { width: auto; }
}
.hs-reviews .quote-card {
  break-inside: avoid;
  margin-bottom: 0;
  gap: var(--s-3);
  border-color: var(--n-07);
  background: var(--ground);
}
/* Body face at body size. The display face at 26px suits one pull quote; a
   wall of them reads as shouting. */
.hs-reviews .quote-card blockquote {
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: 1.6;
  letter-spacing: 0;
  color: var(--text);
}
/* v19 made --text-on-dark ink, so the base monogram rule paints an ink letter
   on an ink tile outside an .on-dark section (1:1). Cream tile, ink letter:
   16:1 on the card. */
.hs-reviews .byline .monogram { background: var(--p-cream); color: var(--text); }
.hs-source { margin: 0; font-size: var(--t-slate); font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); }
.hs-review-more p { margin: 0; }
@media (min-width: 760px) {
  .hs-reviews .quote-card { margin-bottom: var(--s-3); }
}

/* ---- FAQ head ---------------------------------------------------------------- */

/* ---- book the call ------------------------------------------------------------ */
.hs-book { text-align: left; }
.hs-book-grid { display: grid; gap: var(--s-6); }
@media (min-width: 960px) {
  .hs-book-grid { grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); align-items: start; }
}
.hs-book h2 { margin: 0; max-width: 14ch; }
.hs-book .slate { margin: 0 0 10px; }
.hs-book .lede { margin: var(--s-3) 0 var(--s-5); max-width: 44ch; }
.hs-book-alt { margin: var(--s-5) 0 0; font-size: var(--t-small); }

/* The form card is a light island on the dark band. It resets the ground
   tokens, and then overrides the band's hard-coded colour rules, which are
   (0,2,1) for body text and (0,3,1) for links. Without the second half every
   paragraph and link in the card paints near-white on white. */
.hs-form-card {
  position: relative;
  padding: clamp(24px, 3vw, 40px);
  border-radius: var(--r-card);
  background: var(--p-canvas);
  color: var(--p-ink);
  --ground: var(--p-canvas);
  --text: var(--p-ink);
  --text-muted: var(--p-stone);
  --ground-soft: var(--p-cream);
  --hair: rgba(17, 17, 17, 0.10);
  --hair-strong: rgba(17, 17, 17, 0.10);
  --focus-ring: var(--p-ink);
}
.on-dark.cta .hs-form-card p,
.on-dark.cta .hs-form-card li,
.on-dark.cta .hs-form-card span { color: var(--p-stone); }
.on-dark.cta .hs-form-card label { color: var(--p-ink); }
.on-dark.cta .hs-form-card a:not(.btn) { color: var(--p-ink); }
/* Field errors are paragraphs too, so they need their colour back from the
   rule above: (0,4,0) against its (0,3,1). */
.on-dark.cta .hs-form-card .field-error { color: var(--alert); }
.hs-form-card .btn { width: 100%; }
/* The floating call to action steps aside while the form it points at is on
   screen: it sat over the submit button. main.js adds the class. */
.sticky-cta.visible.is-at-form { transform: translateY(101%); }
@media (min-width: 901px) {
  .sticky-cta.visible.is-at-form { transform: translateY(calc(100% + var(--gutter) + 8px)); }
}
.hs-form-note { margin: var(--s-3) 0 0; text-align: center; font-size: var(--t-small); }

/* ==========================================================================
   v21 type: Halcyon's three faces, Tom's call on 2026-09-18.
   Plus Jakarta Sans for headings, Inter for everything else, Playfair Display
   italic as a single accent. All three are SIL Open Font License, self-hosted
   latin subsets from Google Fonts, same range as the Manrope file.

   Manrope and Clash Display stay declared above and stay on disk: nothing
   here downloads them any more, but untracked preview pages still preload
   them. Delete both together with those pages, not before.
   ========================================================================== */

@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/assets/fonts/inter.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Plus Jakarta Sans";
  font-style: normal;
  font-weight: 500 800;
  font-display: swap;
  src: url("/assets/fonts/plus-jakarta-sans.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Playfair Display";
  font-style: italic;
  font-weight: 400 700;
  font-display: swap;
  src: url("/assets/fonts/playfair-display-italic.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  --font: "Inter", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  --display-font: "Plus Jakarta Sans", "Inter", system-ui, sans-serif;
  /* The old serif slot has held the display face since v19, and the rules that
     use it (pull quotes, the case quote) were drawn for a sans. Keep it so. */
  --serif: var(--display-font);
  --accent-font: "Playfair Display", Georgia, "Times New Roman", serif;
}

/* Set the way the reference sets it: heavy, tight, but with room between
   lines. Clash was condensed and could sit at a line-height of 1; Jakarta is
   wider and rounder and needs the extra leading or lines touch. */
.display,
.hero .display { font-weight: 800; letter-spacing: -0.035em; line-height: 1.06; }
h2 { font-weight: 700; letter-spacing: -0.025em; line-height: 1.1; }
h3, h4 { font-weight: 700; letter-spacing: -0.02em; line-height: 1.2; }
.figure, .hs-stat-figure { font-weight: 800; letter-spacing: -0.03em; }

/* The one accent, as the reference uses its serif: once, on the line that
   carries the most weight. Here that is the case study's outcome. The
   `.on-dark` twin matches the base rule's own selector list, which is (0,2,0). */
.on-dark .case-outcome,
.case-outcome {
  font-family: var(--accent-font);
  font-style: italic;
  font-weight: 400;
  letter-spacing: -0.01em;
  line-height: 1.12;
}

/* ==========================================================================
   The primary button, flat and light. Tom, 2026-09-18: "hard to read, dark".
   The label sat on the gradient's dark end, #e8400d, where Ink only just
   passes at 4.65:1 and reads muddy. The fill is now the ramp's own light stop
   end to end: Ink measures 7.66 on --brand-lift and 8.6 on --brand-lift-hover,
   both already asserted by tools/contrast.mjs. No new colour.

   The border is untouched and is now the ONLY thing separating the button
   from the page: the light fill is 2.36 on canvas and 2.12 on cream, but the
   #e8400d border is 3.89, 3.50 and 4.68 on the three grounds. Do not remove
   it, and do not lighten it.
   ========================================================================== */
:root {
  --btn-fill: var(--brand-lift);
  --btn-fill-hover: var(--brand-lift-hover);
}

/* ==========================================================================
   Four polish items from the 2026-09-18 review of the v20 build.
   ========================================================================== */

/* 1. The floating call to action is a phone device. On desktop the header
   carries the same button the whole time, so the floating copy only ever sat
   over content. Hidden at the breakpoint where the header CTA is visible. */
@media (min-width: 901px) {
  .sticky-cta { display: none; }
}

/* 2. The FAQ head joins the centred axis the new sections use. */
.hs-head .btn { margin-top: var(--s-4); }

/* 3. A faint warm glow behind the hero copy, the reference's peach radial in
   our accent. Static: it is atmosphere, not motion. The brand at 10% over
   canvas composites to about #f8e6e1, where ink is still above 15:1 and the
   muted note above 5:1. Sits behind the copy only, not the reel row. */
.hs-hero::before {
  content: "";
  display: block;
  position: absolute;
  z-index: -1;
  left: 50%;
  top: -20%;
  width: min(1100px, 120vw);
  height: 70%;
  transform: translateX(-50%);
  background: radial-gradient(ellipse at 50% 30%, rgba(232, 64, 13, 0.10), rgba(232, 64, 13, 0) 65%);
  animation: none;
  pointer-events: none;
}

/* 4. "Priced on your free call" is a note, not a price. Bold in the price
   slot read as a number that was missing. */
.hs-plan-price {
  font-size: var(--t-small);
  font-weight: 500;
  color: var(--text-muted);
}

/* ==========================================================================
   Finish pass, 2026-09-18. Every value below comes from a measured delta in
   docs/finish-pass.md, Halcyon against this page at 1440 wide. Scoped to the
   homepage by `main.hs-page`. No colour, font, radius or duration token is
   changed: those deltas are listed in the doc for Danny and left alone.
   ========================================================================== */

/* Page-wide: the reference's 1,104px content column, its 100px section
   rhythm, and a section heading scale that tops out at 44px instead of 52. */
.hs-page {
  --container: 1104px;
  --section-y: clamp(64px, 7vw, 100px);
  --hs-h2: clamp(2rem, 3vw, 2.75rem);
}
@media (max-width: 599px) {
  .hs-page { --section-y: clamp(48px, 8vw, 64px); }
}
.hs-page h2 { font-size: var(--hs-h2); }
.hs-page .slate,
.hs-page .eyebrow { letter-spacing: 0.12em; }
.hs-head { margin-bottom: calc(var(--s-6) + var(--s-2)); }
.hs-head .slate { margin-bottom: var(--s-3); }
.hs-head h2 { max-width: 30ch; text-wrap: balance; }
.hs-head p:not(.slate) { font-size: var(--t-lede); line-height: 1.6; }

/* Hero */
.hs-hero { padding-top: clamp(56px, 8vw, 120px); padding-bottom: clamp(48px, 5.5vw, 80px); }
.hs-hero .display { font-size: clamp(2.75rem, 4.4vw, 3.875rem); margin-bottom: var(--s-5); }
.hs-hero .lede { max-width: 44rem; line-height: 1.6; }
.hs-reels-track > li { width: clamp(150px, 16vw, 232px); }
.hs-stats { padding: var(--s-5) var(--s-4); }
.hs-stat-figure { font-size: var(--t-h3); }
@media (max-width: 599px) {
  .hs-stats { padding: var(--s-4) var(--s-2); }
  .hs-stat-figure { font-size: var(--t-lede); }
}

/* Three cards: square, twice the gap, heavier titles. The scrim goes solid
   higher up to match, because on a square card the copy starts near the
   middle: at 45% it is already 80% of the dark ground. */
.hs-cards { gap: var(--s-5); }
@media (max-width: 759px) {
  .hs-cards { gap: var(--s-3); }
}
.hs-card { aspect-ratio: 1 / 1; }
.hs-card::after {
  background: linear-gradient(180deg, rgba(16, 16, 16, 0) 10%, rgba(16, 16, 16, 0.8) 45%, rgba(16, 16, 16, 0.9) 100%);
}
.hs-card h3 { font-weight: 800; }

/* How it works: bigger titles and photos, more air between steps, and a grey
   rail with orange nodes instead of an all-orange rail. */
.hs-step { padding-bottom: clamp(32px, 4.5vw, 64px); }
.hs-step h3 { font-size: var(--t-h3); font-weight: 800; }
.hs-step p:not(.slate) { line-height: 1.7; }
.hs-step::before { background: var(--n-06); }
.hs-step-media { aspect-ratio: 4 / 5; }
@media (min-width: 760px) {
  .hs-step { grid-template-columns: minmax(0, 1fr) 260px; }
}

/* Pricing: heavier titles, the tag lifted onto the top border so all three
   titles share a baseline, and light accent rings in place of ink squares. */
.hs-plans { gap: var(--s-4); }
.hs-plan { position: relative; }
.hs-plan h3 { font-weight: 800; }
.hs-plan > .hs-tag {
  position: absolute;
  top: 0;
  right: var(--s-4);
  margin: 0;
  transform: translateY(-50%);
  background: var(--p-canvas);
}
/* align-content: the list is a grid that flexes to fill the card, and a
   grid's rows stretch by default: a short list spread its items apart. */
.hs-plan .check-list { gap: calc(var(--s-1) * 3); align-content: start; }
.hs-plan .check-list li { padding-left: 26px; font-size: var(--t-small); line-height: 1.5; }
.hs-plan .check-list li::before {
  top: 5px;
  width: 12px;
  height: 12px;
  border: 1.5px solid var(--brand);
  border-radius: var(--r-badge);
  background: transparent;
}

/* Case study: the pull quote comes down from 30px to the h4 size. */
.hs-page .case-quote { font-size: var(--t-h4); }

/* Reviews: three narrow columns, round 40px portraits, smaller and softer
   quote type. Phones keep the swipe row. */
@media (min-width: 960px) {
  .hs-reviews { columns: 3; }
}
.hs-reviews .quote-card { padding: var(--s-4); }
.hs-reviews .byline img,
.hs-reviews .byline .monogram {
  width: 40px;
  height: 40px;
  border-radius: var(--r-badge);
}
.hs-reviews .byline .monogram { font-size: var(--t-small); }
.hs-reviews .byline-name { font-size: var(--t-small); font-weight: 700; }
.hs-reviews .quote-card blockquote { font-size: var(--t-small); line-height: 1.75; color: var(--n-02); }

/* FAQ: two columns from 960px, head left, questions right. */
.hs-faq-grid { display: grid; gap: var(--s-5); }
.hs-faq-grid .hs-head { margin-bottom: 0; }
@media (min-width: 960px) {
  .hs-faq-grid {
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    gap: var(--s-7);
    align-items: start;
  }
  .hs-faq-grid .hs-head { text-align: left; }
  .hs-faq-grid .hs-head h2,
  .hs-faq-grid .hs-head p:not(.slate) { margin-inline: 0; }
}
.hs-page .faq-question { min-height: 68px; font-weight: 600; }

/* Form: shorter inputs at the 8px radius, heavier labels. 44px is the touch
   target floor, which is why it stops short of the reference's 39. */
.hs-form-card .field input,
.hs-form-card .field select { min-height: 44px; padding-block: 10px; line-height: 1.25; }
.hs-form-card .field input,
.hs-form-card .field select,
.hs-form-card .field textarea { border-radius: var(--r-sm); }
.hs-form-card .field label { font-weight: 600; }

/* ==========================================================================
   Owner cards, 2026-09-26. Two faults Tom's screenshot led to:

   1. The first card's photo is a tall portrait, and a centred crop cut the
      owner's head off at the chin. It now anchors to the top edge, and the
      image is a 900px export of the original rather than the 608px copy.
   2. Measured at eleven widths: between 760 and 1099px the three cards were
      212 to 300px wide and their copy ran taller than the card, clipping the
      heading off the top (text started at -10% of the card at 760px). Those
      widths now get one full-width landscape card per row.

   The cards also grow from square to 4:5 at desktop, so the copy starts
   below 50% of the card, and the scrim starts lower to match, which keeps
   faces in the top third out of the dark. Text still sits on at least 80%
   of the dark ground wherever it starts (measured: 48 to 81% of the card
   height at every width from 360 to 1920).
   ========================================================================== */
.hs-card-img-top { object-position: 50% 0; }
.hs-card::after {
  background: linear-gradient(180deg, rgba(16, 16, 16, 0) 25%, rgba(16, 16, 16, 0.8) 50%, rgba(16, 16, 16, 0.9) 100%);
}
@media (min-width: 600px) {
  .hs-card::after {
    background: linear-gradient(180deg, rgba(16, 16, 16, 0) 35%, rgba(16, 16, 16, 0.8) 58%, rgba(16, 16, 16, 0.9) 100%);
  }
}
@media (min-width: 600px) and (max-width: 1099px) {
  .hs-cards { grid-template-columns: minmax(0, 1fr); }
  .hs-card { aspect-ratio: 4 / 3; }
}
@media (min-width: 1100px) {
  .hs-card { aspect-ratio: 4 / 5; }
}

