/* Self-hosted webfonts. Regenerate with tools/fetch_fonts.py. */
/* latin-ext */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400 700;
  font-stretch: 62% 125%;
  font-display: swap;
  src: url(/assets/fonts/Archivo-latin-ext-normal-k3kQo8UDI-1M0wlSfdfoLmvDIaK18A.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400 700;
  font-stretch: 62% 125%;
  font-display: swap;
  src: url(/assets/fonts/Archivo-latin-normal-k3kQo8UDI-1M0wlSfdnoLmvDIaI.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;
}
/* latin-ext */
@font-face {
  font-family: 'Martian Mono';
  font-style: normal;
  font-weight: 300 700;
  font-stretch: 75% 112.5%;
  font-display: swap;
  src: url(/assets/fonts/MartianMono-latin-ext-normal-2V0aKIcADoYhV6w87xrTKjsSC6nIRWbh8q05.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Martian Mono';
  font-style: normal;
  font-weight: 300 700;
  font-stretch: 75% 112.5%;
  font-display: swap;
  src: url(/assets/fonts/MartianMono-latin-normal-2V0aKIcADoYhV6w87xrTKjsSBanIRWbh8g.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;
}
/* latin-ext */
@font-face {
  font-family: 'Newsreader';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/Newsreader-latin-ext-italic-cY9kfjOCX1hbuyalUrK439vogqC9yFZCYg7oRZaLP4obnf7fTXglsMwoT9ZJFjSShVCjzSZ-Mg.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Newsreader';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/Newsreader-latin-italic-cY9kfjOCX1hbuyalUrK439vogqC9yFZCYg7oRZaLP4obnf7fTXglsMwoT9ZHFjSShVCjzSY.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;
}
/* latin-ext */
@font-face {
  font-family: 'Newsreader';
  font-style: normal;
  font-weight: 300 600;
  font-display: swap;
  src: url(/assets/fonts/Newsreader-latin-ext-normal-cY9AfjOCX1hbuyalUrK439DyjIJFJpeBZQ.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Newsreader';
  font-style: normal;
  font-weight: 300 600;
  font-display: swap;
  src: url(/assets/fonts/Newsreader-latin-normal-cY9AfjOCX1hbuyalUrK4397yjIJFJpc.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;
}

/* ==========================================================================
   SantiXS — the one stylesheet.

   Organised as: tokens → reset → type → shell → chrome → primitives → pages
   → responsive. Every colour and size is a token; nothing below the token
   block hardcodes a hex value. Selectors stay at one class deep, except tone
   overrides, which are deliberately two (`.section--dark .x`) so a tone always
   outranks the base rule it is correcting.

   build.mjs concatenates fonts.css ahead of this file into one stylesheet, so
   there is no @import here and no second request before the faces resolve.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

:root {
  /* Palette. Six values. Indigo and amber are the client's own — amber is the
     XS in the wordmark, indigo the brand blue — and everything else is built
     to serve them. The paper is a cool blue-grey rather than a warm cream so
     the indigo bands read as the same family of colour, not as a dark mode. */
  --ink:     #0f1533;   /* the dark ground: hero, dark bands, masthead, footer */
  --indigo:  #25318d;   /* labels and links on paper; the photographic duotone */
  --amber:   #ffb400;   /* actions, and the gain on every delta               */
  --paper:   #edeef2;   /* the reading ground                                 */
  --slate:   #7c86ae;   /* metadata, hairlines on dark, the "before" figure   */
  --claret:  #8c2f1e;   /* the two or three figures a case study leads with   */

  /* Tints. Derived from the six, never a seventh hue. */
  --paper-lift:  #f6f7f9;   /* a panel raised off the paper                   */
  --paper-sink:  #e2e4ec;   /* a band sunk into it                            */
  --ink-lift:    #1a2149;   /* a panel raised off the dark ground             */
  --amber-deep:  #e8a300;   /* the same action, pressed                       */
  --rule:        #cbcfdd;
  --rule-invert: rgba(124, 134, 174, 0.35);

  --text:        #171c33;
  --text-soft:   #4a5170;
  --text-invert: #eef0f6;

  /* Marker. Amber against paper measures 1.54:1 — as a 2px hairline it is
     effectively invisible, which is why a page of amber-marked sections read
     as one undifferentiated column of text. Amber is a dark-ground colour and
     a fill colour; on paper the same job goes to indigo at 9.52:1. Every rule,
     tick and bullet reads --marker, and the dark tones re-point it to amber. */
  --marker:        var(--indigo);
  --marker-quiet:  var(--rule);

  /* The traced ring round a button. It is drawn just outside the amber fill,
     so it sits on the ground rather than on the button — which means it has to
     follow the ground like everything else here. Indigo on paper at 9.5:1;
     near-white on any ink band, where indigo measured 1.6:1 and the ring was
     simply invisible on every hero on the site. */
  --ring:          var(--indigo);

  /* The emphasis colour, on the same principle. Indigo emphasises on paper;
     against the ink ground it measures 1.62:1 and the emphasised word is the
     hardest thing on the page to read — which is precisely backwards. */
  --emphasis:      var(--indigo);
  --alarm:         var(--claret);

  /* Motion. One curve, three durations, so a reveal on the approach page and a
     bar on the home page are recognisably the same gesture. */
  --ease:      cubic-bezier(0.22, 0.7, 0.2, 1);
  --dur-fast:  0.18s;
  --dur-mid:   0.5s;
  --dur-slow:  0.85s;

  /* Type. Three faces, each variable, each with one job.

     Archivo carries a width axis, and the display sizes are set expanded
     (118%) — an all-lowercase headline has no ascender rhythm to give it
     presence, so the width does that work instead. Newsreader reads the body:
     a serif here is a deliberate inversion, because the whole category sets
     body copy in a neutral grotesque. Martian Mono is also width-variable,
     which is what lets a long figure and a short one hold the same slot. */
  --display: 'Archivo', 'Helvetica Neue', Arial, sans-serif;
  --body:    'Newsreader', 'Iowan Old Style', Georgia, serif;
  --mono:    'Martian Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Scale. */
  --t-hero:    clamp(2.5rem, 1.1rem + 5.9vw, 4.75rem);
  --t-display: clamp(1.875rem, 1.15rem + 3vw, 3rem);
  --t-stat:    clamp(1.75rem, 1.15rem + 2.4vw, 2.625rem);
  --t-card:    clamp(1.25rem, 1.05rem + 0.8vw, 1.625rem);
  --t-index:   clamp(1.5rem, 1.1rem + 1.7vw, 2.25rem);
  --t-kicker:  clamp(1.1875rem, 1rem + 0.8vw, 1.5625rem);
  --t-lede:    clamp(1.125rem, 1.05rem + 0.35vw, 1.3125rem);
  --t-body:    1.0625rem;
  --t-ui:      1.0625rem;
  --t-small:   0.9375rem;
  --t-meta:    0.6875rem;
  --t-micro:   0.625rem;

  /* Layout. The rail is the site's one structural idea: a fixed column on the
     left of every section holding the label, with the content squared off
     against it. --rail and --rail-gap are read by both .section__inner and
     .section-head, which have to declare the same track sizes. */
  --shell:    76rem;
  --rail:     10rem;
  --rail-gap: clamp(1.5rem, 3vw, 3rem);
  --gutter:   clamp(1.25rem, 5vw, 4rem);
  --flow:     1.1rem;
  --band:     clamp(3.5rem, 8vw, 7rem);
  --band-tight: clamp(2.5rem, 5vw, 4rem);
  --band-tall:  clamp(5rem, 11vw, 9.5rem);
  --masthead: 5.25rem;
}

/* On any dark ground the marker goes back to amber, which measures 10:1 there.
   One declaration, and every rule, tick and bullet on the site follows. */
.section--dark,
.cta-band,
.hero,
.colophon,
.masthead {
  --marker: var(--amber);
  --marker-quiet: var(--rule-invert);
  --emphasis: var(--amber);
  --alarm: var(--amber);
  /* The ring is drawn on the ground outside the button, so it follows the
     ground like everything else. Indigo measures 1.6:1 against the ink band —
     on every hero on this site the ring was simply not there. Paper-white
     measures 15:1. */
  --ring: var(--text-invert);
}

/* --------------------------------------------------------------------------
   2. Reset
   -------------------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--text);
  font-family: var(--body);
  font-size: var(--t-body);
  line-height: 1.6;
  font-optical-sizing: auto;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, h4, p, ul, ol, dl, dd, figure, blockquote { margin: 0; }
ul, ol { padding: 0; list-style: none; }
img { display: block; max-width: 100%; height: auto; }
a { color: inherit; }
summary::-webkit-details-marker { display: none; }

:focus-visible {
  outline: 3px solid var(--marker);
  outline-offset: 3px;
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 0.75rem 1.25rem;
  background: var(--amber);
  color: var(--ink);
  font-family: var(--mono);
  font-size: var(--t-meta);
}
.skip:focus { left: 0; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* --------------------------------------------------------------------------
   3. Type primitives
   -------------------------------------------------------------------------- */

/* Every headline on the site. Expanded and set tight: the copy is written
   all-lowercase, so width and weight are the only things carrying emphasis. */
.display {
  font-family: var(--display);
  font-weight: 600;
  font-stretch: 118%;
  font-size: var(--t-display);
  line-height: 1.06;
  letter-spacing: -0.022em;
  color: var(--ink);
}

/* The source set the emphasised phrase of a heading in italic. Archivo ships
   no italic, and left alone the browser synthesises an oblique that reads as a
   mistake at hero size. Emphasis moves onto the width axis instead — the em
   goes wider and heavier than the line around it, which is the axis this
   family was chosen for. font-style has to be reset explicitly: the emphasis
   is marked up as <em>, and that is italic by user-agent default. */
.display--italic {
  font-style: normal;
  font-weight: 700;
  font-stretch: 125%;
}

/* The turn in a headline — the second sentence, the one that lands.
 *
 * `display--italic` above is not italic and never was: Archivo ships no italic
 * in this build, so the class has always been weight and width doing an
 * impression of one. It reads as emphasis but not as a change of voice.
 *
 * Newsreader has a real italic and is already loaded for body copy. Setting
 * the turn in it gives a hero two voices — the grotesque states the claim, the
 * serif answers it — which is the hero treatment Patrick picked out of the
 * prototype. Sized up 6% because Newsreader sets smaller than Archivo at the
 * same pixel value, and opsz pushed to 72 so the thins survive at hero scale
 * instead of going spindly. */
.display--turn {
  font-family: var(--body);
  font-style: italic;
  font-weight: 400;
  font-stretch: normal;
  font-size: 1.06em;
  font-variation-settings: 'opsz' 72;
  letter-spacing: -0.012em;
}

.eyebrow {
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-weight: 500;
  font-stretch: 100%;
  line-height: 1.45;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--indigo);
}
.eyebrow--soft    { color: var(--text-soft); }
.eyebrow--on-dark { color: var(--text-invert); }
.eyebrow--muted   { color: var(--slate); }
.eyebrow--amber   { color: var(--marker); }

/* An eyebrow that carries a drawn stroke under it. Not every eyebrow gets one
   — that is the whole point. Eight identical labels with eight identical marks
   under them would be the same uniformity in a different costume. */
.eyebrow--drawn {
  position: relative;
  display: inline-block;
  /* A grid or flex item is blockified, so inline-block silently becomes block
     and the element stretches to its track — which is why the stroke ran the
     full width of the container on the logo strip at phone widths, and why it
     hung 100px below the label in a section head. Belt and braces, at the
     component rather than at each of the four places it is used. */
  justify-self: start;
  align-self: start;
  width: fit-content;
  max-width: 100%;
  padding-bottom: 0.3rem;
  transform: rotate(var(--tilt, -0.7deg));
  transform-origin: left center;
}
.eyebrow--drawn > svg {
  position: absolute;
  left: -3%; bottom: 0;
  width: 106%; height: 9px;
  overflow: visible;
}
.hero .eyebrow--drawn {
  padding-bottom: 0.45rem;
  margin-bottom: 0.3rem;
}
.hero .eyebrow--drawn > svg { left: -2%; bottom: 2px; width: 112%; height: 11px; }
.hero .eyebrow--drawn path { stroke-width: 2.6; }

.lede {
  font-size: var(--t-lede);
  line-height: 1.55;
  color: var(--text);
  max-width: 58ch;
}

/* The blunt one-line assertions the copy turns on. Set in the display face at
   reading size so they read as the writer raising their voice, not as a
   heading interrupting the paragraph. */
.kicker {
  font-family: var(--display);
  font-weight: 500;
  font-stretch: 108%;
  font-size: var(--t-kicker);
  line-height: 1.25;
  letter-spacing: -0.014em;
  color: var(--ink);
}

.strike { text-decoration: line-through; text-decoration-thickness: 2px; }
/* These three follow the ground they land on. Written against the paper
   values they were only ever correct in a paper section, and a hero is not
   one — which is how an emphasised hero word ended up at 1.62:1 on three
   pages while the equivalent word on the home page was fine. */
.flag   { color: var(--alarm); }
.accent { color: var(--emphasis); }
.claret { color: var(--alarm); }

.link-list { display: grid; gap: 0.8rem; justify-items: start; }

.arrow-link {
  font-family: var(--mono);
  font-size: var(--t-small);
  font-stretch: 92%;
  color: var(--marker);
  text-decoration: none;
  padding-bottom: 0.28rem;
  transition: color 0.15s ease;
}
.arrow-link:hover { color: var(--ink); border-bottom-color: var(--ink); }
.arrow-link__arrow { padding-left: 0.45em; }

/* The one filled control on the site, so amber never has to compete with
   itself. Amber on paper fails as text at any size — it is only ever a fill
   with ink on top of it. */
.button {
  display: inline-block;
  padding: 0.95rem 1.6rem;
  background: var(--amber);
  color: var(--ink);
  font-family: var(--mono);
  font-size: var(--t-small);
  font-stretch: 92%;
  letter-spacing: -0.01em;
  text-decoration: none;
  border: 0;
  transition: background-color 0.15s ease, transform 0.15s ease;
}
/* The hover used to flip the fill to near-white and lift the control 2px — a
   button that changes colour that far has been replaced, not pressed. It now
   deepens on the same hue and takes a rule underneath, and :active gives the
   press somewhere to go. */
.button {
  border-bottom: 2px solid transparent;
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}
.button:hover  { background: var(--amber-deep); border-bottom-color: var(--ink); }
.button:active { transform: translateY(1px); }

/* --------------------------------------------------------------------------
   4. Shell and sections
   -------------------------------------------------------------------------- */

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section {
  position: relative;
  padding-block: var(--band);
  background: var(--paper);
  color: var(--text);
}
.section[id] { scroll-margin-top: var(--masthead); }
.section--alt  { background: var(--paper-sink); }
.section--dark { background: var(--ink); color: var(--text-invert); }

.section--dark .display    { color: var(--text-invert); }
.section--dark .lede       { color: var(--text-invert); }
.section--dark .kicker     { color: var(--marker); }
.section--dark .eyebrow    { color: var(--slate); }
.section--dark .arrow-link { color: var(--text-invert); }
.section--dark .arrow-link:hover { color: var(--marker); }

/* A photograph laid under a section as texture. It never sets the ground
   colour — the tone owns that, and a photo that repainted it would silently
   outrank .section--dark and turn a dark band pale. No mask-image and no
   backdrop-filter: both crash Safari on fixed layers. */
.section__photo {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  isolation: isolate;
}
.section__photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.055;
  filter: grayscale(1) contrast(1.1);
}
.section--dark .section__photo img { opacity: 0.16; mix-blend-mode: luminosity; }
.section__inner { position: relative; }

/* The rail. Every section is two columns: the label on the left, everything
   else squared off against a single edge down the whole page. */
.section__inner {
  display: grid;
  grid-template-columns: minmax(0, var(--rail)) minmax(0, 1fr);
  column-gap: var(--rail-gap);
  /* The rhythm between modules inside a section, and for a long time there was
     none: this grid had a column gap and no row gap, so every block a section
     held stacked against the one above it with zero space. What hid it is that
     some components carry their own margins, so the pages built out of those
     looked fine and the pages built out of the others did not. On /gtm-diagnosis
     the last line of a paragraph and the drawn ring round the first figure were
     touching, and the same collision ran twice more down the page.
     
     Sibling margins were tried first and are the wrong instrument here: a
     margin on a grid item is at the mercy of whatever else sets one, and two
     separate attempts measured clean in one place and left the collision in
     another. The container owns the rhythm. One value, every section, every
     page. */
  row-gap: clamp(2.25rem, 4.5vw, 3.5rem);
}
.section__inner > * { grid-column: 2; }

.section-head {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: minmax(0, var(--rail)) minmax(0, 1fr);
  column-gap: var(--rail-gap);
  row-gap: var(--flow);
}
.section-head .eyebrow { grid-column: 1; grid-row: 1; align-self: start; justify-self: start; }
.section-head .display { grid-column: 2; grid-row: 1; max-width: 18ch; }
.section-head .lede    { grid-column: 2; grid-row: 2; }

/* The rule that used to open every rail label has been removed. An uppercase
   mono label with a short rule over it is a recognisable piece of generated
   chrome, and repeating it eight to ten times down a page turns a structural
   device into wallpaper — nothing is marked if everything is. The rail already
   says where a section begins; the mark is now spent once at the top of a page
   (the hero) and once at the bottom (the call to action). */

/* Sections do not all weigh the same. A band that carries the argument gets
   room; a band that carries one line does not. Uniform vertical rhythm down
   the whole page is what makes a site read as generated. */
.section--tall  { padding-block: var(--band-tall); }
.section--tight { padding-block: var(--band-tight); }

.section--offset .section__inner > * { grid-column: 2; }
.section--offset .section-head .display { max-width: 22ch; }

/* --------------------------------------------------------------------------
   5. Masthead
   -------------------------------------------------------------------------- */

.masthead {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--ink);
  color: var(--text-invert);
  border-bottom: 1px solid var(--rule-invert);
}
.masthead__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gutter);
  min-height: var(--masthead);
}
.masthead__brand { display: block; flex-shrink: 0; }
.masthead__brand img { width: clamp(6.5rem, 12vw, 9rem); height: auto; }

.nav__list {
  display: flex;
  align-items: center;
  gap: clamp(0.75rem, 2vw, 1.75rem);
}
.nav__link {
  display: block;
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-invert);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  padding-block: 0.4rem;
  border-bottom: 1px solid transparent;
}
.nav__link:hover { color: var(--marker); }
.nav__link--cta { color: var(--marker); border-bottom-color: var(--marker); }
.nav__link--cta:hover { color: var(--text-invert); border-bottom-color: var(--text-invert); }

.nav__group { position: relative; }
.nav__group > .nav__link { list-style: none; }
.nav__submenu {
  position: absolute;
  top: calc(100% + 0.9rem);
  left: -1rem;
  min-width: 15rem;
  padding: 0.5rem 0;
  background: var(--ink-lift);
  border-top: 2px solid var(--marker);
}
.nav__sublink {
  display: block;
  padding: 0.6rem 1rem;
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  color: var(--text-invert);
  text-decoration: none;
  white-space: nowrap;
}
.nav__sublink:hover { color: var(--marker); }

/* --------------------------------------------------------------------------
   6. Hero
   -------------------------------------------------------------------------- */

.hero {
  position: relative;
  display: flex;
  align-items: flex-end;
  min-height: min(34rem, 82svh);
  padding-block: clamp(3.5rem, 8vw, 6rem);
  background: var(--ink);
  color: var(--text-invert);
  overflow: hidden;
}

/* The photographs are duotoned into the palette rather than dimmed behind a
   scrim: luminosity over the brand indigo makes every hero on the site the
   same blue, whatever the source picture was shot in. */
.hero__photo {
  position: absolute;
  inset: 0;
  background: var(--indigo);
  isolation: isolate;
}
.hero__photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  mix-blend-mode: luminosity;
  filter: contrast(1.08) brightness(0.62);
}
.hero__photo::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(8deg, rgba(15, 21, 51, 0.94) 12%, rgba(15, 21, 51, 0.6) 58%, rgba(15, 21, 51, 0.4) 100%);
}
.hero--open .hero__photo img { filter: contrast(1.05) brightness(0.85); }
.hero--open .hero__photo::after {
  background: linear-gradient(75deg, rgba(15, 21, 51, 0.92) 8%, rgba(15, 21, 51, 0.5) 52%, rgba(15, 21, 51, 0.18) 100%);
}

.hero__inner {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--flow);
}
.hero__title {
  font-size: var(--t-hero);
  font-weight: 600;
  font-stretch: 118%;
  line-height: 0.98;
  letter-spacing: -0.032em;
  color: var(--text-invert);
  max-width: 17ch;
  margin-block: 0.5rem 0.3rem;
}
.hero__lede {
  font-size: var(--t-lede);
  color: var(--text-invert);
  max-width: 52ch;
}
.hero__kicker {
  font-family: var(--display);
  font-weight: 500;
  font-stretch: 108%;
  font-size: var(--t-kicker);
  line-height: 1.25;
  letter-spacing: -0.014em;
  color: var(--marker);
  max-width: 34ch;
}
.hero__cta { margin-top: 1.1rem; }
.hero__mark { height: 2.25rem; width: auto; margin-bottom: 0.5rem; }
.hero__meta {
  margin-top: 1rem;
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.04em;
  color: var(--slate);
}
.hero .eyebrow::before {
  content: none;
  display: block;
  width: 1.75rem;
  height: 2px;
  margin-bottom: 0.85rem;
  background: var(--marker);
}

/* --------------------------------------------------------------------------
   7. Logo strip
   -------------------------------------------------------------------------- */

/* Light, deliberately. The marks arrive as a mix of dark-on-transparent and
   light-on-transparent artwork, and no single filter flattens both onto a dark
   ground without turning one of them into a solid block. A pale band also
   breaks the hero and the first dark section apart. */
.logo-strip {
  padding-block: clamp(2rem, 4vw, 3rem);
  background: var(--paper-sink);
  color: var(--text);
}
.logo-strip__inner {
  display: grid;
  grid-template-columns: minmax(0, var(--rail)) minmax(0, 1fr);
  column-gap: var(--rail-gap);
  row-gap: 1.5rem;
  align-items: center;
}
.logo-strip .eyebrow { color: var(--text-soft); }
.logo-strip__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1.5rem 2.5rem;
}
/* The marks arrive in a dozen different brand colours; desaturating them is
   what makes one row read as a row rather than a collage. */
.logo-strip__list img {
  height: clamp(1.4rem, 3vw, 2.1rem);
  width: auto;
  object-fit: contain;
  filter: grayscale(1) contrast(1.15);
  opacity: 0.7;
  transition: opacity 0.15s ease;
}
.logo-strip__list img:hover { opacity: 1; }

/* Client marks at full strength. Desaturating them makes the row read as one
   row rather than a collage, which is why it is still the default for the
   affiliations strip — but these are the companies the work was done for, and
   holding them back to 70% grey understates the only third-party proof on the
   page. The band stays light because these marks are a mix of dark-on- and
   light-on-transparent artwork and a light ground is the one that carries
   both. */
.logo-strip--full .logo-strip__list img {
  filter: none;
  opacity: 1;
}
.logo-strip__note { grid-column: 2; max-width: 52ch; color: var(--text-soft); }

/* --------------------------------------------------------------------------
   8. Cards, lists, stats
   -------------------------------------------------------------------------- */

/* ── The index ────────────────────────────────────────────────────────────
   Three parallel situations, not three steps — so they were never a numbered
   sequence, and the numbers said otherwise. Three equal cards side by side is
   also the reflex shape, and it forces every entry to the length of the
   longest one. This is an index instead: full-bleed rows, the claim on the
   left at headline size and the qualification on the right at reading size,
   separated by whitespace and a hairline rather than by a box. It reads as
   three things rather than three paragraphs. */
.cards { display: grid; }
.card { border-top: 1px solid var(--rule); }
.cards > .card:last-child { border-bottom: 1px solid var(--rule); }
.section--dark .card,
.section--dark .cards > .card:last-child { border-color: var(--rule-invert); }

.card__link {
  display: grid;
  grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
  column-gap: clamp(1.5rem, 4vw, 3.5rem);
  row-gap: 0.6rem;
  align-items: start;
  padding-block: clamp(1.5rem, 3vw, 2.4rem);
  text-decoration: none;
  color: inherit;
}
/* Not every index row is a link. Where there is no anchor to carry the grid,
   the row itself carries it, so both variants land on the same two columns. */
.card:not(.card--linked) {
  display: grid;
  grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
  column-gap: clamp(1.5rem, 4vw, 3.5rem);
  row-gap: 0.45rem;
  align-content: start;
  padding-block: clamp(1.5rem, 3vw, 2.4rem);
}
/* The unlinked variant carries a label above its title, and the body sat in
   the same row as that label — so the row grew to the body's height and left
   the label stranded 80px above the title it belongs to. Spanning the body
   across both rows lets the first row collapse to the label, which puts the
   label back on top of its own heading. */
.card:not(.card--linked) .card__number { grid-row: 1; }
.card:not(.card--linked) .card__title  { grid-row: 2; }
.card:not(.card--linked) .card__body   { grid-row: 1 / span 2; align-self: start; }
.card--linked .card__link { position: relative; }
/* The hover draws the marker across the row it is on, rather than painting a
   rule under every row permanently. Motion that says which one, not decor. */
.card--linked .card__link::before {
  content: '';
  position: absolute;
  top: -1px;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--marker);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-mid) var(--ease);
}
.card--linked:hover .card__link::before,
.card--linked .card__link:focus-visible::before { transform: scaleX(1); }
.card--linked:hover .card__title { color: var(--indigo); }
.section--dark .card--linked:hover .card__title { color: var(--marker); }

.card__number {
  grid-column: 1;
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--indigo);
}
.card__title {
  grid-column: 1;
  font-size: var(--t-index);
  line-height: 1.08;
  max-width: 17ch;
  color: var(--ink);
  transition: color var(--dur-fast) var(--ease);
}
.card__body {
  grid-column: 2;
  grid-row: 1;
  font-size: var(--t-ui);
  color: var(--text-soft);
  max-width: 46ch;
}
.card__cue {
  grid-column: 2;
  grid-row: 2;
  justify-self: start;
  margin-top: 0.2rem;
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.05em;
  color: var(--indigo);
  transition: transform var(--dur-fast) var(--ease);
}
.card--linked:hover .card__cue { transform: translateX(0.35rem); }
.section--dark .card__number,
.section--dark .card__cue { color: var(--marker); }
.section--dark .card__title  { color: var(--text-invert); }
.section--dark .card__body   { color: var(--slate); }

@media (max-width: 60rem) {
  .card__link { grid-template-columns: minmax(0, 1fr); row-gap: 0.7rem; }
  .card__number, .card__title, .card__body, .card__cue { grid-column: 1; }
  /* The unlinked card was never collapsed here — only the linked one was. Its
     number and title kept the explicit `grid-row: 1` and `grid-row: 2` from the
     two-column layout while the body fell back to auto placement, so all three
     landed in the same cell and the label, the heading and the paragraph drew
     on top of each other. Three cards, every one unreadable on a phone. */
  .card:not(.card--linked) { grid-template-columns: minmax(0, 1fr); row-gap: 0.55rem; }
  .card:not(.card--linked) .card__number,
  .card:not(.card--linked) .card__title,
  .card:not(.card--linked) .card__body { grid-row: auto; grid-column: 1; }
  .card__title { max-width: 22ch; }
}

/* ── The chain ────────────────────────────────────────────────────────────
   For a sequence where the argument is that it only works end to end. Five
   paragraphs stacked say "here are five things"; a chain says "these are in
   series, and one broken link stops the whole thing" — which is the actual
   claim. Each link carries its name, one line of what it is, and the way it
   fails, because the failure is the part a reader recognises.

   The rule runs behind the row and each link sits on it, so the connection is
   drawn rather than described. On a phone the chain turns vertical and the
   rule runs down the left instead. */
.chain {
  display: grid;
  grid-template-columns: repeat(var(--links, 5), minmax(0, 1fr));
  column-gap: clamp(0.75rem, 1.6vw, 1.25rem);
  position: relative;
}
.chain::before { content: none; }
/* Explicit rows rather than source order: the description takes the flexible
   row so the five failure lines land on one baseline. Ragged, they read as
   five separate notes; aligned, they read as a row you can scan straight
   across — which is the whole reason for putting them side by side. */
.chain__link {
  position: relative;
  display: grid;
  grid-template-rows: auto auto 1fr auto;
  gap: 0.4rem;
  padding-top: 1.6rem;
}
/* The node on the rule. Solid, so the sequence reads as five stops on one
   line rather than five columns that happen to be adjacent. */
.chain__link::before {
  content: '';
  position: absolute;
  top: -2px;
  left: -3px;
  width: 17px;
  height: 17px;
  background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 22'><path d='M11,2 C16.6,2 20,5.4 20,11 C20,16.6 16.2,20 10.6,20 C5,20 2,16.2 2,10.6 C2,5 5.4,2 11,2 C13,2 14.8,2.4 16,3.2' fill='none' stroke='%2325318d' stroke-width='2'/></svg>") center / contain no-repeat;
}
.section--dark .chain__link::before {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 22'><path d='M11,2 C16.6,2 20,5.4 20,11 C20,16.6 16.2,20 10.6,20 C5,20 2,16.2 2,10.6 C2,5 5.4,2 11,2 C13,2 14.8,2.4 16,3.2' fill='none' stroke='%23ffb400' stroke-width='2'/></svg>");
}
.chain__index {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.08em;
  color: var(--text-soft);
}
.chain__name {
  font-family: var(--display);
  font-weight: 600;
  font-stretch: 108%;
  font-size: var(--t-ui);
  line-height: 1.12;
  letter-spacing: -0.015em;
  color: var(--ink);
}
.chain__what { font-size: var(--t-small); color: var(--text); }
.chain__fail {
  font-size: var(--t-small);
  color: var(--claret);
  padding-top: 0.75rem;
  border-top: 1px solid var(--rule);
}
.chain__fail-label {
  display: block;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.08em;
  color: var(--text-soft);
  margin-bottom: 0.2rem;
}
.section--dark .chain::before { background: var(--rule-invert); }
.section--dark .chain__name { color: var(--text-invert); }
.section--dark .chain__what { color: var(--text-invert); }
.section--dark .chain__fail { color: var(--amber); border-top-color: var(--rule-invert); }

@media (max-width: 60rem) {
  .chain { grid-template-columns: minmax(0, 1fr); row-gap: 0; }
  .chain::before { top: 0; bottom: 0; left: 5px; right: auto; width: 1px; height: auto; }
  .chain__link { grid-template-rows: none; padding: 0 0 1.6rem 1.9rem; }
  .chain__link:last-child { padding-bottom: 0; }
  .chain__link::before { top: 0.15rem; }
}

/* ── The matrix ───────────────────────────────────────────────────────────
   The comparison the copy makes in prose on five pages, made once as a table.
   A table is the honest form for it: the same questions asked of all three
   options, answered in the same words, in a shape a reader can check rather
   than take on trust.

   The middle column is not styled as "recommended" — that pattern is a thumb
   on the scale, and it is the reason most comparison tables read as marketing
   rather than as information. The last row is the one that earns the rest:
   where SantiXS is the wrong answer.

   On a phone it scrolls sideways rather than stacking. Stacking a comparison
   destroys the only thing it is for — reading across. */
.matrix-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity;
}
.matrix th { scroll-snap-align: start; }
/* Only shown where the table actually overflows. A sideways-scrolling table
   with no affordance is a table with two columns the reader never finds. */
/* A table that scrolls sideways has to look like a thing with edges, or the
   reader never learns there is more to the right of the fold. A drawn box
   round it, and the hint carries an arrow pointing the way out. */
/* The frame has to sit on a wrapper, not on the scroller: a scroll container's
   background paints across its scrollable width, so the drawn box ran off to
   the right with the table instead of staying round the visible fold. */
.matrix-frame {
  padding: clamp(0.7rem, 1.8vw, 1rem);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 300' preserveAspectRatio='none'><path d='M3,6 C120,2 280,3 396,5 C398,100 397,200 396,294 C280,297 120,296 4,294 C2,200 2,100 3,6 Z' fill='none' stroke='%2325318d' stroke-width='1.8' stroke-linejoin='round' opacity='0.4'/></svg>");
  background-repeat: no-repeat;
  background-size: 100% 100%;
}
.matrix-hint__arrow {
  display: inline-block;
  width: 2.4rem; height: 0.7rem;
  margin-left: 0.5rem;
  vertical-align: -1px;
  background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 14'><g fill='none' stroke='%2325318d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M2,7.4 C14,6 28,8.2 43,7'/><path d='M35,2.5 L45,7.2 L35,11.6'/></g></svg>") right center / contain no-repeat;
}
.matrix-hint { display: none; }
.matrix {
  width: 100%;
  min-width: 40rem;
  border-collapse: collapse;
  text-align: left;
}
.matrix th,
.matrix td {
  padding: 0.95rem 1.1rem 0.95rem 0;
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
}
.matrix thead th {
  padding-top: 0;
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--indigo);
  border-bottom-width: 2px;
}
.matrix thead th:first-child { color: transparent; }
/* The one column that is us is marked by a ground shift, not by a badge. */
.matrix thead th.matrix__mine,
.matrix td.matrix__mine { background: var(--paper-lift); padding-inline: 1.1rem; }
.section--alt .matrix thead th.matrix__mine,
.section--alt .matrix td.matrix__mine { background: var(--paper); }
.matrix tbody th {
  width: 12rem;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.matrix td { font-size: var(--t-small); color: var(--text); }
.matrix tbody tr:last-child td,
.matrix tbody tr:last-child th { border-bottom: 0; }
.matrix__note {
  margin-top: 0.9rem;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 92%;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-soft);
}

/* ── The strata ───────────────────────────────────────────────────────────
   The one claim the whole site rests on — that the firm, the hire and the
   tools did not fail on merit, they failed because nothing was under them —
   is asserted in prose on three pages. Drawn, it takes no reading at all: the
   things you bought sit on a dashed edge with a gap beneath, and the two
   layers that should have been there are set solid below it. */
.strata { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; }
.strata__layer {
  display: grid;
  grid-template-columns: minmax(0, 9.5rem) minmax(0, 1fr);
  column-gap: clamp(1rem, 2.5vw, 2rem);
  align-items: center;
  padding-block: clamp(0.9rem, 2vw, 1.25rem);
  border-top: 1px solid var(--rule);
}
.strata__layer:last-of-type { border-bottom: 1px solid var(--rule); }
.strata__label {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.strata__row { display: flex; flex-wrap: wrap; gap: 0.5rem 0.6rem; }
.strata__row li {
  padding: 0.4rem 0.75rem;
  background: var(--paper-lift);
  font-size: var(--t-small);
  color: var(--text);
}
.section--alt .strata__row li { background: var(--paper); }
/* The layer that never held. Dashed, unfilled, and set adrift by the gap
   under it — which is the argument, drawn. */
.strata__layer--loose { border-top: 0; padding-bottom: clamp(1.4rem, 3vw, 2.25rem); }
.strata__layer--loose .strata__row li {
  background: none;
  border: 1px dashed var(--slate);
  color: var(--text-soft);
}
.strata__layer--built .strata__row li {
  background: var(--ink);
  color: var(--text-invert);
}
/* Indented to the chip column rather than placed in one: giving this a
   grid-column of its own opened an implicit second track and laid the layers
   out side by side, which is the opposite of the point. */
.strata__gap {
  padding-left: calc(9.5rem + clamp(1rem, 2.5vw, 2rem));
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--claret);
  padding-bottom: clamp(0.9rem, 2vw, 1.25rem);
}
.strata__caption {
  margin-top: 1.1rem;
  font-size: var(--t-small);
  color: var(--text-soft);
  max-width: 52ch;
}

@media (max-width: 60rem) {
  .strata__layer { grid-template-columns: minmax(0, 1fr); row-gap: 0.6rem; align-items: start; }
  .strata__gap { padding-left: 0; }
  .matrix tbody th { width: 9rem; }
  /* A table that scrolls sideways has to look like a thing with edges, or the
   reader never learns there is more to the right of the fold. A drawn box
   round it, and the hint carries an arrow pointing the way out. */
/* The frame has to sit on a wrapper, not on the scroller: a scroll container's
   background paints across its scrollable width, so the drawn box ran off to
   the right with the table instead of staying round the visible fold. */
.matrix-frame {
  padding: clamp(0.7rem, 1.8vw, 1rem);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 300' preserveAspectRatio='none'><path d='M3,6 C120,2 280,3 396,5 C398,100 397,200 396,294 C280,297 120,296 4,294 C2,200 2,100 3,6 Z' fill='none' stroke='%2325318d' stroke-width='1.8' stroke-linejoin='round' opacity='0.4'/></svg>");
  background-repeat: no-repeat;
  background-size: 100% 100%;
}
.matrix-hint__arrow {
  display: inline-block;
  width: 2.4rem; height: 0.7rem;
  margin-left: 0.5rem;
  vertical-align: -1px;
  background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 14'><g fill='none' stroke='%2325318d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M2,7.4 C14,6 28,8.2 43,7'/><path d='M35,2.5 L45,7.2 L35,11.6'/></g></svg>") right center / contain no-repeat;
}
.matrix-hint {
    display: block;
    margin-bottom: 0.7rem;
    font-family: var(--mono);
    font-size: var(--t-micro);
    font-stretch: 88%;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--text-soft);
  }
}

/* ── Chips ────────────────────────────────────────────────────────────────
   A set of named parts, read at a glance instead of down a paragraph. */
.chips { display: flex; flex-wrap: wrap; gap: 0.5rem 0.6rem; }
.chips li {
  padding: 0.4rem 0.75rem;
  background: var(--paper-lift);
  font-size: var(--t-small);
  color: var(--text);
}
.section--alt .chips li { background: var(--paper); }
.section--dark .chips li { background: var(--ink-lift); color: var(--text-invert); }

/* ── The nine box ─────────────────────────────────────────────────────────
   This was two hundred words describing a grid. It was always a grid: the
   copy names both axes, names the quadrants, and explains that a rep grinding
   in one corner needs the opposite of a rep coasting in another. Drawn, a
   revenue leader recognises the instrument in about a second and the two
   hundred words become a caption.

   The two cells that carry the argument are marked. Everything else is set
   back, because a nine box where every cell shouts is a nine box nobody
   reads. */
.ninebox {
  display: grid;
  grid-template-columns: 1.6rem minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) 1.6rem;
  gap: 0.6rem;
  max-width: 46rem;
}
.ninebox__grid {
  grid-column: 2;
  grid-row: 1;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-rows: repeat(3, minmax(0, 1fr));
  gap: 2px;
  background: var(--rule);
  border: 1px solid var(--rule);
}
.ninebox__cell {
  display: grid;
  align-content: start;
  gap: 0.3rem;
  padding: 0.85rem 0.9rem 1rem;
  background: var(--paper);
  min-height: 6.5rem;
}
.section--alt .ninebox__cell { background: var(--paper-sink); }
/* Specificity, deliberately matched: `.section--alt .ninebox__cell` is two
   classes deep, so a single-class modifier lost to it and the marked cells
   rendered light with invert text on them — invisible. */
.ninebox__grid > .ninebox__cell--key { background: var(--ink); }
.ninebox__where {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.ninebox__do {
  font-size: var(--t-small);
  line-height: 1.35;
  color: var(--text);
}
.ninebox__cell--key .ninebox__where { color: var(--amber); }
.ninebox__cell--key .ninebox__do    { color: var(--text-invert); }

.ninebox__axis {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.ninebox__axis--y {
  grid-column: 1;
  grid-row: 1;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  justify-self: center;
  align-self: center;
}
.ninebox__axis--x { grid-column: 2; grid-row: 2; justify-self: center; }
.ninebox__caption {
  grid-column: 1 / -1;
  margin-top: 0.4rem;
  font-size: var(--t-small);
  color: var(--text-soft);
  max-width: 56ch;
}

@media (max-width: 48rem) {
  .ninebox__cell { min-height: 0; padding: 0.6rem 0.55rem 0.7rem; }
  .ninebox__do { font-size: var(--t-micro); line-height: 1.3; }
  .ninebox__where { font-size: 0.5625rem; }
}

/* ── The spread ───────────────────────────────────────────────────────────
   One bar, divided. Three services listed down a page read as three things
   you can buy separately; drawn as segments of a single band they read as
   what they are — three surfaces of one system, which is the claim the copy
   underneath is making anyway. */
.spread {
  display: grid;
  grid-template-columns: repeat(var(--parts, 3), minmax(0, 1fr));
  gap: 3px;
}
.spread__part { display: grid; }
/* Explicit rows so the three cues land on one baseline. Ragged, they read as
   three separate blocks; aligned, as three parts of one bar. */
.spread__link {
  display: grid;
  grid-template-rows: auto auto 1fr auto;
  gap: 0.5rem;
  height: 100%;
  padding-top: 0.85rem;
  border-top: 3px solid var(--marker);
  text-decoration: none;
  color: inherit;
}
.spread__name {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.spread__title {
  font-size: var(--t-card);
  line-height: 1.1;
  max-width: 15ch;
  transition: color var(--dur-fast) var(--ease);
}
.spread__note { font-size: var(--t-small); color: var(--text-soft); max-width: 30ch; }
.spread__cue {
  align-self: end;
  padding-top: 0.4rem;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.07em;
  color: var(--indigo);
  transition: transform var(--dur-fast) var(--ease);
}
.spread__part:hover .spread__title { color: var(--indigo); }
.spread__part:hover .spread__cue { transform: translateX(0.3rem); }
.spread__caption { margin-top: 1.1rem; font-size: var(--t-small); color: var(--text-soft); }
.section--dark .spread__title { color: var(--text-invert); }
.section--dark .spread__cue { color: var(--marker); }

@media (max-width: 60rem) {
  .spread { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .spread__link { grid-template-rows: none; padding-block: 0.85rem 1.3rem; }
  .spread__title { max-width: 24ch; }
}

/* ── Receipts ─────────────────────────────────────────────────────────────
   Unretouched screenshots of what people actually wrote. A typeset pull quote
   is the same words with the evidence removed — the reader has only the site's
   word that anyone said it. The screenshot keeps the timestamps, the emoji
   reactions and the interface, none of which a marketing page would invent.

   They sit on the ink band on purpose: these are dark-mode Slack, and a dark
   screenshot dropped onto pale paper reads as a foreign object pasted in.
   On the dark ground the interface and the page share a value range, so the
   evidence sits inside the design rather than on top of it.

   The words are carried twice — in the image and in the alt text — so the
   quote survives for a screen reader, for search, and for anyone the image
   fails to load for. */
/* One column, capped near the screenshots' native width. Two columns halved
   them to 440px, which rendered the message text at about eight pixels — a
   quote nobody can read is not proof of anything. Stacked at full size the
   four of them cost barely more height and stay legible. */
.receipts {
  display: grid;
  gap: clamp(1rem, 2.5vw, 1.6rem);
  max-width: 46rem;
}
.receipt { display: flex; flex-direction: column; gap: 0.55rem; }
.receipt img {
  width: 100%;
  height: auto;
  border: 1px solid var(--rule);
}
.section--dark .receipt img { border-color: var(--rule-invert); }

/* A screenshot cannot reflow. At 390px the same image renders its text at
   about seven pixels, so on a phone the evidence steps aside and the words
   themselves are set in the page's own type. Both forms are always in the
   DOM: the image is marked decorative and the quote carries the content, so
   a screen reader gets the words exactly once on either. */
.receipt__text {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
@media (max-width: 48rem) {
  .receipt img { display: none; }
  .receipt__text {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip-path: none;
    white-space: normal;
    font-size: var(--t-ui);
    font-style: italic;
    line-height: 1.45;
    color: var(--text-invert);
    padding-left: 1rem;
    border-left: 2px solid var(--marker);
  }
}
.receipt__who {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--slate);
}
/* ---- A plate ---------------------------------------------------------------
   A photograph at full measure, inside the argument rather than beside it.
   `inset()` is the usual shape for a picture that has to be seen, and it is
   wrong for this one: the inset is a tall strip in the rail, and a landscape
   whiteboard cropped to a vertical slot loses the four quadrants that are the
   whole reason to show it. Width is the subject here, so the figure takes the
   measure and the caption sits under it in the same mono as every other label
   on the site. */
.plate {
  margin-top: clamp(1.5rem, 3vw, 2.25rem);
  max-width: 42rem;
  display: grid;
  gap: 0.7rem;
  align-content: start;
}

/* A figure beside the argument rather than under it. Centred at full measure a
   photograph reads as a blog illustration and pushes the prose into a seventh
   paragraph; alongside, it carries half the height of the block and the reader
   sees the evidence and the sentence about it at the same time. Same reasoning
   as the map bed, and the same collapse below 60rem, where there is no second
   column to sit in. */
.beside {
  /* In column two, squared to the section head and to every other block in the
     band. Spanning the rail bought the figure 140px and cost the section three
     different left edges stacked down one screen, which is the loudest thing on
     a page after the copy itself. The width comes out of the ratio instead: at
     1.15fr the figure is still about 540px, which is where the labels on the
     board stay legible, and the measure beside it is a comfortable 46
     characters. */
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
  column-gap: clamp(1.75rem, 4vw, 3.25rem);
  row-gap: clamp(1.5rem, 3vw, 2rem);
  align-items: start;
}
.beside > .plate { margin-top: 0; max-width: none; }
.beside__say { min-width: 0; max-width: 34rem; }
@media (max-width: 60rem) {
  .beside { grid-template-columns: minmax(0, 1fr); }
}
.plate img {
  width: 100%;
  height: auto;
  border: 1px solid var(--rule);
  filter: saturate(0.82) contrast(1.04);
}
.section--dark .plate img { border-color: var(--rule-invert); }
.plate__caption {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.06em;
  line-height: 1.5;
  text-transform: uppercase;
  color: var(--text-soft);
}
.section--dark .plate__caption { color: var(--slate); }

/* Colour follows the ground. This component was written for the ink band on the
   case study, so both the note and the screenshot border were set to their
   dark-ground values. On paper slate measures 3.08:1, which is under the floor
   for body text, and the inverted rule is all but invisible. Base values are now
   the paper ones and the dark band overrides them, which is the direction the
   rest of the stylesheet works in. */
.receipts__note {
  margin-top: clamp(1.25rem, 3vw, 2rem);
  font-size: var(--t-small);
  color: var(--text-soft);
  max-width: 60ch;
}
.section--dark .receipts__note { color: var(--slate); }

/* ── A slot for a real screenshot ─────────────────────────────────────────
   Deliberately unfilled and deliberately loud. The alternative — a generated
   dashboard — is a claim about work that was never done, and the buyers for
   this site use these tools daily and would know. The slot ships empty and
   states what belongs in it. */
.shot {
  display: grid;
  place-content: center;
  gap: 0.6rem;
  padding: clamp(1.5rem, 4vw, 2.75rem);
  min-height: 14rem;
  border: 2px dashed var(--slate);
  text-align: center;
}
.shot__flag {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--claret);
}
.shot__brief { font-size: var(--t-small); color: var(--text-soft); max-width: 44ch; margin-inline: auto; }
.section--dark .shot__brief { color: var(--slate); }

/* Numbered prose. The numbers carry order the reader needs — sequenced
   diagnoses and build phases — so they are set large enough to navigate by. */
.numbered { display: grid; gap: clamp(1.75rem, 3vw, 3rem); }
.numbered__item {
  display: grid;
  grid-template-columns: minmax(0, var(--rail)) minmax(0, 1fr);
  column-gap: var(--rail-gap);
  padding-top: 1.5rem;
  border-top: 1px solid var(--rule);
}
.numbered__index {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  align-items: flex-start;
  font-family: var(--mono);
  font-size: var(--t-small);
  font-stretch: 85%;
  font-weight: 500;
  letter-spacing: -0.02em;
  color: var(--indigo);
}
.numbered__icon { width: 2rem; height: 2rem; opacity: 0.9; }
.numbered__term {
  font-family: var(--display);
  font-weight: 600;
  font-stretch: 112%;
  font-size: var(--t-card);
  line-height: 1.14;
  letter-spacing: -0.018em;
  color: var(--ink);
  margin-bottom: 0.55rem;
}
.numbered__meta {
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.04em;
  color: var(--text-soft);
  margin-top: -0.3rem;
  margin-bottom: 0.8rem;
}
.numbered__body { font-size: var(--t-ui); max-width: 60ch; }
.numbered__body > p + p { margin-top: 0.9rem; }

.section--dark .numbered__item  { border-top-color: var(--rule-invert); }
.section--dark .numbered__index { color: var(--marker); }
.section--dark .numbered__term  { color: var(--text-invert); }

/* A coloured strip down the left edge of a block is the most mechanical tell
   in the whole vocabulary, and this stylesheet had it three ways. The aside
   separates itself the honest way instead: whitespace, then a shift in ground
   lightness. No rule, no colour, no box drawn around the text. */
.meta-block {
  margin-top: 1.4rem;
  padding: 1.15rem clamp(1rem, 2.5vw, 1.5rem);
  background: var(--paper-lift);
  max-width: 60ch;
}
.section--alt .meta-block { background: var(--paper); }
.section--dark .meta-block { background: var(--ink-lift); }
.meta-block__label {
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-soft);
  margin-bottom: 0.5rem;
}
.meta-block__body { font-size: var(--t-ui); line-height: 1.5; }
.meta-block__body ul { display: grid; gap: 0.45rem; }
.meta-block__body li { position: relative; padding-left: 1.1rem; font-size: var(--t-small); }
.meta-block__body li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.66em;
  width: 0.5rem;
  height: 1px;
  background: var(--slate);
}
.section--dark .meta-block__label { color: var(--slate); }

.stack__after { margin-top: clamp(1.5rem, 3vw, 2.5rem); }

/* ── The delta ────────────────────────────────────────────────────────────
   The site's signature, and the shape almost every number in the copy
   actually has: a before and an after. The track is the whole of the final
   figure; the slate part is what was already there when the engagement
   started and the amber part is what it moved. A stat with a single value
   falls back to the figure alone — there is no bar to draw. */

.stats {
  display: grid;
  grid-template-columns: repeat(var(--stat-columns, auto-fit), minmax(14rem, 1fr));
  column-gap: clamp(1.75rem, 3vw, 3rem);
  /* Each stat carries two to four lines of note under it, so the row gap has to
     clear a paragraph rather than a label — at 28px the next figure started
     before the previous explanation had finished, which on a phone reads as one
     continuous block instead of four separate numbers. */
  row-gap: clamp(3rem, 7vw, 4rem);
}

/* Components stacked inside one section had nothing between them: the figure
   grid began immediately under the dumbbell's note, and the ring round the
   first figure reached up into its last line.
   
   This was first written as a list of the pairs that had gone wrong, which
   fixed those pairs and left every pair nobody had looked at yet. On 19 August
   2026 a walk through the rendered pages found the same collision on
   /gtm-diagnosis and /research, where the figure grids sit between `.prose`
   blocks rather than after a dumbbell: the last line of a paragraph and the
   drawn ring round the first figure were touching, twice on one page, and the
   stat notes ran straight into the kicker underneath.
   
   So the rule is now about the components rather than about the pairs. Any
   sibling either side of a figure grid gets the same clearance, which covers
   the blocks that exist today and the ones added later. */
   A `.section__inner > ` scope was tried first and made it worse: on the pages
   that had the problem these blocks are nested deeper than the section's own
   children, so the rule stopped matching the exact cases it was written for.
   The universal form is the one that measures clean.

   The three pairs after it are named rather than generalised on purpose. A
   figure's caption and a table's note are siblings of the thing they describe,
   so a blanket rule pushes a caption away from its own picture. Clearance
   between components, tight inside one. */
* + .stats,
.stats + *,
* + .dumbbell,
.dumbbell + * { margin-top: clamp(1rem, 2vw, 1.5rem); }
/* Explicit rows rather than source order. A stat with no arrow draws no track,
   and in a flex column its label would ride up 5px above the labels either
   side of it; the empty row keeps a row of figures on one baseline whether or
   not each one has a bar. */
.stat {
  display: grid;
  grid-template-rows: auto 5px auto;
  row-gap: 0.55rem;
  align-content: start;
}
.stat__label {
  grid-row: 3;
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.stat__value {
  grid-row: 1;
  font-family: var(--display);
  font-weight: 600;
  font-stretch: 112%;
  font-size: var(--t-stat);
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--ink);
  white-space: nowrap;
}
.stat__note { grid-row: 4; font-size: var(--t-small); color: var(--text-soft); max-width: 38ch; }
/* A marked stat reads before-bad, after-good: the "from" carries the claret at
   full strength and the "to" lands in ink. The arrow was doing that work on its
   own; colour makes the direction legible at a glance, which is the only reason
   a case study puts two numbers next to each other in the first place.
   Direction does not matter — $14.73 to $9.45 falls and 3.09% to 8.69% rises,
   and both are the before in red and the after in ink. */
.stat--claret .stat__value { color: var(--claret); }
.stat--claret .stat__from { color: var(--claret); opacity: 1; }
.stat--claret .stat__to,
.stat--claret.stat--reduced .stat__to { color: var(--ink); }

.section--dark .stat__value { color: var(--text-invert); }
.section--dark .stat__label { color: var(--slate); }
.section--dark .stat__note  { color: var(--slate); }

.stat__figures {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.stat__from { color: var(--text-soft); }
.section--dark .stat__from { color: var(--slate); }
.stat__to   { color: var(--ink); }
.section--dark .stat__to,
.section--dark .stat--claret .stat__to { color: var(--text-invert); }
.section--dark .stat--claret .stat__from { color: var(--amber); opacity: 1; }
.stat__arrow {
  font-family: var(--mono);
  font-size: var(--t-lede);
  font-stretch: 92%;
  line-height: 1;
  color: var(--indigo);
  align-self: center;
}
.section--dark .stat__arrow { color: var(--marker); }

.stat__track {
  grid-row: 2;
  display: flex;
  height: 5px;
  width: 100%;
  max-width: 15rem;
  background: var(--rule);
  transform-origin: left center;
}
.section--dark .stat__track { background: var(--rule-invert); }
.stat__base { flex: var(--delta-base) 1 0; background: var(--slate); }
.stat__gain { flex: var(--delta-gain) 1 0; background: var(--marker); }

/* A figure that improved by falling — cost per lead. The win is the number
   that remains, so amber holds it and slate holds the part that was cut. */
.stat--reduced .stat__base { background: var(--marker); }
.stat--reduced .stat__gain { background: var(--slate); }
.stat--reduced .stat__from { color: var(--text-soft); }
.stat--reduced .stat__to   { color: var(--ink); }
.section--dark .stat--reduced .stat__from { color: var(--text-soft); }
/* A reduced stat repaints its "from" soft grey, and that rule sits below the
   tone block, so on a marked stat it was quietly winning: cost per click read
   grey-to-black while click-through read red-to-black, which is the one
   inconsistency the colour system exists to prevent. A marked stat keeps its
   claret whichever way the number moved. */
.stat--claret.stat--reduced .stat__from { color: var(--claret); opacity: 1; }
.section--dark .stat--claret.stat--reduced .stat__from { color: var(--amber); opacity: 1; }
.section--dark .stat__from { color: var(--slate); }

/* Drawn on scroll, once. The bars start at full width for anyone without
   scripting — the `js` class is written by the inline script in <head>, so a
   reader with JavaScript off gets a static chart rather than an empty one. */
.js .stat__track { transform: scaleX(0); transition: transform 0.85s cubic-bezier(0.22, 0.7, 0.2, 1); }
.js .stat.is-in .stat__track { transform: scaleX(1); }

.proof-row {
  display: flex;
  flex-direction: column;
  gap: 1.75rem;
  padding-top: 1.75rem;
  border-top: 1px solid var(--rule);
}
.proof-row + .proof-row { margin-top: clamp(2rem, 4vw, 3rem); }
.section--dark .proof-row { border-top-color: var(--rule-invert); }
.proof-row__summary {
  font-size: var(--t-lede);
  line-height: 1.4;
  color: var(--text);
  text-decoration: none;
  border-bottom: 1px solid var(--marker);
  padding-bottom: 0.2rem;
  max-width: 48ch;
  align-self: flex-start;
}
.proof-row__summary:hover { color: var(--indigo); }
.section--dark .proof-row__summary { color: var(--text-invert); }
.section--dark .proof-row__summary:hover { color: var(--marker); }

.terms { display: grid; gap: clamp(1.5rem, 3vw, 2.5rem); }
.terms--split { grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); }
.terms__pair {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  padding-top: 1rem;
  border-top: 1px solid var(--rule);
}
.section--dark .terms__pair { border-top-color: var(--rule-invert); }
.terms__term {
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--indigo);
}
.terms__def { margin: 0; font-size: var(--t-ui); max-width: 54ch; }
.section--dark .terms__term { color: var(--marker); }

.quote {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  max-width: 50ch;
}
/* Carries .display in the markup for the family, then resets everything else
   that class sets: a pull quote is set in the reading face's italic, which is
   the one place on the site that face gets to perform. */
.quote__text {
  font-family: var(--body);
  font-size: var(--t-lede);
  font-style: italic;
  font-weight: 400;
  font-stretch: normal;
  line-height: 1.45;
  letter-spacing: 0;
  color: var(--ink);
}
/* No quote marks are added here — the copy already carries its own, and a
   pseudo-element would print a second pair. */
.quote__attribution {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.04em;
  color: var(--indigo);
}
.quote__role { color: var(--text-soft); }
.section--dark .quote__text { color: var(--text-invert); }
.section--dark .quote__attribution { color: var(--marker); border-top-color: var(--rule-invert); }
.section--dark .quote__role { color: var(--slate); }

.quote-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: clamp(1.75rem, 3vw, 3rem);
  align-items: start;
}

.layers { display: grid; gap: clamp(2rem, 4vw, 3rem); }

/* A layer is a disclosure. Closed, the four ledes are four claims a reader can
   hold at once — which is the only way "built at the same time" lands. Open,
   the prose is unchanged and it was never removed from the DOM, so a crawler
   and a screen reader still get all of it.

   The grid lives on the summary and on the body separately rather than on the
   <details>, because a details element has exactly two children and the two
   columns have to line up across both. Every child of the body is pinned to
   column two, which leaves column one empty and puts the prose directly under
   the lede it belongs to — subgrid without the subgrid. */
.layer {
  display: block;
  padding-top: clamp(1.5rem, 3vw, 2.25rem);
  border-top: 1px solid var(--rule);
}
.layer > summary {
  display: grid;
  grid-template-columns: minmax(0, 13rem) minmax(0, 1fr);
  gap: 0.75rem clamp(1.5rem, 3vw, 2.5rem);
  align-items: start;
  cursor: pointer;
  list-style: none;
}
.layer > summary::-webkit-details-marker { display: none; }
.layer__cue {
  grid-column: 2;
  justify-self: start;
  width: fit-content;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--marker);
  padding-bottom: 0.3rem;
  background: var(--drawn-mark) left bottom / 100% 4px no-repeat;
}
.layer[open] .layer__cue { color: var(--text-soft); }
.layer[open] .layer__cue::after { content: ' · close'; }
.section--dark .layer__cue { color: var(--amber); }
.section--dark .layer { border-top-color: var(--rule-invert); }
.layer__title {
  font-size: var(--t-card);
  line-height: 1.14;
  color: var(--ink);
}
.section--dark .layer__title { color: var(--text-invert); }
/* Both of these belong in the second column, beside the title, and they have to
   say so. A .layer holds three children in a two-column grid, so auto-placement
   put the title at 1/1, the lede at 1/2 and then wrapped the *body* back to
   2/1 — underneath the title, inside a 13rem column. Two hundred and eight
   pixels of measure, about twenty-eight characters a line, with the lede's
   column left empty beside it. */
.layer__lede { grid-column: 2; }
.layer__body {
  display: grid;
  grid-template-columns: minmax(0, 13rem) minmax(0, 1fr);
  gap: 0.9rem clamp(1.5rem, 3vw, 2.5rem);
  margin-top: 1.1rem;
  font-size: var(--t-ui);
}
.layer__body > * { grid-column: 2; max-width: 60ch; }

.marks { display: grid; gap: 0.6rem; }
.marks li { position: relative; padding-left: 1.4rem; }
.marks li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.68em;
  width: 0.7rem;
  height: 1px;
  background: var(--marker);
}
.marks strong { font-weight: 600; }

/* The fit / not-fit pair. A rule in the margin rather than a tick: the two
   lists are a judgement, and a green check would read as an endorsement. */
.check { display: flex; flex-direction: column; gap: 1.1rem; }
.check__heading { font-size: var(--t-kicker); }
.check__list { display: grid; gap: 0.75rem; }
.check__list li {
  position: relative;
  padding-left: 1.5rem;
  font-size: var(--t-ui);
}
.check__list li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.66em;
  width: 0.8rem;
  height: 2px;
  background: var(--indigo);
}
.check--no .check__list li::before { background: var(--claret); }
.section--dark .check__list li::before { background: var(--marker); }
.section--dark .check--no .check__list li::before { background: var(--slate); }

.jump { padding-block: var(--band-tight); }
.jump-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1.5rem;
  align-items: baseline;
  padding-top: 1.1rem;
  border-top: 1px solid var(--rule);
}
.jump-nav__label { flex-shrink: 0; }
.jump-nav__list { display: flex; flex-wrap: wrap; gap: 0.6rem 1.4rem; }
.jump-nav__list a {
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.04em;
  color: var(--indigo);
  text-decoration: none;
  border-bottom: 1px solid var(--marker);
  padding-bottom: 0.2rem;
}
.jump-nav__list a:hover { color: var(--ink); border-bottom-color: var(--ink); }

.faq { display: grid; }
.faq__item { border-top: 1px solid var(--rule); }
.faq__item:last-child { border-bottom: 1px solid var(--rule); }
.faq__question {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1.5rem;
  padding: 1.35rem 0;
  font-family: var(--display);
  font-weight: 500;
  font-stretch: 105%;
  font-size: var(--t-ui);
  letter-spacing: -0.01em;
  color: var(--ink);
  cursor: pointer;
  list-style: none;
}
.faq__question:hover { color: var(--indigo); }
.faq__question::after {
  content: '+';
  font-family: var(--mono);
  font-size: var(--t-lede);
  line-height: 1;
  color: var(--marker);
  flex-shrink: 0;
}
.faq__item[open] .faq__question::after { content: '−'; }
.faq__answer { padding-bottom: 1.6rem; max-width: 66ch; }
.faq__answer > p + p { margin-top: 0.9rem; }

/* --------------------------------------------------------------------------
   9. Call-to-action band
   -------------------------------------------------------------------------- */

.cta-band { background: var(--ink); color: var(--text-invert); }
.cta-band .display { color: var(--text-invert); }
.cta-band .lede { color: var(--text-invert); }
.cta-band .eyebrow { color: var(--marker); }
.cta-band .section__photo img { opacity: 0.16; mix-blend-mode: luminosity; }

/* The band used to be one 40ch column: label, headline, paragraph, button and
   aside stacked five deep, which made the last thing on every page the tallest
   thing on it. Split in two, the ask sits on the left and what you get for it
   on the right, and the paragraph becomes three lines you can read at a glance
   instead of a block you have to work through. */
.cta-band__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.92fr);
  column-gap: clamp(2rem, 6vw, 5rem);
  row-gap: clamp(1.75rem, 4vw, 2.5rem);
  align-items: center;
}
.cta-band__lead,
.cta-band__offer {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
.cta-band__lead  { gap: var(--flow); }
.cta-band__offer { gap: 1.3rem; }
.cta-band__inner .display { max-width: 15ch; }

.cta-band__points { display: grid; gap: 0.7rem; }
.cta-band__points li {
  position: relative;
  padding-left: 1.5rem;
  font-size: var(--t-ui);
  max-width: 36ch;
}
.cta-band__points li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.34em;
  width: 1.1rem;
  height: 0.85rem;
  background: var(--tick) center / contain no-repeat;
}

@media (max-width: 60rem) {
  .cta-band__inner { grid-template-columns: minmax(0, 1fr); }
  .cta-band__inner .display { max-width: 20ch; }
}
.cta-band__inner .eyebrow::before {
  content: none;
  display: block;
  width: 1.75rem;
  height: 2px;
  margin-bottom: 0.85rem;
  background: var(--marker);
}
.cta-band__action { margin-top: 0.9rem; }
/* The source set these two asides in a handwriting face. Here they are a
   margin note in the mono, which says the same thing without the costume. */
.cta-band__hand {
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--slate);
}

/* --------------------------------------------------------------------------
   10. Prose (legal pages, blog posts)
   -------------------------------------------------------------------------- */

.prose { max-width: 66ch; font-size: var(--t-ui); line-height: 1.7; }
.prose > * + * { margin-top: 1.15rem; }
.prose h2 {
  font-family: var(--display);
  font-weight: 600;
  font-stretch: 112%;
  font-size: var(--t-card);
  line-height: 1.16;
  letter-spacing: -0.018em;
  color: var(--ink);
  margin-top: 2.75rem;
}
.prose h3 {
  font-family: var(--display);
  font-weight: 600;
  font-stretch: 100%;
  font-size: var(--t-ui);
  color: var(--ink);
  margin-top: 2rem;
}
.prose ul, .prose ol { padding-left: 1.3rem; display: grid; gap: 0.5rem; }
.prose ul { list-style: disc; }
.prose ol { list-style: decimal; }
.prose a { color: var(--indigo); text-underline-offset: 0.2em; }
/* Same reasoning as .meta-block: the quote earns its separation from size and
   indent, not from a coloured bar in the margin. */
.prose blockquote {
  margin-block: 1.9rem;
  padding-left: clamp(1rem, 3vw, 2rem);
  font-style: italic;
  font-size: var(--t-kicker);
  line-height: 1.4;
  color: var(--ink);
}
.section--dark .prose blockquote { color: var(--text-invert); }
.prose strong { font-weight: 600; }
.section--dark .prose a { color: var(--marker); }

/* --------------------------------------------------------------------------
   11. Blog
   -------------------------------------------------------------------------- */

.tabs {
  margin-top: clamp(1.5rem, 3vw, 2.25rem);
  margin-bottom: clamp(1.75rem, 3vw, 2.5rem);
  overflow-x: auto;
}
/* One line naming the work, between the tabs and the cards. Set at body size
   and not as a lede: it is a signpost, not the page's argument. */
.blog-index__what {
  margin-bottom: clamp(1.75rem, 3vw, 2.5rem);
  color: var(--text-soft);
  max-width: 62ch;
}
.tabs__list { display: flex; gap: 0.5rem 1.5rem; flex-wrap: wrap; }
.tabs__tab {
  display: inline-block;
  padding-bottom: 0.5rem;
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-soft);
  text-decoration: none;
  border-bottom: 2px solid transparent;
}
.tabs__tab:hover { color: var(--ink); }
.tabs__tab--active { color: var(--ink); border-bottom-color: var(--marker); }

.post-list { display: grid; gap: clamp(1.75rem, 3vw, 2.75rem); }
.post-card {
  display: grid;
  grid-template-columns: minmax(0, 17rem) 1fr;
  gap: clamp(1.25rem, 3vw, 2.5rem);
  align-items: start;
  padding-top: clamp(1.75rem, 3vw, 2.5rem);
  border-top: 1px solid var(--rule);
}
/* Nine posts have no cover photograph yet. The card drops to one column rather
   than leaving an empty 17rem track beside the text, and the excerpt keeps its
   58ch measure so the line length does not change between a card with art and a
   card without. A missing photo should read as a missing photo, not as a bug. */
.post-card--nocover { grid-template-columns: 1fr; }
.post-card__cover { overflow: hidden; background: var(--paper-sink); }
.post-card__cover img {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  transition: transform 0.4s ease;
}
.post-card:hover .post-card__cover img { transform: scale(1.03); }
.post-card__body { display: flex; flex-direction: column; gap: 0.7rem; }
.post-card__title {
  font-family: var(--display);
  font-weight: 600;
  font-stretch: 112%;
  font-size: var(--t-card);
  line-height: 1.14;
  letter-spacing: -0.018em;
  color: var(--ink);
}
.post-card__title a { text-decoration: none; }
.post-card__title a:hover { color: var(--indigo); }
.post-card__excerpt { font-size: var(--t-ui); max-width: 58ch; }
.post-card__meta {
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.04em;
  color: var(--text-soft);
}

.post-header {
  display: grid;
  grid-template-columns: minmax(0, var(--rail)) minmax(0, 1fr);
  column-gap: var(--rail-gap);
  row-gap: var(--flow);
  margin-bottom: clamp(2rem, 4vw, 3rem);
}
.post-header__meta { grid-column: 1; grid-row: 1; }
.post-header__title {
  grid-column: 2;
  grid-row: 1;
  font-size: var(--t-display);
  max-width: 20ch;
}
.post-header .lede { grid-column: 2; grid-row: 2; }
.post-hero { margin-bottom: clamp(2rem, 4vw, 3rem); }

/* The body of a post: the reading column, and a rail of related posts beside
   it. Pattern borrowed from torc.dev, which puts "related articles" in a right
   rail alongside the article rather than in a row underneath it — a reader who
   is still reading can see where to go next without first reaching the end.
   
   The prose keeps its 66ch measure. The rail takes what is left, and below
   64rem there is nothing left to take, so it drops under the article and the
   thumbnails go wider. */
.post-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 16rem);
  column-gap: clamp(2rem, 4vw, 3.5rem);
  align-items: start;
}
.related {
  position: sticky;
  /* The masthead is sticky at 88px. A rail that scrolls under it loses its
     heading first, which is the part that says what the list is. */
  top: calc(88px + 1.5rem);
}
.related__head {
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--indigo);
  padding-bottom: 0.7rem;
  border-bottom: 1px solid var(--rule);
}
.related__list { display: grid; gap: 1.1rem; margin-top: 1.1rem; }
.related__link {
  display: grid;
  grid-template-columns: 5rem minmax(0, 1fr);
  gap: 0.85rem;
  align-items: start;
  text-decoration: none;
  color: var(--text);
}
/* A post with no cover gives its title the whole width rather than an empty
   thumbnail track. Same rule as the cards on the index. */
.related__link:not(:has(.related__thumb)) { grid-template-columns: minmax(0, 1fr); }
.related__thumb {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  background: var(--paper-sink);
  transition: transform 0.4s ease;
}
.related__link:hover .related__thumb { transform: scale(1.04); }
.related__title { font-size: var(--t-small); line-height: 1.35; }
.related__link:hover .related__title { color: var(--indigo); }

@media (max-width: 64rem) {
  .post-body { grid-template-columns: minmax(0, 1fr); row-gap: clamp(2.5rem, 5vw, 4rem); }
  .related { position: static; }
  .related__link { grid-template-columns: 7rem minmax(0, 1fr); }
}
.post-hero img { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; }
/* The route out of the post and into the work it is about. Sits above the
   older/newer pair, because "more of the same reading" is the weaker of the two
   offers and should not be the only one. */
.post-onward { margin-top: clamp(2rem, 4vw, 3rem); }
.post-footer {
  margin-top: clamp(2.5rem, 5vw, 4rem);
  padding-top: 1.75rem;
  border-top: 1px solid var(--rule);
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2rem;
  align-items: baseline;
  justify-content: space-between;
}

/* --------------------------------------------------------------------------
   12. Contact / booking
   -------------------------------------------------------------------------- */

.booking {
  display: grid;
  grid-template-columns: minmax(0, 21rem) 1fr;
  gap: clamp(1.75rem, 4vw, 3.5rem);
  align-items: start;
}
.booking__aside { display: flex; flex-direction: column; gap: var(--flow); }
.booking__aside .display { max-width: 12ch; }
.booking__aside .eyebrow::before {
  content: none;
  display: block;
  width: 1.75rem;
  height: 2px;
  margin-bottom: 0.85rem;
  background: var(--marker);
}
.booking__points { display: grid; gap: 0.75rem; }
.booking__points li {
  position: relative;
  padding-left: 1.5rem;
  font-size: var(--t-ui);
}
.booking__points li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.66em;
  width: 0.8rem;
  height: 2px;
  background: var(--marker);
}
.booking__note {
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.04em;
  color: var(--slate);
}
/* The one sentence that has to survive a skim on this page. Set in the display
   face rather than the mono note style, because a promise typeset as chrome
   reads as chrome. */
.booking__promise {
  margin-top: 1.35rem;
  font-family: var(--display);
  font-weight: 500;
  font-stretch: 108%;
  font-size: var(--t-lede);
  line-height: 1.3;
  color: var(--marker);
  max-width: 32ch;
}
/* Calendly is a third-party iframe; it gets a fixed floor so the page does not
   jump when it finishes loading, and a plain link underneath in case the
   embed is blocked. */
.booking__panel { background: var(--paper-lift); padding: clamp(0.75rem, 2vw, 1.25rem); }
.booking__embed { min-height: 44rem; }
.booking__embed iframe { display: block; width: 100%; height: 44rem; border: 0; }
.booking__fallback { margin-top: 1rem; font-size: var(--t-small); color: var(--text); }
.booking__fallback a { color: var(--indigo); }

/* --------------------------------------------------------------------------
   13. Figures
   -------------------------------------------------------------------------- */

.figure { display: flex; flex-direction: column; gap: 0.9rem; }
.figure img { width: 100%; }
.figure__caption { font-size: var(--t-small); color: var(--text-soft); max-width: 50ch; }
.section--dark .figure__caption { color: var(--slate); }

.portrait { position: sticky; top: calc(var(--masthead) + 1.5rem); }
.portrait img { aspect-ratio: 4 / 5; object-fit: cover; }
.portrait__caption {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding-top: 0.9rem;
  border-top: 2px solid var(--marker);
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.04em;
}
.portrait__name { color: var(--ink); }
.portrait__role { color: var(--text-soft); }
.section--dark .portrait__name { color: var(--text-invert); }
.section--dark .portrait__role { color: var(--slate); }

/* ── The inset photograph ─────────────────────────────────────────────────
   A photograph beside the argument rather than in front of it.

   What this replaced: a 1800px figure centred in the measure with a caption
   under it. That arrives as a thing to be looked at, so the reading stops
   while it is looked at, and it resumes cold. An inset never takes the
   measure. It sits in the rail — the column the section labels already live
   in — and runs off the left edge of the screen, cropped to a tall slot. At
   3:4 over a 16:9 source it shows about a third of the frame: enough to read
   as a room with people in it, not enough to be the subject of the page.

   The crop is named per photograph with `object-position`, for the same
   reason the heroes name theirs — a 3:4 window over a landscape frame lands
   on a wall by default.

   The bleed: max() reaches the viewport edge on a wide screen and falls back
   to the gutter below the shell width, where the gutter already is the edge.
   The caption is padded back in by the same amount so it starts on the rail
   and not off the side of the screen. `--bleed` is read by both. */
.inset-bed {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: minmax(0, var(--rail)) minmax(0, 1fr);
  column-gap: var(--rail-gap);
  /* The photograph is as tall as the argument beside it, rather than a fixed
     3:4 that hangs 200px past the last line and leaves a hole in the corner.
     Cropping harder is free; a void under the text is not. The bounds stop it
     becoming a stripe beside two lines or a tower beside twenty. */
  align-items: stretch;
}
.inset {
  --bleed: max(var(--gutter), (100vw - var(--shell)) / 2 + var(--gutter));
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  margin-inline-start: calc(-1 * var(--bleed));
}
.inset img {
  width: 100%;
  flex: 1 1 auto;
  min-height: 17rem;
  max-height: 34rem;
  object-fit: cover;
  /* Calmed rather than drained. Full greyscale would read as an effect; this
     just stops the photograph competing with the amber and the claret. */
  filter: saturate(0.82) contrast(1.04);
}
.inset__caption {
  padding-inline-start: var(--bleed);
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.05em;
  line-height: 1.5;
  color: var(--text-soft);
}
.section--dark .inset__caption { color: var(--slate); }

.split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  gap: clamp(1.75rem, 4vw, 3.5rem);
  align-items: start;
}
.split--portrait { grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.6fr); }

.stack { display: flex; flex-direction: column; gap: var(--flow); align-items: flex-start; }
.stack--wide > p { max-width: 60ch; }
.stack--lines > p {
  font-family: var(--display);
  font-weight: 500;
  font-stretch: 108%;
  font-size: var(--t-kicker);
  line-height: 1.25;
  letter-spacing: -0.014em;
}

/* --------------------------------------------------------------------------
   14. Colophon
   -------------------------------------------------------------------------- */

.colophon {
  padding-top: var(--band-tight);
  background: var(--ink);
  color: var(--text-invert);
  font-size: var(--t-meta);
}
.colophon__inner {
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 2fr);
  gap: clamp(2rem, 5vw, 4rem);
  padding-bottom: clamp(2rem, 5vw, 3.5rem);
}
.colophon__identity { display: flex; flex-direction: column; gap: 1.1rem; align-items: flex-start; }
.colophon__logo { width: 10rem; height: auto; }
.colophon__blurb { font-family: var(--body); font-size: var(--t-ui); line-height: 1.5; max-width: 32ch; }
.colophon__where { font-size: var(--t-micro); letter-spacing: 0.04em; color: var(--slate); }

.colophon__columns {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: clamp(1.75rem, 4vw, 3rem);
}
.colophon__column { display: flex; flex-direction: column; gap: 1rem; }
.colophon__column .eyebrow { color: var(--marker); }
.colophon__list { display: grid; gap: 0.55rem; }
.colophon__list li {
  position: relative;
  padding-left: 1.1rem;
  font-family: var(--body);
  font-size: var(--t-small);
  color: var(--text-invert);
}
.colophon__list li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.7em;
  width: 0.5rem;
  height: 1px;
  background: var(--slate);
}

.colophon__base {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 2rem;
  align-items: baseline;
  justify-content: space-between;
  padding-top: 1.2rem;
  padding-bottom: 1.75rem;
  border-top: 1px solid var(--rule-invert);
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 92%;
  letter-spacing: 0.05em;
  color: var(--slate);
}
.colophon__links { display: flex; flex-wrap: wrap; gap: 0.75rem 1.5rem; }
.colophon__links a { text-decoration: none; }
.colophon__links a:hover { color: var(--marker); }

/* --------------------------------------------------------------------------
   15. Reveal

   One gesture, used sparingly. The argument against motion on a page like this
   is that a generated site fades every element in identically; the argument
   against no motion is a dead page. So there is exactly one reveal — a short
   rise and fade — and it is spent only on elements whose arrival is part of
   the reading: the phases, and the figures they land on.

   Everything starts visible. The `js` class is written by the inline script in
   <head>, so the hidden state only ever exists where scripting can undo it.
   -------------------------------------------------------------------------- */

.js [data-reveal] {
  opacity: 0;
  transform: translateY(0.85rem);
  transition: opacity var(--dur-mid) var(--ease),
              transform var(--dur-mid) var(--ease);
}
.js [data-reveal].is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .js [data-reveal] { opacity: 1; transform: none; }
}

/* --------------------------------------------------------------------------
   16. The phase reel

   The execution model was a four-row chart: name, duration, bar, description
   and output, stacked four deep, all of it arriving at once. The information
   was right and the delivery was a wall. It is now two columns — the claim
   stays anchored on the left while the four phases move past it on the right,
   so the reader holds "four phases, one motion" in view the entire time they
   are reading what the four phases are. The ticks track which one is up.

   The twelve-week frame survives: each phase still draws its span against the
   same scale, so the overlap between build, run and hand off is still a fact
   you can see rather than a claim you have to take.
   -------------------------------------------------------------------------- */

.phases__inner {
  grid-template-columns: minmax(0, 19rem) minmax(0, 1fr);
  column-gap: clamp(2rem, 6vw, 5.5rem);
}
.phases__inner > * { grid-column: auto; }

.phases__anchor {
  position: sticky;
  top: calc(var(--masthead) + clamp(1.5rem, 6vh, 4rem));
  align-self: start;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1rem;
}
/* Set a step below the page's other headings so each of the two sentences
   holds one line in the anchor column. Broken across four lines it stops
   reading as two statements and starts reading as a stack of words. */
.phases__title {
  font-size: clamp(1.75rem, 1.15rem + 1.9vw, 2.375rem);
  max-width: 14ch;
}
.phases__lede {
  font-size: var(--t-body);
  line-height: 1.5;
  color: var(--text-soft);
  max-width: 32ch;
}

.phases__ticks {
  display: grid;
  width: 100%;
  max-width: 15rem;
  margin-top: 0.5rem;
  border-top: 1px solid var(--rule);
}

/* The qualification on the whole model. It keeps its place under the phase it
   qualifies, but it rides inside the pinned stage rather than below the reel,
   so it is on screen the entire time instead of arriving once the fourth phase
   has finished — where it read as a footnote on hand off rather than a note on
   all four. Fixed position under the frame block, so it does not move as the
   phases swap above it. */
.phases__note { margin-top: clamp(1.5rem, 4vh, 2.75rem); max-width: 44ch; }
.phases__tick {
  position: relative;
  display: grid;
  grid-template-columns: 2.1rem minmax(0, 1fr);
  column-gap: 0.7rem;
  align-items: baseline;
  padding-block: 0.5rem;
  border-bottom: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 92%;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-soft);
  transition: color var(--dur-mid) var(--ease);
}
/* The progress mark is drawn under the tick that is up, and it slides rather
   than blinks — the reader should feel the reel advance, not see a highlight
   move. */
.phases__tick::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -1px;
  width: 100%;
  height: 1px;
  background: var(--marker);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-mid) var(--ease);
}
.phases__tick.is-active { color: var(--ink); }
.phases__tick.is-active::after { transform: scaleX(1); }
.phases__tick-num { color: var(--marker); }

/* The axis and the span belong together: the scale across the top, the bar
   immediately under it, and the phase reading below both. One bar, not four —
   it slides to the weeks the current phase occupies, so the same instrument
   is re-read four times rather than redrawn four times. */
.phases__scale {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  padding-bottom: 0.45rem;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 92%;
  letter-spacing: 0.06em;
  color: var(--text-soft);
}
.phases__scale span { grid-column: var(--at) / span 1; white-space: nowrap; }
.phases__scale span:last-child { grid-column: 12 / span 1; justify-self: end; }

.phases__rail {
  position: relative;
  height: 6px;
  background: var(--marker-quiet);
}
.phases__bar {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc((var(--from) - 1) / 12 * 100%);
  right: calc((12 - var(--to) + 1) / 12 * 100%);
  background: var(--marker);
  transition: left var(--dur-slow) var(--ease), right var(--dur-slow) var(--ease);
}
/* Run has no promised end, so its span dissolves instead of stopping. */
.phases__rail[data-span="open"] .phases__bar {
  background: linear-gradient(90deg, var(--marker) 55%, transparent 100%);
}
/* Hand off is a thread under the other three rather than a block of time. */
.phases__rail[data-span="ambient"] .phases__bar {
  background: repeating-linear-gradient(90deg, var(--slate) 0 6px, transparent 6px 11px);
}

/* Without scripting there is no reel: the four phases simply read in order,
   and the shared axis is withheld because a single bar cannot describe four
   spans at once. Everything below is scoped to `.js` for that reason. */
.phases__scale,
.phases__rail,
.phases__steps { display: none; }

.phases__frames { display: grid; gap: clamp(2rem, 6vh, 3.5rem); }
.phase { display: grid; gap: 0.5rem; }

.phase__index {
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--marker);
}
.phase__name {
  font-size: var(--t-index);
  line-height: 1.02;
  letter-spacing: -0.026em;
}
.phase__when {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.phase__what { font-size: var(--t-ui); max-width: 52ch; }
.phase__out {
  margin-top: 0.35rem;
  font-size: var(--t-small);
  color: var(--text-soft);
  max-width: 52ch;
}
.phase__out-label {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.08em;
  color: var(--indigo);
  margin-right: 0.5rem;
}

/* ── With scripting: the reel ──────────────────────────────────────────────
   The stage pins under the masthead and holds the axis, the bar and one
   phase. The four steps behind it carry the scroll height and are the only
   thing the observer watches, so the swap is driven by position on the page
   rather than by a timer. One phase is on stage at a time; the bar slides to
   the weeks that phase occupies. */

.js .phases__scale { display: grid; }
.js .phases__rail  { display: block; }

/* 40vh of scroll per phase. At 80vh the reel felt like it had stalled between
   phases — the reader was scrolling through an unchanging screen for most of
   each step. Short enough to feel responsive, long enough that the swap does
   not fire on a flick. */
.js .phases__scroll { position: relative; height: 160vh; }
.js .phases__stage {
  position: sticky;
  top: calc(var(--masthead) + clamp(1.25rem, 5vh, 3rem));
}
.js .phases__steps { display: block; position: absolute; inset: 0; pointer-events: none; }
.js .phases__step  { height: 25%; }

.js .phases__frames {
  display: block;
  position: relative;
  min-height: 19rem;
  margin-top: clamp(1.5rem, 4vh, 2.5rem);
}
/* On stage a phase is the only thing on the screen, so it is set to hold one:
   the name at headline size, the description at reading-lede size. In the
   list fallback the same phases stay at list weight. */
.js .phases__frames > .phase .phase__name { font-size: var(--t-display); }
.js .phases__frames > .phase .phase__what {
  font-size: var(--t-lede);
  line-height: 1.5;
  max-width: 44ch;
  margin-top: 0.35rem;
}
.js .phases__frames > .phase .phase__out { max-width: 44ch; }
.js .phases__frames > .phase {
  position: absolute;
  inset: 0 0 auto 0;
  opacity: 0;
  transform: translateY(0.7rem);
  transition: opacity var(--dur-mid) var(--ease), transform var(--dur-mid) var(--ease);
}
.js .phases__frames > .phase.is-active { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .js .phases__frames > .phase { transform: none; }
}

/* The reel opens on a tall band because the anchor needs room to establish
   itself, but it closes on a short one: the stage has already left a screen of
   empty paper under the last phase, so a full tall band underneath it reads as
   a hole rather than as breathing room. */
.phases { padding-bottom: var(--band-tight); }

@media (max-width: 60rem) {
  .phases__inner { grid-template-columns: minmax(0, 1fr); row-gap: 2rem; }
  /* Sticky costs more than it returns on a short screen: the anchor would eat
     half the viewport the phases are meant to scroll through, and a pinned
     stage inside a pinned column leaves nowhere to read. The reel unwinds
     back into a list, and the duration survives in words on each phase. */
  .phases__anchor { position: static; }
  .phases__ticks { display: none; }
  .js .phases__scale,
  .js .phases__rail,
  .js .phases__steps { display: none; }
  .js .phases__scroll { height: auto; }
  .js .phases__stage { position: static; }
  .js .phases__frames {
    display: grid;
    gap: clamp(1.75rem, 5vh, 2.75rem);
    min-height: 0;
    margin-top: 0;
  }
  .js .phases__frames > .phase { position: static; opacity: 1; transform: none; }
}

/* --------------------------------------------------------------------------
   16b. Ink and scenery

   Two things were grafted on after the site was already working, and both
   exist to answer one specific complaint: the page looked drawn by a machine.

   It was, and the evidence was measurable. Nine sections on the home page
   squared to the same left edge — 176px, every one, down 6,577 pixels. Eight
   eyebrows in the same mono caps at the same size. A hundred and twenty
   elements with a zero border radius and not one curve between them. Nothing
   overlapped, nothing bled, nothing sat off-axis. Perfect regularity is not a
   neutral choice; at that dose it is a signature.

   The fix is not decoration. It is putting a hand back into three places the
   machine had taken over: the lines, the edges, and the empty space.

   1. INK. Every rule, tick, ring and underline that used to be a border is now
      a stroked bezier pushed through one displacement filter. A clean curve
      goes in and a line with a slight tremor comes out, and the tremor is the
      entire difference between "border-bottom: 1px" and "someone drew this".
      One filter, two seeds, no images, no requests.

   2. SCENERY. Soft grounds behind a few sections — never behind reading copy,
      never more than two per page. They are tuned for the cool paper this site
      actually has rather than the cream of the prototype: indigo at 5% and
      amber at 8% read as a wash on #edeef2, where anything stronger reads as
      a bug.

   3. LEAN. Three sections per page are allowed to break the shared edge. Not
      randomly — the ones whose content is an aside rather than an argument.

   What deliberately did NOT change: the rail, the type, the palette, the
   contrast system, the density. The complaint was that the site looked
   machine-made, not that it was wrong.
   -------------------------------------------------------------------------- */

.ink-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

.ink {
  fill: none;
  stroke: var(--marker);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.ink--quiet { stroke: var(--marker-quiet); }
.ink--amber { stroke: var(--amber); }
.ink--thin  { stroke-width: 1.5; }

/* Drawn strokes that arrive rather than sit there. pathLength normalises the
   dash maths to 1, so a long label and a short one trace in the same time. */
.js .ink--draw { stroke-dasharray: 1 2; stroke-dashoffset: 1; }
.js .is-in .ink--draw,
.js .ink--draw.is-in { stroke-dashoffset: 0; transition: stroke-dashoffset var(--dur-slow) var(--ease); }

/* ---- The traced ring -----------------------------------------------------
   The one interaction worth having on a button: a circle that draws itself
   round the control on hover and crosses its own tail at the end, the way a
   hand circling something on paper does. It costs one path and one transition,
   and it never moves the button — a control that jumps under the cursor is a
   control you miss. */
.button { position: relative; }
.button__ring {
  position: absolute;
  left: -20px; top: -16px;
  width: calc(100% + 40px);
  height: calc(100% + 32px);
  overflow: visible;
  pointer-events: none;
}
.button__ring path {
  fill: none;
  stroke: var(--ring);
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-dasharray: 1 2;
  stroke-dashoffset: 1;
  transition: stroke-dashoffset 0.6s var(--ease);
}
.button:hover .button__ring path,
.button:focus-visible .button__ring path { stroke-dashoffset: 0; }


/* ---- Drawn link underline ------------------------------------------------
   Replaces border-bottom: 1px solid. Same affordance, drawn. */
.arrow-link { position: relative; border-bottom: 0 !important; }
.arrow-link__rule {
  position: absolute;
  left: 0; bottom: -1px;
  width: 100%; height: 8px;
  overflow: visible;
  pointer-events: none;
}
.arrow-link__rule path {
  fill: none;
  stroke: var(--marker);
  stroke-width: 1.8;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
  transition: stroke var(--dur-fast) var(--ease);
}
.arrow-link:hover .arrow-link__rule path { stroke: var(--ink); }
.section--dark .arrow-link:hover .arrow-link__rule path { stroke: var(--marker); }
.arrow-link__arrow { transition: transform var(--dur-fast) var(--ease); display: inline-block; }
.arrow-link:hover .arrow-link__arrow { transform: translateX(3px); }

/* ---- Drawn ticks and circles --------------------------------------------
   A tick that was a 2px rectangle, and an index that was a number floating
   over nothing. Both are backgrounds rather than markup, so they land on every
   list and every index on all 67 pages without touching a page module. */
:root {
  --tick: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 16'><path d='M2,7.6 C4.8,10 6.8,12 8.6,14.4 C12,8.8 16.2,3.8 20,1.6' fill='none' stroke='%2325318d' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/></svg>");
  --tick-amber: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 16'><path d='M2,7.6 C4.8,10 6.8,12 8.6,14.4 C12,8.8 16.2,3.8 20,1.6' fill='none' stroke='%23ffb400' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/></svg>");
  --cross: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><path d='M3.4,3 C7,7.2 12.4,12.6 16.8,17.2 M16.6,3.2 C12.6,7.4 7.4,12.8 3.2,17' fill='none' stroke='%238c2f1e' stroke-width='2.1' stroke-linecap='round'/></svg>");
  /* The same cross for the ink grounds. Claret measures 2.4:1 on navy,
     which is a mark you have to hunt for. */
  --cross-invert: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><path d='M3.4,3 C7,7.2 12.4,12.6 16.8,17.2 M16.6,3.2 C12.6,7.4 7.4,12.8 3.2,17' fill='none' stroke='%238d90a6' stroke-width='2.1' stroke-linecap='round'/></svg>");
  --circled: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 44' preserveAspectRatio='none'><path d='M30,3 C48,3 60,9 60,21 C60,34 46,41 29,41 C12,41 3,34 3,22 C3,10 14,3 32,3 C39,3 45,4 49,6' fill='none' stroke='%2325318d' stroke-width='2.6' stroke-linecap='round'/></svg>");
  --circled-amber: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 44' preserveAspectRatio='none'><path d='M30,3 C48,3 60,9 60,21 C60,34 46,41 29,41 C12,41 3,34 3,22 C3,10 14,3 32,3 C39,3 45,4 49,6' fill='none' stroke='%23ffb400' stroke-width='2.6' stroke-linecap='round'/></svg>");
}

.cta-band .cta-band__points li::before { background-image: var(--tick-amber); }

.check__list li::before {
  content: '';
  background: var(--tick) center / contain no-repeat !important;
  width: 1.15rem !important;
  height: 0.9rem !important;
  top: 0.3em !important;
  border-radius: 0 !important;
}
.check--no .check__list li::before { background-image: var(--cross) !important; height: 1.05rem !important; }
.section--dark .check__list li::before { background-image: var(--tick-amber) !important; }
/* Equal specificity to the rule above and both carry !important, so on an ink
   band a "not this" list was quietly drawing amber ticks: four things marked
   with the approval mark under a heading that says the opposite. Claret is also
   the wrong pen here, at 2.4:1 on navy. This is the same cross in the pale
   pen the ink grounds use for everything else. */
.section--dark .check--no .check__list li::before { background-image: var(--cross-invert) !important; }

/* Circled numbers. Only the numeric indices — circling a worded label would
   read as a mistake, and the chain and the phases are the two places on the
   site where somebody is counting. */
.chain__index,
.phase__index,
.phases__tick-num {
  /* justify-self, not inline-block: these are grid items, and a grid item's
     display is blockified, so inline-block silently became block and the
     circle stretched to a 200px ellipse round a two-character number. */
  justify-self: start;
  display: inline-block;
  width: fit-content;
  background: var(--circled) center / 100% 100% no-repeat;
  padding: 0.34em 0.68em 0.4em;
}
.section--dark .chain__index,
.section--dark .phase__index,
.hero .phase__index { background-image: var(--circled-amber); }

/* Two circles were marking the same link: the node on the thread, and the
   number circled beside it. On a wide screen the node is the one that means
   something — it is a stop on a line, and the line is the argument — so the
   number goes plain there.

   Below 60rem the thread is hidden, because a wandering horizontal line has
   nowhere to go once the five links stack. That left the nodes hanging off
   nothing: a stray ring beside a circled number, which is what looked out of
   place. So on a phone the node goes and the circle returns to the number,
   where it is the only mark. One circle per link at every width. */
.chain__index { background-image: none; padding-inline: 0; }
@media (max-width: 60rem) {
  .chain__link::before { content: none; }
  .chain__index { background-image: var(--circled); padding-inline: 0.68em; }
}

/* ---- Circled index -------------------------------------------------------
   The numbers on the phases, the chain and the numbered cards were set in mono
   over nothing. Circled by hand they stop being a data label and start being
   someone counting. */
.index-mark {
  position: absolute;
  left: -0.75rem; top: -0.6rem;
  width: calc(100% + 1.5rem);
  height: calc(100% + 1.2rem);
  overflow: visible;
  pointer-events: none;
}
.index-mark path { fill: none; stroke: var(--marker); stroke-width: 1.6; stroke-linecap: round; vector-effect: non-scaling-stroke; }

/* ---- The hero arrow ------------------------------------------------------
   Drawn once on arrival rather than on scroll: the hero is already on screen,
   so an IntersectionObserver would fire before the reader had focused on
   anything and the gesture would be over before it was seen. */
.hero__cta { position: relative; display: inline-block; }
.hero__arrow {
  position: absolute;
  left: -132px; top: -30px;
  width: 120px; height: 90px;
  overflow: visible;
  pointer-events: none;
}
.hero__arrow path {
  fill: none;
  stroke: var(--marker);
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.js .hero__arrow path { stroke-dasharray: 1 2; stroke-dashoffset: 1; animation: arrow-draw 1s var(--ease) 0.55s forwards; }
@keyframes arrow-draw { to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) {
  .js .hero__arrow path { animation: none; stroke-dashoffset: 0; }
}

/* The drawn arrow lives 132px to the left of the call to action, in the
   gutter. Below about 1360px there is no gutter left to live in: the shell hits
   the viewport edge, the button slides left with it, and the arrow goes
   negative. Measured at 1100px it started at x=-77, so two thirds of it was
   off the screen, and at 1280 (an ordinary laptop) it lost 36px off its tail.
   Half an arrow reads as a rendering fault, which is exactly what it was.

   It used to hide at 60rem, which only covered phones and tablets and left a
   400px band of real laptop widths drawing a clipped one. 86rem is the width
   at which the whole arrow still clears the edge. */
@media (max-width: 86rem) {
  .hero__arrow { display: none; }
}

/* ---- Scenery -------------------------------------------------------------
   Soft grounds and one drawn contour, sat behind a section. `aria-hidden` and
   `pointer-events: none` throughout: none of this is content, and none of it
   is allowed to eat a click.

   The blobs breathe on a 26-second keyframe and drift against the scroll at
   their own rates. --sy is written once per frame for the whole document, so
   six blobs cost one listener and one style write rather than six of each. */
.scenery { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 0; }
.scenery__stage {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  width: min(100%, calc(var(--shell) + var(--gutter) * 2));
}
.section, .hero, .cta-band { isolation: isolate; }
.section__inner, .hero__inner, .cta-band__inner { position: relative; z-index: 2; }

@keyframes scenery-breathe {
  0%, 100% { border-radius: 57% 43% 48% 52% / 47% 52% 48% 53%; }
  33%      { border-radius: 44% 56% 60% 40% / 56% 44% 56% 44%; }
  66%      { border-radius: 52% 48% 39% 61% / 40% 59% 41% 60%; }
}

.scenery__blob {
  position: absolute;
  border-radius: 57% 43% 48% 52% / 47% 52% 48% 53%;
  animation: scenery-breathe 26s var(--ease) infinite;
  /* The grounds were indigo at 5% — the same hue as the paper, the ink and
     every rule on the page — which is why they read as grey haze rather than
     as colour. Claret is the site's third colour and the only warm one in the
     palette: against a cool blue-grey paper it sits close to indigo's
     complement, so it separates from the ground instead of dissolving into it,
     and it costs no seventh hue.

     Ten per cent is the ceiling on this paper. Past that it stops being a
     ground and starts being a shape the reader has to account for. */
  background: rgba(140, 47, 30, 0.075);
  transform: translate3d(0, calc(var(--sy, 0) * var(--rate, 0.02) * -1px), 0);
}
.scenery__blob--amber { background: rgba(255, 180, 0, 0.1); }
/* Alternating, so a long page reads warm red then warm gold rather than the
   same wash repeated down the whole scroll. */
.section:nth-of-type(even) .scenery__blob:not(.scenery__blob--amber) {
  background: rgba(255, 180, 0, 0.085);
}
.section--dark .scenery__blob,
.hero .scenery__blob,
.cta-band .scenery__blob { background: rgba(255, 180, 0, 0.07); }
.section--dark .scenery__blob--amber { background: rgba(238, 240, 246, 0.05); }

/* A drawn outline, not a filled shape. This is the one that reads as a mark on
   paper rather than as a gradient, so it does the most work of anything here. */
.scenery__contour {
  position: absolute;
  overflow: visible;
  transform: translate3d(0, calc(var(--sy, 0) * var(--rate, 0.015) * -1px), 0);
}
/* The drawn objects carry more weight than the washes: an outline that is
   meant to read as a thing has to be legible as one. Claret, like every other
   ground mark — the muted indigo was the problem, not the opacity. */
.scenery__shape path { stroke: rgba(140, 47, 30, 0.34); }

/* Every background mark renders at the same weight, whatever its viewBox.
   They did not: the objects are authored on a 70-unit grid and the contours on
   a 200-unit one, so the same `stroke-width: 1.6` came out at 5.5px on one and
   1.9px on the other — which is why the contour looked like a different, finer
   pen. Non-scaling-stroke makes the number the rendered pixel width, and it is
   safe here because neither of these draws itself; the pathLength trap only
   applies to the dash-animated strokes. */
.scenery__contour path,
.scenery__shape path {
  vector-effect: non-scaling-stroke;
  stroke-width: 4;
  stroke-linejoin: round;
}
.section--dark .scenery__shape path,
.cta-band .scenery__shape path { stroke: rgba(255, 180, 0, 0.26); }
.scenery__contour path {
  fill: none;
  stroke: rgba(140, 47, 30, 0.26);
  stroke-linecap: round;
}
.section--dark .scenery__contour path,
.hero .scenery__contour path,
.cta-band .scenery__contour path { stroke: rgba(255, 180, 0, 0.22); }

@media (prefers-reduced-motion: reduce) {
  .scenery__blob { animation: none; transform: none; }
  .scenery__contour { transform: none; }
}

/* ---- Lean ----------------------------------------------------------------
   The edge break. Nine sections on one left edge is the loudest machine tell
   on the page, and it is also the cheapest to fix: three of them step out of
   line, and the column stops being a column. The values are small on purpose —
   this is a page that got looser, not a page that fell over. */
/* `left`, not `translateX`. Visually identical — a relatively positioned
   element offsets without disturbing layout, exactly like a transform — but a
   transform creates a containing block for `position: fixed` descendants, and
   `.section__inner` is an ancestor of the nine box. With translateX the map's
   lightbox at `inset: 0` sized itself to the shell instead of the viewport:
   a full-screen overlay that covered 1,215 of 1,440 pixels and left the page
   showing round three of its edges. `.section__inner` is already
   `position: relative` for the scenery z-index, so this costs nothing. */
.section--lean  > .section__inner { left: clamp(0px, 2.2vw, 2.25rem); }
.section--lean2 > .section__inner { left: clamp(0px, 4vw, 4rem); }
.section--tuck  > .section__inner { left: clamp(-1.75rem, -2vw, 0px); }


/* ---- The drawn hairline --------------------------------------------------
   Every editorial divider on this site was `border-top: 1px solid var(--rule)`
   — the card stack, the strata, the numbered items, the proof rows, the FAQ,
   the terms, the receipts. Two dozen selectors, one perfectly straight line,
   repeated maybe forty times down a page. That is the machine tell that
   survived the audit, because a straight line is invisible as a decision until
   you have seen forty of them.

   Converting them one at a time would have meant forty markup changes. This
   does it in one: the same wobbled stroke as an inline SVG background, sized
   to the element's width, with the border zeroed out. Nothing in the markup
   moves, and every divider on the site is now drawn.

   Two variants because a data URI cannot read a custom property, and a rule on
   the ink ground has to be lighter than a rule on paper. */
:root {
  --drawn: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='400' height='4' preserveAspectRatio='none' viewBox='0 0 400 4'><path d='M1,2.3 C60,1 118,3.2 180,2 C242,0.8 300,3 399,1.8' fill='none' stroke='%23cbcfdd' stroke-width='1.4' stroke-linecap='round'/></svg>");
  /* The heavier structural mark — the 3px indigo rule over each part of the
     spread and over a term list. Drawn at the same weight it was ruled at. */
  --drawn-mark: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='400' height='6' preserveAspectRatio='none' viewBox='0 0 400 6'><path d='M1.5,3.4 C58,1.6 116,4.6 178,3 C240,1.4 298,4.4 398,2.6' fill='none' stroke='%2325318d' stroke-width='2.8' stroke-linecap='round'/></svg>");
  --drawn-mark-amber: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='400' height='6' preserveAspectRatio='none' viewBox='0 0 400 6'><path d='M1.5,3.4 C58,1.6 116,4.6 178,3 C240,1.4 298,4.4 398,2.6' fill='none' stroke='%23ffb400' stroke-width='2.8' stroke-linecap='round'/></svg>");
  --drawn-invert: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='400' height='4' preserveAspectRatio='none' viewBox='0 0 400 4'><path d='M1,2.3 C60,1 118,3.2 180,2 C242,0.8 300,3 399,1.8' fill='none' stroke='rgba(124,134,174,0.42)' stroke-width='1.4' stroke-linecap='round'/></svg>");
}

.card,
.strata__layer,
.numbered__item,
.proof-row,
.faq__item,
.terms__pair,
.chain__link,
.chain__fail,
.spread__part,
.receipt,
.matrix__row,
.layer,
.quote__attribution,
.jump-nav,
.post-card,
.post-footer,
.phases__ticks {
  border-top-color: transparent !important;
  background-image: var(--drawn);
  background-repeat: no-repeat;
  background-position: left top;
  background-size: 100% 4px;
}
/* The last item in a stack carries a closing rule on its bottom edge. Two
   backgrounds, one per edge. */
.cards > .card:last-child,
.strata__layer:last-of-type,
.faq__item:last-child {
  border-bottom-color: transparent !important;
  background-image: var(--drawn), var(--drawn);
  background-position: left top, left bottom;
  background-size: 100% 4px, 100% 4px;
}
.masthead,
.colophon__base,
.section--dark .card,
.section--dark .strata__layer,
.section--dark .quote__attribution,
.section--dark .layer,
.section--dark .numbered__item,
.section--dark .proof-row,
.section--dark .faq__item,
.section--dark .terms__pair,
.section--dark .chain__link,
.section--dark .spread__part,
.section--dark .receipt,
.cta-band .terms__pair {
  background-image: var(--drawn-invert);
}
/* These two carry the rule on their bottom edge, not their top. */
.matrix__mine, .ninebox__grid, .shot, .tabs__tab, .phases__tick {
  border-color: transparent !important;
}
.matrix__mine, .tabs__tab, .phases__tick {
  background-image: var(--drawn);
  background-repeat: no-repeat;
  background-position: left bottom;
  background-size: 100% 4px;
}
/* These two are boxes rather than rules, so they take a drawn outline instead
   of a drawn edge. */
.ninebox__grid, .shot {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 300' preserveAspectRatio='none'><path d='M3,6 C120,2 280,3 396,5 C398,100 397,200 396,294 C280,297 120,296 4,294 C2,200 2,100 3,6 Z' fill='none' stroke='%23cbcfdd' stroke-width='1.6' stroke-linejoin='round'/></svg>");
  background-repeat: no-repeat;
  background-size: 100% 100%;
}
.masthead, .jump-nav__list a, .proof-row__summary {
  border-bottom-color: transparent !important;
  background-repeat: no-repeat;
  background-position: left bottom;
  background-size: 100% 4px;
}
.colophon__base {
  border-top-color: transparent !important;
  background-repeat: no-repeat;
  background-position: left top;
  background-size: 100% 4px;
}
.masthead { background-image: var(--drawn-invert); }
.jump-nav__list a, .proof-row__summary { background-image: var(--drawn-mark); }
.section--dark .proof-row__summary { background-image: var(--drawn-mark-amber); }
.colophon__base { background-image: var(--drawn-invert); }
/* Panels that carry their own fill keep it, and take the rule on top of it. */
.spread__part, .card { background-color: transparent; }

.spread__link,
.portrait__caption {
  border-top-color: transparent !important;
  background-image: var(--drawn-mark);
  background-repeat: no-repeat;
  background-position: left top;
  background-size: 100% 6px;
}
.section--dark .spread__link,
.hero .portrait__caption,
.section--dark .portrait__caption { background-image: var(--drawn-mark-amber); }

/* ---- Hand-cut corners ----------------------------------------------------
   Only the panels that are raised off the ground, and never the same radius
   twice — four unequal corners is what something cut out by hand looks like.
   The editorial rectangles that carry reading copy stay square, because the
   point was to stop the page being 120 identical boxes, not to round it. */
.receipt,
.terms--split .terms__pair,
.ninebox__cell,
.shot {
  border-radius: 14px 7px 16px 9px / 9px 15px 8px 17px;
}
.card, .spread__part {
  border-radius: 10px 18px 8px 15px / 15px 9px 17px 10px;
}

@media (max-width: 60rem) {
  .section__inner,
  .section-head,
  .numbered__item,
  .post-header,
  .logo-strip__inner { grid-template-columns: minmax(0, 1fr); }
  .section__inner > *,
  .section-head .eyebrow,
  .section-head .display,
  .section-head .lede,
  .post-header__meta,
  .post-header__title,
  .post-header .lede,
  .logo-strip__note { grid-column: 1; }
  .section-head .display,
  .post-header__title { grid-row: auto; }
  .section-head .lede,
  .post-header .lede { grid-row: auto; }
  .booking { grid-template-columns: 1fr; }
  .colophon__inner { grid-template-columns: 1fr; }
  .post-card { grid-template-columns: 1fr; }
  .post-card__cover { max-width: 22rem; }
  .split--portrait { grid-template-columns: 1fr; }
  .portrait { position: static; max-width: 22rem; }
  /* One column, so the two children pinned to column 2 have to be let go — an
     explicit grid-column on a one-column grid does not clamp, it invents a
     second column and puts them in it. */
  .layer > summary, .layer__body { grid-template-columns: minmax(0, 1fr); }
  .layer__lede, .layer__cue, .layer__body > * { grid-column: 1; }
  .section-head .display { max-width: 22ch; }
  /* The lean is a break in a shared column, and below this width there is no
     shared column left to break — the rail has already collapsed. Kept on, it
     bought nothing and pushed the page 16px sideways on a 390px screen. */
  .section--lean > .section__inner,
  .section--lean2 > .section__inner,
  .section--tuck > .section__inner { left: 0; }
  /* Nothing to lean into once the call to action is flush left. */
  /* moved: see the 86rem rule below */

  /* There is no side on a phone. The inset keeps its job by getting shorter
     rather than wider: edge to edge at 21:9 it reads as a band between two
     blocks of text. At 3:4 it would be 520px tall on a 390px screen, which is
     the full-width focal photograph this component exists to avoid. */
  .inset-bed { grid-template-columns: minmax(0, 1fr); row-gap: clamp(1.25rem, 4vw, 2rem); }
  .inset { margin-inline: calc(-1 * var(--bleed)); }
  .inset img { flex: none; min-height: 0; max-height: none; aspect-ratio: 21 / 9; }
  .inset__caption { padding-inline: var(--bleed); }
}

@media (max-width: 48rem) {
  /* The masthead was 145px on a 390px screen — 17% of the viewport, held
     there permanently by position:sticky — because five nav items at the
     desktop gap wrap onto two rows below the logo. A smaller mark and a
     tighter gap fit the whole menu on one row under the logo, and the link
     padding goes up rather than down: every nav target now clears the 44px
     minimum it was failing at 28px. Shorter masthead, bigger targets. */
  .masthead__inner { position: relative; flex-wrap: wrap; gap: 0; padding-block: 0.4rem; min-height: 0; }
  .masthead__brand img { width: 6.25rem; }
  .nav { width: 100%; }
  .nav__list { flex-wrap: wrap; gap: 0 0.95rem; justify-content: flex-start; }
  /* The call-to-action moves up beside the mark, which is both where a thumb
     expects it and what leaves the remaining four items room for a single
     row. Five items across a 390px screen is what forced the second row. */
  .nav__item:last-child {
    position: absolute;
    top: 0.4rem;
    right: var(--gutter);
  }
  .nav__link { padding-block: 0.8rem; }
  /* Everything a thumb has to hit gets the same 44px floor. The colophon
     links were the worst offenders at 15px — a row of five 15px targets set
     1.5rem apart is a mis-tap waiting to happen. */
  .nav__sublink { padding-block: 0.85rem; }
  .arrow-link { display: inline-block; padding-block: 0.7rem; }
  .colophon__links { gap: 0 1.5rem; }
  .colophon__links a { display: inline-block; padding-block: 0.9rem; }
  /* The FAQ jump links were 17px targets set in a wrapping row — the single
     worst cluster on the site for a thumb. */
  .jump-nav__list { gap: 0 1.4rem; }
  .jump-nav__list a { padding-block: 0.95rem; }
  .skip { padding-block: 0.85rem; }
  /* The case-study grids ask for two columns by name, and the 14rem floor in
     .stats then holds them there: two 224px tracks plus the gap need 476px of
     content, which a 390px screen does not have. The page overflowed to
     496px and scrolled sideways. An explicit column count is a request, not a
     guarantee — below this width it goes back to one.

     The track is overridden rather than `--stat-columns`, because the count is
     written as an inline style at the call site and an inline custom property
     wins against any rule in here. */
  .stats { grid-template-columns: minmax(0, 1fr); }
  /* The mark is left at its own height rather than padded out to 44px. It
     clears the 24px WCAG 2.2 AA floor at 30px, it is not a target anyone
     hunts for, and padding it costs 14px off the top of every screen — which
     is the thing this whole block exists to win back. */
  .colophon__base { padding-top: 0.6rem; }
  .nav__submenu {
    position: static;
    min-width: 0;
    padding: 0.35rem 0 0.35rem 0.85rem;
    border-top: 0;
    border-left: 2px solid var(--marker);
  }
  .numbered__index { flex-direction: row; align-items: center; gap: 0.85rem; margin-bottom: 0.75rem; }
  .stat__value { white-space: normal; }
  .stat__track { max-width: none; }
  .hero { align-items: flex-end; min-height: min(30rem, 76svh); }
}


/* --------------------------------------------------------------------------
   18. The rest of the hand

   The first graft took the lines and the grounds and stopped there, which left
   two thirds of the prototype's vocabulary on the floor: no grain, no waves,
   no highlighter, no quote marks, no drawn icons, no thread on the chain, no
   ring round a figure. This is the rest of it, ported to the cool paper and
   the existing type rather than to the prototype's cream.

   It is also the layer that carries the argument. The site says the systems
   are the deliverable; the hand says people run them. Outbound and SDR work is
   a people business, and a page drawn entirely by machine says the opposite of
   that no matter what the copy claims. So the marks are heaviest where the
   claim is human — the coaching, the team, the seat — and quietest around the
   machinery, where a wobble would just look like a mistake.

   All of it is CSS or inline SVG. No images and no requests.
   -------------------------------------------------------------------------- */

/* ---- Paper grain ---------------------------------------------------------
   One fixed turbulence layer over the document, multiplied into the ground: a
   single composited layer that never repaints on scroll. Held at a third of
   the prototype's strength — that page was cream, which hides grain, and
   #edeef2 is cooler and shows it far more readily. Past about 0.2 it stops
   reading as paper and starts reading as a broken gradient. */
body::after {
  content: '';
  position: fixed;
  inset: -20%;
  z-index: 9000;
  pointer-events: none;
  opacity: 0.16;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='g'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='200' height='200' filter='url(%23g)' opacity='0.4'/></svg>");
}

/* ---- Wave dividers -------------------------------------------------------
   A band ends on a drawn edge rather than a straight one. The wave sits at the
   top of the arriving section, filled with the colour of the section it is
   leaving, so the two never have to agree about who owns the boundary.

   Sibling selectors decide which wave goes where, rather than a flag in every
   page module: the tone change is already in the markup, so asking the
   stylesheet to read it keeps the pages clean and a new section gets the right
   edge for free. The crest tops out at 52px and the bands open at 56px of
   padding, so no content needed moving. */
.section--alt::before,
.section--dark::before,
.cta-band::before,
.section--alt + .section::before,
.section--dark + .section::before,
.cta-band + .section::before {
  content: '';
  position: absolute;
  /* Inset flush. Bleeding a pixel either side to hide a subpixel seam bought
     one pixel of horizontal scroll on every page at every width instead. */
  left: 0;
  top: -1px;
  width: 100%;
  height: clamp(24px, 3vw, 48px);
  z-index: 1;
  pointer-events: none;
  background-repeat: no-repeat;
  background-size: 100% 100%;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 70' preserveAspectRatio='none'><path d='M0,-1 L1200,-1 L1200,28 C1140,20 1110,14 1078,11 C958,6 838,34 718,46 C598,58 478,42 358,22 C236,2 118,4 0,30 Z' fill='%23edeef2'/></svg>");
}
/* Leaving a sunk band for paper: the sink spills down instead. */
.section--alt + .section::before {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 70' preserveAspectRatio='none'><path d='M0,-1 L1200,-1 L1200,28 C1140,20 1110,14 1078,11 C958,6 838,34 718,46 C598,58 478,42 358,22 C236,2 118,4 0,30 Z' fill='%23e2e4ec'/></svg>");
}
/* Leaving the ink ground. This one does the most work: a hard horizontal edge
   between #0f1533 and paper is about the most machine-made thing a page can
   do, and it happens twice on every page here. */
.section--dark + .section::before,
.cta-band + .section::before,
.section--dark + .section--alt::before {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 70' preserveAspectRatio='none'><path d='M0,-1 L1200,-1 L1200,28 C1140,20 1110,14 1078,11 C958,6 838,34 718,46 C598,58 478,42 358,22 C236,2 118,4 0,30 Z' fill='%230f1533'/></svg>");
}

/* ---- The highlighter -----------------------------------------------------
   One marked phrase per page, where the argument turns human. A swipe of amber
   behind the letters that does not quite line up with them, because a real one
   would not. It sits behind the text rather than under it, so the ink keeps
   full contrast and the wash never dulls the type. */
/* An absolutely-positioned pseudo-element cannot highlight a phrase that wraps:
   the box it measures is the whole inline fragment run, so on a phone "what we
   build" broke across two lines and the wash collapsed into a 6px sliver
   between two words. A background on the element itself is drawn once per line
   fragment, which is what a highlighter actually does — and `clone` is what
   makes the padding and radius repeat on the second line instead of only
   opening on the first. */
.mark-word {
  background-image: linear-gradient(rgba(255, 180, 0, 0.34), rgba(255, 180, 0, 0.34));
  background-repeat: no-repeat;
  background-size: 100% 0.78em;
  background-position: 0 0.58em;
  border-radius: 0.35em 0.6em 0.4em 0.55em;
  padding: 0 0.14em;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}
/* On the ink bands the marked words are already amber, so an amber wash behind
   amber type is mush. The swipe goes pale there — it is still a highlighter,
   just drawn in the other pen. */
.section--dark .mark-word,
.hero .mark-word,
.cta-band .mark-word {
  background-image: linear-gradient(rgba(238, 240, 246, 0.19), rgba(238, 240, 246, 0.19));
}

/* ---- Drawn quote marks ---------------------------------------------------
   The pull quotes opened with a typographic double prime and nothing else. A
   drawn pair sitting just outside the measure says the same thing in the same
   hand as the rest of the page. */
.quote {
  position: relative;
  /* Room for the marks, which sit above the first line rather than inside the
     measure. 2.4rem was not enough: the glyphs are 2rem tall and start 1.75rem
     above the quote, so they were still landing on the link above them. */
  margin-top: clamp(3.5rem, 5vw, 5rem);
}
.quote::before {
  content: '';
  position: absolute;
  left: -1.15rem; top: -1.75rem;
  width: 2.9rem; height: 2rem;
  opacity: 0.45;
  pointer-events: none;
  background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 52 36'><g fill='none' stroke='%2325318d' stroke-width='2.1' stroke-linecap='round'><path d='M7,32 C5,19 9,10 19,4'/><path d='M9,31 C15,29 19,25 19,19 C19,14 15,11 11,12 C7,13 5,17 6,22'/><path d='M33,32 C31,19 35,10 45,4'/><path d='M35,31 C41,29 45,25 45,19 C45,14 41,11 37,12 C33,13 31,17 32,22'/></g></svg>") left top / contain no-repeat;
}
.section--dark .quote::before {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 52 36'><g fill='none' stroke='%23ffb400' stroke-width='2.1' stroke-linecap='round'><path d='M7,32 C5,19 9,10 19,4'/><path d='M9,31 C15,29 19,25 19,19 C19,14 15,11 11,12 C7,13 5,17 6,22'/><path d='M33,32 C31,19 35,10 45,4'/><path d='M35,31 C41,29 45,25 45,19 C45,14 41,11 37,12 C33,13 31,17 32,22'/></g></svg>");
}

/* ---- Sketched icons ------------------------------------------------------
   Drawn, not sourced. An icon set is the fastest place for a page to give
   itself away — every generated site reaches for the same rounded-stroke
   library — and the first of these three is two people in conversation rather
   than a funnel, which is the argument the whole layer exists to make. */
.sketch {
  flex: none;
  width: clamp(26px, 2.2vw, 32px);
  height: auto;
  overflow: visible;
}
.sketch path {
  fill: none;
  stroke: var(--marker);
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.85;
}

/* ---- Playful hover -------------------------------------------------------
   Panels straighten and lift when a cursor lands on them. Small enough to read
   as the page being alive rather than as an effect, and it never moves the
   text a reader is aiming at by more than a few pixels. */
.spread__part,
.card,
.receipt,
.terms--split .terms__pair { transition: transform var(--dur-mid) var(--ease-back); }
.spread__part:hover,
.card:hover,
.receipt:hover,
.terms--split .terms__pair:hover { transform: translateY(-4px) rotate(-0.35deg); }
.logo-strip__list img { transition: transform var(--dur-fast) var(--ease-back); }
.logo-strip__list img:hover { transform: translateY(-3px) rotate(-2deg); }

@media (prefers-reduced-motion: reduce) {
  .spread__part:hover, .card:hover, .receipt:hover,
  .terms--split .terms__pair:hover, .logo-strip__list img:hover { transform: none; }
}

/* ---- The chain thread ----------------------------------------------------
   Five nodes hung on one line that wanders as it goes down the page. A ruled
   connector would put the grid straight back into the component that exists to
   escape one. */
.chain-bed { position: relative; }
.chain__thread {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 16px;
  overflow: visible;
  pointer-events: none;
  z-index: 0;
}
.chain__thread path {
  fill: none;
  stroke: var(--marker-quiet);
  stroke-width: 2;
  stroke-linecap: round;
}
.js .chain__thread path { stroke-dasharray: 1 2; stroke-dashoffset: 1; }
.js .chain-bed.is-in .chain__thread path { stroke-dashoffset: 0; transition: stroke-dashoffset 2s var(--ease); }
@media (max-width: 60rem) { .chain__thread { display: none; } }
@media (max-width: 60rem) {
  .scenery__shape { opacity: 0.5; }
}
.chain-bed { position: relative; }

/* The drawn multiplier, set to sit on the same baseline as the figures it
   follows and at the same optical weight. */
.ex {
  display: inline-block;
  width: 0.62em;
  height: 0.8em;
  margin-left: 0.06em;
  vertical-align: baseline;
  overflow: visible;
}
.ex path {
  fill: none;
  stroke: currentColor;
  stroke-width: 3.4;
  stroke-linecap: round;
}
.stat__value--mult { display: flex; align-items: baseline; gap: 0.02em; }

/* Text carried for a screen reader only — the drawn multiplier's real value. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}


/* ---- The dumbbell --------------------------------------------------------
   Before to after, several rows, one shared 0-100 axis. Sized so the whole
   figure fits a phone without stacking: the label sits above its own plot, the
   values label the dots directly, and the axis is stated once at the bottom
   rather than repeated per row.

   The dots are drawn rings rather than filled circles, and the move between
   them is a drawn stroke, so the chart is in the same hand as the rest of the
   page. Before is claret and after is ink, which is the site's stat semantic
   already — colour is never the only cue, because both ends carry their value
   as text. */
.dumbbell { display: grid; gap: clamp(1.4rem, 3vw, 2.1rem); }
.dumbbell__rows { display: grid; gap: clamp(1.5rem, 3.5vw, 2.4rem); }
.dumbbell__row { display: grid; gap: 0.55rem; }
.dumbbell__label {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.dumbbell__plot { position: relative; height: 3.1rem; }

/* The full scale, drawn faintly so the reader can see how much room is left. */
.dumbbell__rule {
  position: absolute;
  left: 0; right: 0; top: 2.05rem;
  height: 4px;
  background: var(--drawn) left center / 100% 4px no-repeat;
  opacity: 0.85;
}
/* The move itself. --lo and --hi are pre-sorted so a fall draws the same way
   as a rise. */
.dumbbell__move {
  position: absolute;
  top: 2.05rem;
  left: calc(var(--lo) * 1%);
  width: calc((var(--hi) - var(--lo)) * 1%);
  height: 4px;
  background: var(--drawn-mark) left center / 100% 4px no-repeat;
}
.dumbbell__dot {
  position: absolute;
  top: 1.55rem;
  width: 15px; height: 15px;
  margin-left: -7px;
  border-radius: 50%;
  background: center / contain no-repeat;
}
.dumbbell__dot--from {
  left: calc(var(--from) * 1%);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 22'><path d='M11,2 C16.6,2 20,5.4 20,11 C20,16.6 16.2,20 10.6,20 C5,20 2,16.2 2,10.6 C2,5 5.4,2 11,2 C13,2 14.8,2.4 16,3.2' fill='none' stroke='%238c2f1e' stroke-width='2.6'/></svg>");
}
.dumbbell__dot--to {
  left: calc(var(--to) * 1%);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 22'><path d='M11,2 C16.6,2 20,5.4 20,11 C20,16.6 16.2,20 10.6,20 C5,20 2,16.2 2,10.6 C2,5 5.4,2 11,2 C13,2 14.8,2.4 16,3.2' fill='%230f1533' stroke='%230f1533' stroke-width='2.6'/></svg>");
}
.dumbbell__val {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  font-family: var(--display);
  font-weight: 700;
  font-stretch: 112%;
  font-size: clamp(1.05rem, 0.9rem + 0.8vw, 1.45rem);
  line-height: 1.1;
  white-space: nowrap;
}
.dumbbell__val--from { left: calc(var(--from) * 1%); color: var(--claret); }
.dumbbell__val--to   { left: calc(var(--to) * 1%);   color: var(--ink); }
/* The two labels collide when the move is short. Nudging them apart by half a
   character each is enough at every pair on the site and keeps both anchored
   to their own dot. */
.dumbbell__row { --nudge: 0; }
.dumbbell__axis {
  display: flex;
  justify-content: space-between;
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.08em;
  color: var(--text-soft);
  padding-top: 0.2rem;
}
.dumbbell__note { font-size: var(--t-small); color: var(--text-soft); max-width: 52ch; }
.section--dark .dumbbell__val--to { color: var(--text-invert); }
.section--dark .dumbbell__val--from { color: var(--amber); }
.section--dark .dumbbell__label,
.section--dark .dumbbell__axis,
.section--dark .dumbbell__note { color: var(--slate); }
.section--dark .dumbbell__rule { background-image: var(--drawn-invert); }

@media (max-width: 40rem) {
  /* At the ends of the axis a centred label runs off the edge. */
  .dumbbell__val--from { transform: translateX(min(0px, -50%)); }
  .dumbbell__plot { height: 3rem; }
}

/* One line per layer, ahead of the prose. The section's claim is that four
   things were built at once; four lines let a reader hold all four before
   deciding whether to read any of them, which is what "at the same time"
   actually needs in order to land. */
.layer__lede {
  font-family: var(--display);
  font-weight: 500;
  font-stretch: 108%;
  font-size: var(--t-lede);
  line-height: 1.3;
  color: var(--emphasis);
  max-width: 40ch;
  margin-bottom: 0.85rem;
}
.section--dark .layer__lede { color: var(--marker); }
/* With a line carrying the point, the prose can recede. */
.layer__lede + .layer__body { color: var(--text-soft); }

/* ---- The map and what it says --------------------------------------------
   The instrument on the left, the sentences about it on the right. Stacked,
   the reader had to hold two paragraphs in their head, scroll a 46rem figure
   into view, and then read two more. Adjacent, the thing and the explanation
   are in the eye at the same time.

   Closed, the nine box carries nine positions and the two marked instructions.
   That is the readable amount at this width, and the two marked cells are the
   two the copy argues about. Open, the remaining seven fill in.

   <details> supplies the toggle, the keyboard contract and the state, and the
   grid lives inside <summary> so it stays on screen either way — the body of
   the disclosure holds the caption, which is genuinely extra. */
/* The map column is deliberately the narrower of the two. A nine box is a
   diagram, not a photograph — it needs to be legible, not large, and at full
   half-width it out-shouted the argument sitting next to it. The copy takes the
   width it gives up, capped at a readable measure. */
.map-bed {
  display: grid;
  /* 0.86, not lower. Past about here the marked cells' text rewraps into more
     lines and the figure gets *taller* as the column gets narrower — at 0.78 it
     was 549px tall against 416 here, on a narrower base. */
  grid-template-columns: minmax(0, 0.86fr) minmax(0, 1.14fr);
  column-gap: clamp(2rem, 5vw, 4rem);
  row-gap: clamp(1.75rem, 4vw, 2.5rem);
  align-items: start;
}
.map-bed__map { min-width: 0; }
.map-bed__say { min-width: 0; max-width: 34rem; }
.map-bed__map > summary {
  display: block;
  cursor: pointer;
  list-style: none;
}
.map-bed__map > summary::-webkit-details-marker { display: none; }
/* Scaled down about a quarter against the standalone nine box: the type comes
   down one step, the padding and the axis gutters come down with it, and the
   column is narrower. Shrinking by column width alone does the opposite of what
   it looks like — the marked cells' text rewraps into a taller stack and the
   figure grows. The chrome has to come down with the measure. */
.map-bed__map .ninebox {
  --t-small: 0.8125rem;
  --t-micro: 0.5625rem;
  max-width: none;
  grid-template-columns: 1.2rem minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) 1.2rem;
  gap: 0.45rem;
}
.map-bed__map .ninebox__cell {
  min-height: 0;
  gap: 0.2rem;
  padding: 0.55rem 0.6rem 0.65rem;
}
.map-bed__map .ninebox__do { line-height: 1.3; }

/* Closed: positions only, except where the copy is pointing. */
.map-bed__map:not([open]) .ninebox__cell:not(.ninebox__cell--key) .ninebox__do { display: none; }
.map-bed__map:not([open]) .ninebox__cell { min-height: 3.6rem; }

.map-bed__cue {
  display: inline-block;
  margin-top: 0.9rem;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--marker);
  padding-bottom: 0.3rem;
  background: var(--drawn-mark) left bottom / 100% 4px no-repeat;
}
.map-bed__cue-shut { display: none; }
.map-bed__map[open] .map-bed__cue-open { display: none; }
.map-bed__map[open] .map-bed__cue-shut { display: inline; }
.map-bed__map .ninebox__caption { margin-top: 1rem; }

/* ---- The map, opened -----------------------------------------------------
   Closed, the nine box is deliberately small: it sits beside the argument, at
   about three quarters of the standalone size, and it carries nine positions
   and the two marked instructions. That is the right size to glance at and the
   wrong size to read.

   So opening it does not grow the column — it lifts the map out of the page
   entirely and puts it on the screen at full size. Everything below is the
   whole mechanism:

   - There is exactly one nine box in the DOM. A lightbox built by duplicating
     the grid into a dialog would make a screen reader read all nine positions
     twice, and would need the two copies kept in sync forever.
   - <details> is still the state, the toggle and the keyboard contract. The
     lightbox is what [open] looks like, not a separate component.
   - The overlay styling is behind `.js` for one reason only: without scripting
     there is no Escape key and no scroll lock, and a full-screen overlay you
     can only leave by finding one small link is worse than an inline expander.
     No JS, and it expands in place exactly as it did before.
   - `.map-bed` sizes its columns from the template, not from its items, so the
     left column stays exactly as wide when the map leaves it. Nothing behind
     the overlay moves. */
.js .map-bed__map[open] {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: block;
  overflow: auto;
  overscroll-behavior: contain;
  padding: clamp(1rem, 6vh, 4rem) clamp(0.75rem, 4vw, 3rem);
  background: rgba(16, 18, 32, 0.82);
  animation: lightbox-in 0.22s var(--ease) both;
}
.js .map-bed__map[open] > summary,
.js .map-bed__map[open] > .ninebox__caption {
  max-width: 56rem;
  margin-inline: auto;
  background: var(--paper);
}
.js .map-bed__map[open] > summary {
  position: relative;
  padding: clamp(2.6rem, 5vw, 3.2rem) clamp(1.1rem, 3vw, 2rem) 0;
  border-radius: 0.5rem 0.7rem 0 0;
  cursor: default;
}
.js .map-bed__map[open] > .ninebox__caption {
  margin-top: 0;
  padding: 1.1rem clamp(1.1rem, 3vw, 2rem) clamp(1.4rem, 3vw, 2rem);
  border-radius: 0 0 0.6rem 0.5rem;
}
/* Full size in here. The scale-down is a rule about the column, not about the
   figure, and the column is not on screen any more. */
.js .map-bed__map[open] .ninebox {
  --t-small: 0.9375rem;
  --t-micro: 0.625rem;
  grid-template-columns: 1.6rem minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) 1.6rem;
  gap: 0.6rem;
}
.js .map-bed__map[open] .ninebox__cell {
  gap: 0.3rem;
  padding: 0.85rem 0.9rem 1rem;
  min-height: 6.5rem;
}
/* Top right, out of the grid, and big enough to hit. It reads "close" here —
   the two labels are both in the markup and one of them is display:none, so a
   screen reader is told what the control does now rather than what it did
   before it was pressed. */
.js .map-bed__map[open] .map-bed__cue {
  position: absolute;
  top: 0;
  right: clamp(0.6rem, 2vw, 1.4rem);
  margin-top: 0;
  padding: 1rem 0.4rem 0.7rem;
  min-height: 44px;
  cursor: pointer;
}
@keyframes lightbox-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .js .map-bed__map[open] { animation: none; }
}
/* Every section carries `isolation: isolate`, so `z-index: 60` on the overlay
   is scoped to its own section's stacking context — and a later sibling
   section paints over the whole of an earlier one regardless of what is inside
   it. Measured: the overlay was correctly 1440×900 at inset 0 and the bottom
   65 pixels of it were painted over by the next band. The section holding an
   open map has to be lifted above its siblings, not just its own contents. */
.section:has(.map-bed__map[open]),
.section.is-lifted { z-index: 60; }

/* The page behind does not scroll while the map is up. Written by the inline
   script, because a stylesheet cannot know that a details somewhere is open. */
.is-lightboxed, .is-lightboxed body { overflow: hidden; }

/* The onward link closes the right-hand column rather than starting a fourth
   block under the grid. It gets a gap of its own so it reads as an exit, not as
   another sentence in the argument. */
.map-bed__on { margin-top: 1.4rem; }

@media (max-width: 60rem) {
  /* No second column to sit beside, so the map leads and the copy follows it —
     the order a reader wants when the figure is the subject. */
  .map-bed { grid-template-columns: minmax(0, 1fr); }
  .map-bed__map:not([open]) .ninebox__cell { min-height: 4.2rem; }
}

/* ---- The handover list ---------------------------------------------------
   Five things you are left holding. Ticked rather than numbered — they arrive
   together, and the rule says do not number what is not a sequence. The tick
   is the same drawn mark the fit list uses, so "you get this" reads the same
   way everywhere on the site.

   On a wide screen the term sits in its own column so the five terms scan as a
   list of nouns; stacked, the tick stays with the term and the definition
   indents under it, which keeps the pair together when the column goes. */
.terms--kept .terms__pair {
  display: grid;
  grid-template-columns: 1.6rem minmax(0, 0.7fr) minmax(0, 1.3fr);
  column-gap: clamp(1rem, 2.5vw, 2rem);
  row-gap: 0.35rem;
  align-items: start;
  padding-block: clamp(0.9rem, 1.8vw, 1.2rem);
}
.terms--kept .terms__pair::before {
  content: '';
  width: 1.15rem;
  height: 0.9rem;
  margin-top: 0.35em;
  background: var(--tick) center / contain no-repeat;
}
.terms--kept .terms__term { align-self: start; }
.terms--kept .terms__def { color: var(--text-soft); }
.section--dark .terms--kept .terms__pair::before { background-image: var(--tick-amber); }
.section--dark .terms--kept .terms__def { color: var(--slate); }

@media (max-width: 60rem) {
  .terms--kept .terms__pair { grid-template-columns: 1.6rem minmax(0, 1fr); }
  .terms--kept .terms__def { grid-column: 2; }
}

/* ---- The unowned list ----------------------------------------------------
   Five statements and the owner none of them had. The right column is set in
   claret and always resolves to a non-answer, so scanning down it is the
   argument: no name appears anywhere in it. */
.owners { display: grid; }
.owners__row {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
  column-gap: clamp(1.5rem, 4vw, 3.5rem);
  row-gap: 0.7rem;
  align-items: start;
  padding-block: clamp(1.4rem, 3vw, 2.1rem);
  background-image: var(--drawn);
  background-repeat: no-repeat;
  background-position: left top;
  background-size: 100% 4px;
}
/* No owner column, no second track: the situation takes the full row and the
   body keeps its 46ch measure, so the line length does not change between the
   two shapes. */
.owners__row--bare { grid-template-columns: minmax(0, 1fr); }
.owners__term { font-size: var(--t-card); line-height: 1.15; margin-bottom: 0.45rem; }
.owners__body { color: var(--text-soft); max-width: 46ch; }
.owners__body p { margin: 0; }
.owners__owner {
  position: relative;
  font-family: var(--display);
  font-size: var(--t-lede);
  line-height: 1.3;
  color: var(--claret);
  max-width: 26ch;
}
/* Stated once, at the top of the column it heads. Repeated on all five rows it
   was five identical mono labels down one edge — the exact wallpaper the
   eyebrow rule exists to prevent, just in a narrower column.

   Lifted out of the flow rather than hidden on rows two to five, because in the
   flow it pushed the first row's answer down by its own height and nothing
   else's: row one's claret line sat level with its body copy while the other
   four sat level with their term. Five answers down a column, one of them
   half a line off — the sort of misalignment you feel before you find it. Out
   of flow, all five start at the same place and the label rides in the row's
   top padding. */
.owners__row:not(:first-child) .owners__owner-label { display: none; }
.owners__owner-label {
  position: absolute;
  bottom: 100%;
  left: 0;
  margin-bottom: 0.35rem;
  white-space: nowrap;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.section--dark .owners__owner { color: var(--amber); }
.section--dark .owners__body,
.section--dark .owners__owner-label { color: var(--slate); }

@media (max-width: 60rem) {
  .owners__row { grid-template-columns: minmax(0, 1fr); }
  /* Stacked, the owner needs to read as the answer to the row above it rather
     than as the start of the next one. */
  .owners__owner { padding-left: 1rem; border-left: 0; background: none; }
  /* Back into the flow. Stacked there is no column for it to head, and out of
     flow it would print over the paragraph above. */
  .owners__owner-label {
    position: static;
    display: block;
    margin-bottom: 0.3rem;
  }
}

/* ---- The figure ring -----------------------------------------------------
   A circle round the number a case study leads with. Only ever the first one
   in a row — circling every figure marks nothing. */
.stat { position: relative; }
.stat__value { position: relative; width: fit-content; }
.stat__ring {
  position: absolute;
  left: -0.85rem; top: -0.5rem;
  width: calc(100% + 1.7rem);
  height: calc(100% + 1rem);
  overflow: visible;
  pointer-events: none;
  z-index: 0;
}
.stat__ring path {
  fill: none;
  stroke: var(--claret);
  stroke-width: 2.2;
  stroke-linecap: round;
  opacity: 0.7;
}
.section--dark .stat__ring path { stroke: var(--amber); opacity: 0.6; }
.js .stat__ring path { stroke-dasharray: 1 2; stroke-dashoffset: 1; }
.js .stat.is-in .stat__ring path { stroke-dashoffset: 0; transition: stroke-dashoffset 1.1s var(--ease) 0.3s; }

/* A figure inside a post. The prose had no image vocabulary at all, because no
   post had needed one; a diagram that replaces four paragraphs does. Full
   measure, drawn rule under the caption in the same mono as every other label
   on the site. */
.prose__fig { margin-top: clamp(1.5rem, 3vw, 2.25rem); display: grid; gap: 0.7rem; }
.prose__fig img { width: 100%; height: auto; border: 1px solid var(--rule); }
.prose__fig figcaption {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.06em;
  line-height: 1.5;
  color: var(--text-soft);
}
/* `code` arrived with the same post: schema property names read as code and
   setting them in the body serif makes them look like a typo. */
/* Tables inside a post body. The comparison tables on the main pages use
   `.matrix`, which carries its own scroll frame and drawn box; a post body is a
   66ch column and does not want either. This is the plain version: same rule
   colour, same mono caps header, no frame. Without it a <table> in a post
   renders as unstyled rows, which is the failure the design notes name. */
.prose table {
  width: 100%;
  border-collapse: collapse;
  text-align: left;
  font-size: var(--t-small);
}
.prose thead th {
  padding: 0 0.9rem 0.6rem 0;
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--indigo);
  border-bottom: 2px solid var(--rule);
  vertical-align: bottom;
}
.prose tbody th,
.prose tbody td {
  padding: 0.75rem 0.9rem 0.75rem 0;
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
}
.prose tbody th { font-weight: 600; }
.prose tbody tr:last-child th,
.prose tbody tr:last-child td { border-bottom: 0; }
/* Stacked on a phone, because a 66ch prose column has no room for the sideways
   scroll `.matrix` uses and no frame to hang a hint on. The design note is right
   that a bare stack destroys a comparison: the first pass rendered "response
   rate in narrow segments" and "operationally, when the team cannot produce
   volume with consistency" one under the other with nothing saying which column
   either came from. The cells therefore carry the column name themselves, so a
   stacked row still reads as a set of labelled answers rather than a list of
   orphaned phrases. Any table added to a post needs `data-label` on every `td`. */
@media (max-width: 34rem) {
  .prose table, .prose thead, .prose tbody, .prose tr, .prose th, .prose td { display: block; }
  .prose thead { display: none; }
  .prose tbody tr { padding-bottom: 0.7rem; border-bottom: 1px solid var(--rule); }
  .prose tbody tr:last-child { border-bottom: 0; }
  .prose tbody th, .prose tbody td { border-bottom: 0; padding: 0.15rem 0 0; }
  .prose tbody th { padding-top: 0.9rem; }
  .prose tbody td[data-label]::before {
    content: attr(data-label);
    display: block;
    font-family: var(--mono);
    font-size: var(--t-meta);
    font-stretch: 92%;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--indigo);
    margin-top: 0.45rem;
  }
}

/* ── The wall ────────────────────────────────────────────────────────────────
   Every published figure and quote on one surface. Six tracks, cells spanning
   two or three of them, chosen by a hash of the cell's own text so the layout
   is irregular and stable at the same time. `dense` backfills the holes that
   leaves, which is the whole reason the spans can vary at all.

   The cells sit on the paper with a lift rather than a border. A ruled box
   round twenty cards is the "coloured strip down the edge of a card" rule in
   its grid form: separate with ground, not with a line. */
.wall {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  grid-auto-flow: dense;
  gap: clamp(0.6rem, 1.4vw, 1rem);
  align-items: stretch;
}
.wall__cell {
  grid-column: span var(--wall-span, 2);
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  padding: clamp(1rem, 2vw, 1.6rem);
  background: var(--paper-lift);
}
.wall__cell--voice { background: var(--paper-sink); justify-content: space-between; }
.wall__cell--refusal { background: transparent; box-shadow: inset 0 0 0 1px var(--rule); }

.wall__value {
  font-size: clamp(1.65rem, 3.4vw, 2.6rem);
  font-weight: 700;
  font-stretch: 108%;
  line-height: 1;
  letter-spacing: -0.015em;
  color: var(--ink);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3em;
}
.wall__from { color: var(--claret); }
.wall__arrow { color: var(--indigo); font-size: 0.62em; }
.wall__cell--down .wall__from { color: var(--ink); }
.wall__cell--down .wall__to { color: var(--claret); }

.wall__label {
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--indigo);
}
.wall__note { font-size: var(--t-small); color: var(--text-soft); max-width: 40ch; }
.wall__source {
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  color: var(--slate);
  margin-top: auto;
}
.wall__quote { font-size: var(--t-ui); line-height: 1.45; }
.wall__who {
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--indigo);
  margin-top: 0.8rem;
}
.wall__role { display: block; color: var(--text-soft); }
.wall__refused {
  font-size: var(--t-ui);
  line-height: 1.45;
  color: var(--text);
  text-decoration: line-through;
  text-decoration-color: var(--claret);
  text-decoration-thickness: 1px;
}

/* Below 48rem there are not six tracks worth of width. Two, and every cell
   takes both: a 2-of-6 cell at 390px is 110px wide and a figure does not set
   in it. The irregularity is a desktop property and forcing it onto a phone
   buys a broken layout to preserve a texture nobody can see. */
@media (max-width: 48rem) {
  .wall { grid-template-columns: 1fr; }
  .wall__cell { grid-column: 1 / -1; }
}

/* A figure drawn as inline SVG rather than shipped as an image. */
.prose__fig svg { width: 100%; height: auto; display: block; }

.prose code {
  font-family: var(--mono);
  font-size: 0.85em;
  font-stretch: 88%;
  background: var(--paper-sink);
  padding: 0.1em 0.35em;
  border-radius: 0.2em;
}

/* A ticked list inside a two-column split has half the width it was designed
   for, and the three-track layout (tick, term, definition) squeezed the term to
   about eleven characters: "SEARCH, PAID AND ORGANIC" came out over three
   lines beside a definition set four words wide. In a split the term takes the
   full measure and the definition sits under it, which is the same collapse the
   list already does on a phone and for the same reason. */
.split .terms--kept .terms__pair { grid-template-columns: 1.6rem minmax(0, 1fr); }
.split .terms--kept .terms__def { grid-column: 2; }
