/* 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 and indigo the brand blue, and everything else is built
     to serve them.

     27 August 2026, phase 1 of the reimagined port. The paper moved from a cool
     blue-grey to a warm cream and the amber went down to an ochre. The old note
     here argued the cool paper kept the indigo bands reading as one family
     rather than as a dark mode. That was true and it is no longer the whole
     goal: warmth is now what separates paper from ink rather than binding them
     to each other.

     What did NOT change is the ground. The reimagined build is dark by default
     with cream evidence sections set into it, and flipping :root to match is
     the obvious next move — but it is deferred until the rest of the rebuild
     has landed and the whole thing can be judged at once. That is a decision,
     not an oversight. The consequence for anything added between now and then:
     write it against --marker, --emphasis and --alarm rather than
     against a literal light or dark value, so the flip stays a change to this
     block and not a sweep through the file.

     Every ratio below was measured, not estimated. Five improved, one held. */
  --ink:     #0e1126;   /* the dark ground: hero, dark bands, masthead, footer */
  --indigo:  #25318d;   /* labels and links on paper; the photographic duotone */
  --amber:   #e5af2f;   /* actions, and the gain on every delta               */
  --paper:   #f4f0e6;   /* the reading ground                                 */
  --slate:   #7c86ae;   /* metadata, hairlines on dark, the "before" figure   */
  --claret:  #8c2f1e;   /* the two or three figures a case study leads with   */

  /* A colour changed here has to be chased into two places the cascade cannot
     reach: the hexes inside SVG data URIs, and the rgb()/rgba() literals, which
     are the same value written in decimal and so do not show up in a search for
     the hex. Phase 1 caught the first set and missed the second, and the site
     shipped with the hero scrim still mixed at the old ink and the highlighter
     swipe still washed in the old amber. Search for both forms.

     Tints. Derived from the six, never a seventh hue. */
  --paper-lift:  #faf7ef;   /* a panel raised off the paper                   */
  --paper-sink:  #eae5d9;   /* a band sunk into it                            */
  --ink-lift:    #151832;   /* a panel raised off the dark ground             */
  /* The ground below the ground. The reference build carries three darks, not
     two: this one under the body dark, for the masthead, the hero, the logo
     strip and the colophon. It is what makes that build read near-black where
     ours read navy, and it was the single largest colour difference left. */
  --ink-deep:    #080a18;
  --amber-deep:  #c98512;   /* the same action, pressed                       */
  --rule:        #d5cfc0;
  --rule-invert: rgba(124, 134, 174, 0.35);

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

  /* Marker. Ochre against cream measures 1.76:1, so as a 2px hairline it is
     still effectively invisible, which is why a page of amber-marked sections
     read as one undifferentiated column of text. The warmer paper moved this
     from 1.54 and it changes nothing: amber remains a dark-ground colour and a
     fill colour, and on paper the same job still goes to indigo, now at 9.70:1.
     Every rule, tick and bullet reads --marker, and the dark tones re-point it
     to amber, where ochre measures 9.32:1 against the new ink. */
  --marker:        var(--indigo);
  --marker-quiet:  var(--rule);

  /* The two derived surfaces, on the same principle as the marker. A panel
     that lifts off the ground, or sinks into it, has to do that relative to
     whatever ground it is actually on. --paper-lift written straight into a
     component means the component can never appear on an ink band without
     going pale, which is exactly what happened to the wall on /success-stories:
     amber figures on a cream cell inside a dark section, at 1.87:1. */
  --lift:          var(--paper-lift);
  --sink:          var(--paper-sink);

  /* The emphasis colour, on the same principle. Indigo emphasises on paper;
     against the ink ground it measures 1.69: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. */
  /* 27 August 2026. The two faces swapped roles. Newsreader states the claim,
     Archivo carries the copy, which is the arrangement the reference build
     uses and the reason it reads as it does: a high-contrast serif at display
     size is the whole signal.

     One consequence runs through everything below. Archivo is width-variable
     and every display rule was setting font-stretch to expand it, because an
     all-lowercase headline has no ascender rhythm and the width was doing that
     work. Newsreader has no width axis. A font-stretch left on a display rule
     now either does nothing or asks the browser to synthesise a stretch, which
     is worse than nothing, so all fourteen came out. Weights came down with
     them: 600 in a wide grotesque is presence, 600 in a text serif is just
     heavy. */
  --display: 'Newsreader', 'Iowan Old Style', Georgia, serif;
  --body:    'Archivo', 'Helvetica Neue', Arial, sans-serif;
  --mono:    'Martian Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Scale. */
  /* 27 August 2026. Only the ceilings moved, from 4.75rem and 3rem. The floor
     and the slope of both are untouched, so nothing below roughly 1000px
     renders at a different size than it did before: the ramp simply runs
     further before it flattens. Mobile is not affected by this change.

     The ceilings are measured, not chosen. On the section headings 72px is
     where two lines becomes three, because the 18ch measure scales with the
     size until the 883px column stops it. 64 sits under that with room. The
     hero's own ceiling is 88px for the same reason. Taking headings to 72
     without moving the hero would put the two within 4px of each other and
     they would stop reading as different levels; at 88 and 64 the ratio is
     1.4, against 1.58 before. */
  /* Patrick's ceiling, not the reference's. The rebrand runs 126px on its home
     page and 114px on its inner pages; this is 88 and 80, which holds the
     reference's ratio between the two tiers while topping out where he wants
     it. The slopes are still theirs, so below about 1073px the type is
     identical to the reference and only the cap differs.

     The measured consequence, taken in Chrome with the real faces: the home
     headline drops from four lines to three and the hero from 746px to 640px.
     Nothing else in the hero moves. */
  /* One size, every hero. There were two tiers, 88px on the home page and
     80px on the rest, copied from the reference's own split. With the italic
     turn gone the heroes are a single voice and a second size was the last
     thing making twelve pages look like two systems. The slope is still the
     reference's; only the ceiling is Patrick's. */
  --t-hero:    clamp(3.625rem, 8.2vw, 5.5rem);
  /* The reference's .split-heading h2: 46px floor, 5.7vw, 82px ceiling. Ours
     capped at 64px, which is where a section heading reads as a heading rather
     than as the section's argument.

     Measured before it was set, in Chrome with the real faces loaded, across
     the home, about and outbound heads: at 82px in the new 876px content
     column NOT ONE heading gains a line over 64px. The bigger type and the
     wider column cancel. It is free. */
  --t-display: clamp(2.875rem, 5.7vw, 5.125rem);
  --t-stat:    clamp(1.75rem, 1.15rem + 2.4vw, 2.625rem);
  /* A title in a set laid across the page: the spread, the lattice. It is its
     own step because those titles are never measured against the page, they
     are measured against a column roughly a third of it, and --t-display is
     sized for the page. Measured in Chrome with Archivo actually loading: at
     --t-display every one of them broke to three and four lines. */
  /* Their .signal-row h3: 27px floor, 3vw, 43px ceiling. Ours stopped at 36. */
  --t-set:     clamp(1.6875rem, 3vw, 2.6875rem);
  --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 column down the
     left of every section holding the label, with the content squared off
     against it.

     The rail is PROPORTIONAL, not fixed. It was 10rem, which held the eyebrow
     and nothing else and left a content column ~1008px wide. The rebrand runs
     roughly 1:3 with a 72px gap, and that narrower content column is most of
     why its section headings wrap to two and three lines instead of running
     flat across the page. Track sizes now live in ONE place, --rail-cols,
     because six rules have to declare them identically: .section__inner,
     .section-head, .numbered__item, .post-header and
     .inset-bed. Change it here or the six drift apart.

     --shell is 85.5rem, not the rebrand's flat 1240px, because .shell is
     border-box and eats --gutter out of its own width: 1368 - 2x64 lands the
     content box on 1240 at a 1512 viewport, which is where the rebrand's is.
     Below the shell width the gutter clamp takes over and the number stops
     mattering. All six collapse to one column at 60rem. */
  --shell:    85.5rem;
  --rail-cols: minmax(0, 1fr) minmax(0, 3fr);
  --rail-gap: clamp(1.5rem, 4.8vw, 4.5rem);
  --gutter:   clamp(1.25rem, 5vw, 4rem);
  --flow:     1.1rem;
  --band:     clamp(3.5rem, 8.6vw, 8.125rem);
  --band-tight: clamp(2.5rem, 5vw, 4rem);
  --band-tall:  clamp(5rem, 11vw, 9.5rem);
  /* Arriving on the same ground as the band above: about 60% of a normal
     seam, which lands near 160px between the two at desktop. */
  --band-abut:  clamp(2rem, 5vw, 4.5rem);
  --masthead: 5.5rem;   /* 88px, the reference's header height */
}

/* On any dark ground the marker goes back to amber, which measures 9.32:1 there.
   One declaration, and every rule, tick and bullet on the site follows. */
.section--dark,
.hero,
.colophon,
.masthead {
  --marker: var(--amber);
  --marker-quiet: var(--rule-invert);
  --lift: var(--ink-lift);
  --sink: var(--ink);
  /* The two text values follow the ground for the same reason every other
     semantic here does. --text measures 1.05:1 against this band and
     --text-soft 2.4:1, so any component that set either one explicitly rather
     than inheriting was illegible the moment it landed on ink. There were 45
     such elements across seven pages. Two declarations instead of forty-five
     overrides. --ink and --claret are NOT re-pointed here: both are used as
     backgrounds elsewhere, and a background that follows the ground is not a
     ground, it is a disappearing act. */
  --text: var(--text-invert);
  --text-soft: var(--slate);
  --emphasis: var(--amber);
  --alarm: var(--amber);
}

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

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

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

body {
  margin: 0;
  /* The ground. Sections paint their own surface over this, so what shows here
     is the overscroll and any gap between them — which has to be the dark, not
     a strip of cream flashing at the top of a rubber-band scroll.

     .section keeps `color: var(--text)` in its own rule, so a cream section
     still gets dark type by inheriting from the section rather than the body. */
  background: var(--ink);
  color: var(--text-invert);
  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: 400;
  font-size: var(--t-display);
  line-height: 1.06;
  letter-spacing: -0.022em;
  color: var(--text);
}

/* 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. */
/* Weight, not italic, despite the name.

   Under Archivo this class faked an italic with weight and width, because
   Archivo ships none. Newsreader has a true one, so the obvious move was to
   let the class mean what it says. Rendered, that was wrong: the class wraps
   whole headings, not words, in 24 places across 11 pages. An italic on most
   of the headings on a site is not emphasis, it is the heading style, and the
   roman ones become the exception. Emphasis that is everywhere is not
   emphasis — the same reason the eyebrow rule stopped being a coin flip and
   the scenery wash stopped keying off tone.

   The true italic is now used nowhere. The hero turn was the one place it was
   earned and the heroes are plain roman as of this pass.

   To take the italic instead, this is the one line: font-style: italic. */
h1.display {
  font-size: var(--t-hero);
}

.display--italic {
  font-style: normal;
  font-weight: 600;
}

/* .display--turn is gone. It set the second sentence of a hero headline in
   Newsreader italic at 106%, and it was a real device: the grotesque states
   the claim, the serif answers it. What retired it is that the site ran three
   emphasis systems across twelve heroes — the italic turn on four, an amber
   word on two, nothing on six — and three ways of saying "this part" is the
   same as none. Plain roman everywhere. .display--italic stays; it is doing
   work outside heroes, seven times on /about alone. */

.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(--marker);
}
.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 {
  /* Sans, not the display serif. The kicker answers the headline
     rather than continuing it, and in the same voice at a smaller size it
     read as a stray second line of the same sentence. */
  font-family: var(--body);
  font-weight: 500;
  font-size: var(--t-kicker);
  line-height: 1.25;
  letter-spacing: -0.014em;
  color: var(--text);
}

.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.69: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;
  /* Mixed rather than solid so the rule reads as a support under the label and
     not as a second line of type. currentColor keeps it on whatever ground the
     link lands on with no override. */
  border-bottom: 1px solid color-mix(in srgb, currentColor 52%, transparent);
  transition: color 0.15s ease, border-color 0.15s ease;
}
.arrow-link:hover { color: var(--text); border-bottom-color: currentColor; }
.arrow-link__arrow { padding-left: 0.45em; }
/* The exit register. Same rule underneath, set as a control rather than as a
   sentence: upper case, letterspaced, and a little more air over the rule. */
.arrow-link--cue {
  font-size: var(--t-micro);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding-bottom: 0.45rem;
}
.arrow-link--cue .arrow-link__arrow { padding-left: 0.6em; }

/* There is no filled control on this site any more.

   It used to be a solid amber plate with ink type on it. The reference has no
   filled button anywhere: every call to action on it, in the hero, in the CTA
   band, in the nav and on the booking card, is the same thing — a short
   uppercase label with a hairline under it and the arrow in ochre. That is a
   quieter site, and in the hero's floor bar it is the difference between a
   composition and a composition with a traffic sign in the corner.

   The label follows the ground through --text, which is the whole reason the
   old rule had to hardcode ink: on an amber plate the label could not follow
   anything. Off the plate it can, and the contrast problem goes with it. Only
   the arrow is marker-coloured, which is the reference's one accent. */
.button {
  display: inline-block;
  padding: 0 0 0.45rem;
  background: none;
  color: var(--text);
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  text-decoration: none;
  border: 0;
  border-bottom: 1px solid color-mix(in srgb, var(--text) 48%, transparent);
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.button:hover { border-bottom-color: var(--marker); }
.button__arrow { padding-left: 0.5em; color: var(--marker); }

/* --------------------------------------------------------------------------
   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); }
/* Amber, not slate. This rule used to paint every eyebrow on a dark band the
   same blue-grey as the body copy, which put half the labels down a dark page
   in the marker colour (the ones that are .kicker, .conviction__label,
   .signal__label) and half in text colour, with nothing deciding which. One
   colour per ground: --marker already re-points to amber on every dark
   context, so the label just follows it.

   It also has to outrank .eyebrow--soft, which is why the tone modifier is
   named here rather than left to source order — 23 section heads pass
   labelTone: 'soft', and on ink that tone has nothing left to do. */
.section--dark .eyebrow,
.section--dark .eyebrow--soft { color: var(--marker); }
.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;
  /* --section-cols, not --rail-cols. The collapse below re-points a variable,
     and --rail-cols is read by five other rules further down the file
     (.numbered__item, .post-header, .inset-bed and the section head). Setting
     that one on the container would inherit into all of them and collapse a
     numbered item's index rail because the section it happens to sit in has no
     label of its own. This name is read here and nowhere else. */
  grid-template-columns: var(--section-cols, var(--rail-cols));
  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: var(--rail-place, 2); }

/* A rail with nothing in it is not a rail, it is a hole.

   The rail reserves 1fr — about 292px at a 1512 window — for a section's
   label. Sections that never had a label were reserving it anyway, so their
   heading started 360px in from the edge with nothing to its left and read as
   a mistake. The reference does not do this: its 1fr/3fr grid appears only on
   the blocks that carry a label (.split-heading, .belief, .service-promise,
   .cta-inner) and everything unlabelled runs the full shell.

   :has() rather than a modifier class because the condition is exactly "is
   there a label in the rail", which the markup already answers. Scoped to the
   direct child chain so an .eyebrow inside a card further down the section
   cannot hold the rail open on the section's behalf.

   It re-points the VARIABLES rather than setting grid-template-columns and
   grid-column directly, and that is not a style preference. Written directly,
   the selector carries a :has() and outranks any single class, so it beat
   .phases__inner — a module that declares its own two tracks and places its own
   children — forced that whole module into one column, and dropped a sticky
   anchor on top of the frames it is supposed to sit beside. Through the
   variables, a module that sets its own tracks simply sets them and this rule
   never enters the argument. */
.section__inner:not(:has(> .section-head > .eyebrow)) {
  --section-cols: minmax(0, 1fr);
  --rail-place: 1;
}
.section__inner:not(:has(> .section-head > .eyebrow)) > .section-head { grid-template-columns: minmax(0, 1fr); }
.section__inner:not(:has(> .section-head > .eyebrow)) > .section-head > * { grid-column: 1; grid-row: auto; }

/* The opt-back-in.

   The rule above asks "is there a label in the rail" and answers it by looking
   for a section head, because until now every rail label was one. A band that
   holds two labelled modules and no section head of its own — the conviction
   and the curve that draws it — answers yes to the question and no to the
   selector, so it lost the rail and the curve, which spans `1 / -1`, spanned a
   single implicit track.

   A modifier rather than widening the :has() chain: that selector runs on every
   section of every page, and this is one band with a known shape.

   Two classes on the section, because :not() and :has() each take the
   specificity of their most specific argument: the rule above scores (0,3,0)
   off its `> .section-head > .eyebrow` and a single-class modifier loses to
   it. */
.section.section--railed > .section__inner {
  --section-cols: var(--rail-cols);
  --rail-place: 2;
  /* The seam between the two modules, opened past the standard module rhythm.
     Two labelled modules in one band need more air between them than two
     blocks of one module do, or the second eyebrow reads as a continuation of
     the paragraph above it rather than the start of something. Still well
     short of a section boundary: this is a paragraph break, not a chapter. */
  row-gap: clamp(3.5rem, 7vw, 5.5rem);
}

.section-head {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: var(--rail-cols);
  column-gap: var(--rail-gap);
  row-gap: var(--flow);
}
.section-head .eyebrow { grid-column: 1; grid-row: 1; align-self: start; justify-self: start; }
/* On a case study the label goes above its own heading instead of out in the
   rail, which is how the reference build sets its chapter marks — and which is
   what frees the rail track for the contents list. Scoped by :has() so it
   applies exactly to the pages that carry one. */
@media (min-width: 60.0625rem) {
  body:has(.case-nav) .section-head .eyebrow { grid-column: 2; grid-row: 1; margin-bottom: 0.35rem; }
  body:has(.case-nav) .section-head .display { grid-row: 2; }
  body:has(.case-nav) .section-head .lede { grid-row: 3; }
}
/* No character cap. 18ch was doing real work at 64px, holding the heading to
   683px inside an 1008px column; at 82px inside 876px it is 1476px and inert,
   and at the sizes below the top of the scale it only ever pulls the heading
   narrower than the column it was given. The reference sets no measure here
   and lets the column be the measure, which is what the column is for. The
   one-column rule below the breakpoint still caps it. */
.section-head .display { grid-column: 2; grid-row: 1; }
.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); }

/* A seam with no ground change under it.
 *
 * Almost every band on this site lands on the opposite ground from the one
 * above it, so the standard 124px each side of a seam is split by a
 * paper-to-ink edge and reads as a chapter break. Where two bands share a
 * ground the same 248px has nothing crossing it, and it stops reading as a
 * boundary and starts reading as a hole — measurably identical to every other
 * seam on the page and, to a reader, twice the size.
 *
 * So the seam gets smaller, not bigger: with no colour doing the work, the
 * space itself has to be tight enough to read as deliberate.
 *
 * The class goes on the arriving band, and the departing one is reached with
 * :has(+ …) rather than a second class to remember — a seam is a relationship
 * between two bands, and marking one of them should be enough to describe it.
 * Both sides close, because trimming only the top leaves two thirds of the
 * gap standing. */
.section--abut { padding-top: var(--band-abut); }
.section:has(+ .section--abut) { padding-bottom: var(--band-abut); }

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

/* --------------------------------------------------------------------------
   4b. Case-study figures
   -------------------------------------------------------------------------- */

/* The contents rail.

   It sits in the rail track — the same 1fr column every section opens on the
   left — which on a case study is empty, because the section label has been
   moved into the content column above its heading (see the .case-nav rule in
   the section-head block). Fixed rather than sticky: the sections are
   full-bleed bands and a sticky element cannot span past its own parent.

   The first attempt was a 28px strip of numerals with the label clipped to
   nothing until hover. It fit, it never collided, and nobody could tell it was
   a table of contents. An index that has to be discovered is not an index. */
.case-nav {
  position: fixed;
  top: 50%;
  left: max(var(--gutter), calc((100vw - var(--shell)) / 2 + var(--gutter)));
  z-index: 40;
  transform: translateY(-50%);
  display: none;
  width: min(15.625rem, 20vw);
  opacity: 0;
  /* visibility, not just opacity. At opacity 0 the six links are still in the
     tab order, so a keyboard reader leaving the hero fell into an index that
     is not on screen yet and cannot be seen. visibility takes them out of the
     tree and the transition still runs, because visibility is animatable
     between hidden and visible as a step at the end of the timing function. */
  visibility: hidden;
  transition: opacity var(--dur-mid) var(--ease), visibility var(--dur-mid);
  pointer-events: none;
}
.case-nav.is-on { opacity: 1; visibility: visible; pointer-events: auto; }
.case-nav__label {
  margin: 0 0 1.5rem;
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--marker);
}
.case-nav__list { margin: 0; padding: 0; list-style: none; display: grid; gap: 1.125rem; }
.case-nav__link {
  display: block;
  font-family: var(--body);
  font-size: 0.9375rem;
  line-height: 1.35;
  color: var(--text-soft);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}
.case-nav__link:hover { color: var(--text); }
.case-nav__link:focus-visible { outline: 2px solid var(--marker); outline-offset: 3px; }
.case-nav__link[aria-current='true'] { color: var(--text); }
/* The rail follows the ground it is floating over, the same way everything
   else on this site does. Set from the band the reader is actually on. */
.case-nav[data-ground='paper'] { --text: #171c33; --text-soft: #62625f; --marker: #8a6a10; }
.case-nav[data-ground='ink'] { --text: #eef0f6; --text-soft: #7c86ae; --marker: #e5af2f; }
/* 60rem is where the rail track itself collapses. Above it there is a column
   for this to live in; below it there is not, and a phone has no use for a
   floating index anyway. */
@media (min-width: 60.0625rem) {
  .js .case-nav { display: block; }
}
@media (prefers-reduced-motion: reduce) {
  .case-nav { transition: none; }
}

/* The metric band. Sits between the hero and the first section, on the same
   deep ground as the hero above it, so the numbers read as the last line of
   the hero rather than the first line of the article. */
.case-metrics {
  background: var(--ink-deep);
  color: var(--text-invert);
  --marker: var(--amber);
  --text: var(--text-invert);
  --text-soft: var(--slate);
}
.case-metrics__inner {
  display: grid;
  grid-template-columns: repeat(var(--metric-columns, 4), minmax(0, 1fr));
  padding-block: clamp(2.25rem, 5vw, 3.5rem);
}
.case-metrics__cell {
  display: flex;
  flex-direction: column;
  padding: 0.5rem clamp(1rem, 2vw, 1.875rem);
  border-left: 1px solid var(--rule-invert);
}
.case-metrics__cell:last-of-type { border-right: 1px solid var(--rule-invert); }
.case-metrics__value {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.75rem, 3.4vw, 3rem);
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--marker);
}
.case-metrics__label {
  margin-top: 0.5rem;
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--slate);
}
.case-metrics__note {
  grid-column: 1 / -1;
  margin: 2rem 0 0;
  padding-top: 1.375rem;
  border-top: 1px solid var(--rule-invert);
  font-size: var(--t-small);
  line-height: 1.6;
  color: var(--slate);
  max-width: 78ch;
}
@media (max-width: 60rem) {
  .case-metrics__inner { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 1.75rem; }
  .case-metrics__cell:last-of-type { border-right: 0; }
}
@media (max-width: 30rem) {
  .case-metrics__inner { grid-template-columns: minmax(0, 1fr); }
}

/* The two-state control. A rule above and below, the offer on the left, the
   two buttons on the right: it reads as a setting for the page rather than as
   a call to action competing with the one at the bottom. */
.case-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem 1.75rem;
  padding-block: 1.25rem;
  border-block: 1px solid var(--marker-quiet);
}
.case-controls[hidden] { display: none; }
.case-controls__label {
  margin: 0;
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.case-controls__buttons { display: flex; gap: 0.625rem; }
.case-controls button {
  padding: 0.625rem 0.875rem;
  background: none;
  color: var(--text);
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  border: 1px solid var(--marker-quiet);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.case-controls button:hover { color: var(--marker); border-color: var(--marker); }
.case-controls button:focus-visible { outline: 2px solid var(--marker); outline-offset: 2px; }

/* A metric that opens. The cue is a plus that turns into a cross, the same
   control the phone menu uses, so it is one gesture learned once. */
.case-metrics__cell--open > summary {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  column-gap: 0.75rem;
  cursor: pointer;
  list-style: none;
}
.case-metrics__cell--open > summary::-webkit-details-marker { display: none; }
.case-metrics__cell--open > summary > .case-metrics__value { grid-column: 1; }
.case-metrics__cell--open > summary > .case-metrics__label { grid-column: 1; }
.case-metrics__cue { grid-column: 2; grid-row: 1; align-self: start; font-size: 1.125rem; }
.case-metrics__cell--open[open] .case-metrics__cue { transform: rotate(45deg); }
.case-metrics__detail {
  margin: 0.875rem 0 0;
  font-size: var(--t-small);
  line-height: 1.6;
  color: var(--slate);
}

/* The scope note as a door. Same plus, same rotation, one row wide instead of
   one cell: the label names what is behind it, so opening it is a choice
   rather than a discovery. */
.case-metrics__note--open {
  max-width: none;
  padding-bottom: 0;
}
.case-metrics__note--open > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  cursor: pointer;
  list-style: none;
}
.case-metrics__note--open > summary::-webkit-details-marker { display: none; }
.case-metrics__note--open > summary:focus-visible { outline: 2px solid var(--marker); outline-offset: 4px; }
.case-metrics__note-label {
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--slate);
}
.case-metrics__note--open[open] .case-metrics__cue { transform: rotate(45deg); }
.case-metrics__note-body { margin-top: 1.125rem; max-width: 78ch; }
.case-metrics__note-body > p { margin: 0 0 0.875rem; }
.case-metrics__note-body > p:last-child { margin-bottom: 0; }

/* Two columns behind one disclosure: what we saw, and who owned it. On the
   surface the owner line was a restatement of the body beside it, which is why
   it came off. Set against the observation rather than beside the claim, it is
   a second dimension rather than an echo. */
.layer--split > .layer__body { grid-template-columns: minmax(0, 13rem) minmax(0, 1fr); }
/* align-content: start, and it is not decoration. Two auto rows in a cell that
   is stretched to the height of the taller column get the spare height
   distributed into them, which floated the owner line halfway down the row
   while its label stayed at the top. */
.layer--split > .layer__body > .layer__col { grid-column: 2; display: grid; align-content: start; gap: 0.4rem; }
.layer__col-label {
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--text-soft);
}
@media (min-width: 60rem) {
  .layer--split > .layer__body { grid-template-columns: minmax(0, 13rem) minmax(0, 1fr) minmax(0, 1fr); }
  .layer--split > .layer__body > .layer__col:first-child { grid-column: 2; }
  .layer--split > .layer__body > .layer__col:last-child { grid-column: 3; }
}

/* The comparison graph. A panel on the raised ground with a hairline round it,
   so it reads as an exhibit rather than as more page. */
.graph {
  margin: 0;
  padding: clamp(1.25rem, 3vw, 2.375rem);
  background: var(--lift);
  border: 1px solid var(--marker-quiet);
}
.graph__legend {
  display: flex;
  justify-content: flex-end;
  gap: 1.5rem;
  margin-bottom: 2rem;
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.graph__legend span { display: flex; align-items: center; gap: 0.45rem; }
.graph__key { display: block; width: 1rem; height: 5px; }
.graph__key--before, .graph__bar--before { background: color-mix(in srgb, var(--text) 38%, transparent); }
.graph__key--after,  .graph__bar--after  { background: var(--marker); }
.graph__row {
  display: grid;
  grid-template-columns: 9.6875rem minmax(0, 1fr);
  gap: 1.75rem;
  align-items: center;
  padding-block: 1.1875rem;
  border-top: 1px solid var(--marker-quiet);
}
.graph__name { display: flex; flex-direction: column; align-items: flex-start; }
.graph__name b {
  font-family: var(--display);
  font-size: 1.1875rem;
  font-weight: 400;
  line-height: 1.15;
}
.graph__name small {
  margin-top: 0.2rem;
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.graph__bars { display: flex; flex-direction: column; gap: 0.45rem; align-items: flex-start; }
/* The value rides at the end of its own bar. This is the whole reason the form
   replaced the dumbbell on these pages: two labels on two rows cannot collide,
   however close the two figures are. */
.graph__bar { position: relative; min-width: 3.375rem; height: 1rem; display: block; }
.graph__bar em {
  position: absolute;
  left: 100%;
  top: 50%;
  margin-left: 0.5625rem;
  transform: translateY(-50%);
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-style: normal;
  white-space: nowrap;
  color: var(--text);
}
.graph__note {
  margin: 1.5rem 0 0;
  padding-top: 1.25rem;
  border-top: 1px solid var(--marker-quiet);
  font-size: var(--t-small);
  line-height: 1.6;
  color: var(--text-soft);
}
@media (max-width: 44rem) {
  .graph__row { grid-template-columns: minmax(0, 1fr); gap: 0.9rem; }
  /* The bars lose the room the end-labels need, so they come inboard. */
  .graph__bars { width: 100%; }
  .graph__bar { max-width: calc(100% - 4.5rem); }
}

/* Report against reality, with a mark between them. */
.audit__card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  gap: clamp(1.25rem, 3vw, 2.1875rem);
  align-items: center;
  padding: clamp(1.5rem, 4vw, 3.125rem);
  background: var(--lift);
  border: 1px solid var(--marker-quiet);
}
.audit { margin: 0; }
.audit__side { display: flex; flex-direction: column; }
.audit__label {
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.audit__value {
  margin: 1.125rem 0 0.375rem;
  font-family: var(--display);
  font-size: clamp(1.5rem, 2.6vw, 2.5rem);
  font-weight: 400;
  line-height: 1.05;
}
.audit__note { font-size: var(--t-small); line-height: 1.5; color: var(--text-soft); }
.audit__mark {
  font-family: var(--display);
  font-size: clamp(1.75rem, 3vw, 2.5rem);
  font-weight: 400;
  color: var(--marker);
}
.audit__caption {
  margin-top: 1.25rem;
  font-size: var(--t-small);
  line-height: 1.6;
  color: var(--text-soft);
}
@media (max-width: 44rem) {
  .audit__card { grid-template-columns: minmax(0, 1fr); gap: 1rem; }
  .audit__mark { justify-self: start; }
}

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

.masthead {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--ink-deep);
  color: var(--text-invert);
  border-bottom: 1px solid var(--rule-invert);
}
/* On a page that opens with a hero the masthead sits ON the photograph, with
   no bar of its own. This is the rebrand's <Header overlay>: their header is
   transparent and inside the hero section, and it is why their hero runs to
   the top of the window while ours started below a solid strip.

   :has() rather than a page-level class because the condition genuinely is
   "does this page open with a hero" and nothing else has to know. absolute,
   not sticky, because theirs scrolls away with the page: it is positioned
   against the initial containing block, which at top:0 is exactly where the
   masthead already was. Pages with no hero keep the sticky bar untouched.

   The three overrides are not redundant. The masthead's bottom rule is drawn,
   not stroked: it is a background-image set in the texture section further
   down, so clearing `background` alone leaves the hand-inked line hanging in
   mid-air over the photograph. */
body:has(main > .hero:first-child) .masthead {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  background: transparent;
  background-image: none;
  /* The hairline stays. The reference header is transparent over the
     photograph but keeps its bottom rule, and that single line is what stops
     the nav reading as type floating loose on a picture. What comes off is the
     drawn one, which is a background-image and would hang in mid-air. */
  border-bottom: 1px solid var(--rule-invert);
}

/* Three tracks, not a flex row. space-between centres the nav only when the
   brand and the action happen to be the same width, which they are not: the
   links sat 40px right of centre. 1fr auto 1fr puts the middle track in the
   middle of the bar whatever is on either side of it, which is how the
   reference header is built. */
.masthead__inner {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--gutter);
  min-height: var(--masthead);
}
.masthead__cta { justify-self: end; }
.masthead__brand { display: block; flex-shrink: 0; }
.masthead__brand img { width: clamp(6.5rem, 12vw, 9rem); height: auto; }

/* Above the breakpoint the whole disclosure is removed and the list is an
   ordinary row. Nothing here depends on <details> behaviour, which is the
   point: the first version nested the list inside the disclosure and it
   measured 0px wide on desktop, because a closed <details> hides its children
   through a shadow slot that an author `display` cannot resize. */
.nav__disclose { display: none; }
.nav__list { display: flex; }
.nav__disclose > summary::-webkit-details-marker { display: none; }
.nav__disclose > summary::marker { content: ''; }

.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); }
/* The action is a label in the ground colour with an ochre rule under it and
   an ochre arrow, which is the same control as the buttons further down the
   page. It used to be amber type on amber rule, the only place on the site
   where the accent was carrying a whole word at small size. */
.nav__link--cta { border-bottom-color: var(--marker); }
.nav__link--cta:hover { color: var(--marker); }

/* The caret. Ochre, small, and it turns over rather than being swapped for a
   second glyph, so the control reads as one thing changing state. */
.nav__caret {
  display: inline-block;
  padding-left: 0.45em;
  font-size: 0.8em;
  color: var(--marker);
  transition: transform var(--dur-fast) var(--ease);
}
.nav__group[open] > .nav__link .nav__caret { transform: rotate(180deg); }
.nav__caret--go { transform: none; }

/* The menu panel. The reference's one real navigation idea: a menu that says
   what a page is for, not only what it is called. Title in the display serif,
   note under it in small type. A flat list of six labels asks a visitor to
   already know what "what is actually broken" leads to. */
.nav__group { position: relative; }
.nav__group > .nav__link { list-style: none; }
.nav__group > .nav__link::-webkit-details-marker { display: none; }
.nav__group > .nav__link::marker { content: ''; }
.nav__submenu {
  position: absolute;
  top: calc(100% + 1.5rem);
  left: 50%;
  z-index: 30;
  width: 19.375rem;
  margin: 0;
  padding: 0.75rem;
  transform: translateX(-50%);
  list-style: none;
  background: var(--ink-deep);
  border: 1px solid var(--rule-invert);
  box-shadow: 0 24px 50px rgba(0, 0, 0, 0.32);
}
/* The bridge. The panel opens 24px below the summary, and without something
   filling that band the pointer leaves the group on the way down and a
   hover-styled row flickers. It costs nothing and it is invisible. */
.nav__submenu::before {
  content: '';
  position: absolute;
  top: -1.5rem;
  left: 0;
  right: 0;
  height: 1.5rem;
}
.nav__submenu li + li .nav__sublink { border-top: 1px solid var(--rule-invert); }
.nav__sublink {
  display: block;
  padding: 0.9rem 1rem;
  color: var(--text-invert);
  text-decoration: none;
  text-transform: none;
  letter-spacing: 0;
  white-space: normal;
  transition: background-color var(--dur-fast) var(--ease);
}
.nav__sublink:hover { background: var(--ink-lift); }
.nav__subtitle {
  display: block;
  font-family: var(--display);
  font-size: 1.3125rem;
  font-weight: 400;
  line-height: 1.1;
  letter-spacing: -0.015em;
  color: var(--text-invert);
}
.nav__subnote {
  display: block;
  margin-top: 0.3rem;
  font-family: var(--body);
  font-size: 0.6875rem;
  line-height: 1.45;
  color: var(--slate);
}

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

/* The hero fills a band rather than sitting in one. It used to be
   align-items: flex-end on a 34rem box, which stacked everything against the
   bottom edge and left the rest as dead space: on a 1600 by 1000 window that
   was 625px of hero holding 433px of content, and on anything taller the 34rem
   cap meant the hero stopped growing while the screen kept going.

   Stretch instead, and let the title push what follows to the floor (see
   .hero__title). The eyebrow and the headline take the top, the lede, kicker
   and call to action take the bottom, and the distance between them is the
   composition rather than a gap left over. The photograph benefits too: the
   scrim is an angled gradient, so over a taller box it spreads and more of the
   picture comes through.

   88svh with a 42rem ceiling. svh rather than vh because vh on iOS is the
   height with the toolbars hidden, which is not the height you get on arrival. */
.hero {
  position: relative;
  display: flex;
  align-items: stretch;
  /* No ceiling. The 42rem cap meant the hero stopped growing at 672px while
     the screen kept going, so on a large display it sat as a band near the top
     with the page starting under it. The rebrand runs 78vh on inner pages and
     92vh on the home page and never caps, and that uncapped fill is most of
     what reads as "their hero" versus "a header image".

     The top pad clears the masthead, which now sits ON the photograph rather
     than on a bar above it (see the overlay rule in the masthead section). */
  min-height: 78svh;
  /* Tight, and measured rather than chosen. The reference hero at a 1512x771
     window runs 790px tall and lands its floor bar at y=663. Ours carries one
     element theirs does not (the kicker), which is 57px of content the shape
     was not drawn around, and at the roomier padding the call to action fell
     off the bottom of that window entirely. These two numbers buy it back. */
  padding-block: calc(var(--masthead) + clamp(1.25rem, 3.5vh, 3rem)) clamp(1.5rem, 3vh, 2.5rem);
  background: var(--ink-deep);
  color: var(--text-invert);
  overflow: hidden;
}

/* The photographs sit on the ground and are dimmed, not duotoned.

   They used to be plated onto brand indigo with a luminosity blend, which made
   every hero on the site the same blue whatever the source picture was. That
   was a deliberate unifier and it is the reason our heroes read blue-purple
   where the reference build reads near-black. Matching it means the plate goes
   to the deep ground and the blend comes off: the picture keeps its own
   colour, held down by the scrim over it. */
.hero__photo {
  position: absolute;
  inset: 0;
  background: var(--ink-deep);
  isolation: isolate;
}
/* No filter. The picture is not desaturated and not dimmed; every bit of the
   darkness comes from the scrim above it. Ours ran grayscale(0.55) with
   brightness(0.5), which flattened the photograph into a uniform grey field
   with no light anywhere in it. The rebrand leaves the file alone. */
.hero__photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Two gradients, not one, and the first is horizontal.

   Ours was a single 8deg wash: near-flat, so it dimmed the whole frame evenly
   and the photograph never actually appeared anywhere. The rebrand stacks a
   horizontal wash that is 96% opaque at the left edge where the words are and
   opens to 25% at the right, where the picture is allowed to be a picture,
   over a bottom-up wash that holds the floor for the lede and the button. The
   composition is left-dark, right-open. That is the effect worth copying. */
.hero__photo::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(90deg, rgba(8, 10, 24, 0.96) 0%, rgba(8, 10, 24, 0.79) 45%, rgba(8, 10, 24, 0.25) 100%),
    linear-gradient(0deg, rgba(8, 10, 24, 0.75), transparent 55%);
}
/* Same recipe, lighter, for a hero whose photograph is the point. It used to
   be a different angle as well as a different weight, which made those pages
   look like a different site rather than a lighter page of this one. */
.hero--open .hero__photo::after {
  background:
    linear-gradient(90deg, rgba(8, 10, 24, 0.9) 0%, rgba(8, 10, 24, 0.6) 45%, rgba(8, 10, 24, 0.12) 100%),
    linear-gradient(0deg, rgba(8, 10, 24, 0.62), transparent 55%);
}
/* Below the breakpoint the text runs the full width of the frame, so it would
   sit over the open right-hand end of that horizontal wash. The rebrand keeps
   the desktop gradient here and accepts it; we do not, because the lede at 25%
   scrim over a lit photograph fails contrast. Same shape, floor raised. */
@media (max-width: 48rem) {
  .hero__photo::after,
  .hero--open .hero__photo::after {
    background:
      linear-gradient(90deg, rgba(8, 10, 24, 0.95) 0%, rgba(8, 10, 24, 0.87) 55%, rgba(8, 10, 24, 0.66) 100%),
      linear-gradient(0deg, rgba(8, 10, 24, 0.8), transparent 60%);
  }
}

/* Two hero shapes, and the rebrand has both.

   The default is the inner-page shape: everything stacked against the floor,
   eyebrow and headline and lede together as one block. That is what their
   .page-hero does, and it is what all but one of our heroes should be.

   .hero--split is the home shape: eyebrow and headline at the ceiling, the
   lede and the call to action along the floor as a ROW, one at each end. The
   distance between the two groups is the composition. Only the home page
   passes split; theirs is the only page that uses it either. */
.hero__inner {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-end;
  gap: var(--flow);
}
/* The bottom bar. As a row it puts the lede on the left and the button on the
   right, aligned on their last line; stacked, it is just the old column and
   nothing changes for the inner pages. */
.hero__bottom {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--flow);
}
.hero--split .hero__bottom {
  width: 100%;
  flex-direction: row;
  align-items: flex-end;
  justify-content: space-between;
  gap: 3rem;
}
@media (max-width: 60rem) {
  .hero--split .hero__bottom { flex-direction: column; align-items: flex-start; gap: var(--flow); }
}
/* 80svh, not the reference's 92.

   Their home hero fills its box: a 126px headline over four lines is 436px of
   type, and the floor bar starts the pixel the title ends. At 88px the same
   headline is two lines and 180px, so at 92svh the split shape held 271px of
   empty photograph between the headline and the floor and read as a hole
   rather than as air. 80svh puts that gap at 178px, which is breathing room,
   and keeps the home hero taller than the 78svh inner pages rather than
   shorter than them. The number follows the type; change one and re-measure
   the other. */
.hero--split { min-height: 80svh; }
/* The auto margin here is what splits the hero. Everything before the title
   sits at the top, everything after it at the bottom, and it works whatever a
   given page's hero actually contains: /about has only a title and a lede,
   the case study opens with a mark and closes with a meta line, and neither
   needs its own rule. Pushing from the title rather than from the lede is the
   reason. It is set as the end value of margin-block below, not as a separate
   margin-bottom, because a later shorthand in the same rule would eat it. */
.hero__title {
  font-size: var(--t-hero);
  font-weight: 400;
  /* 0.88, not 0.98. At 124px a tenth of leading is 12px per line, and three
     lines set at 0.98 read as three separate lines where the rebrand's read as
     one block of type. This is the single biggest tell after the size. */
  line-height: 0.88;
  letter-spacing: -0.032em;
  color: var(--text-invert);
  /* A pixel measure, not a ch measure, because the rebrand uses one: 17ch
     happens to land within a few pixels of it at the top of the scale but
     drifts at every size below it. */
  max-width: min(100%, 1050px);
  /* The auto end margin is the split, and it has to live in this shorthand
     rather than as a margin-bottom above it. Written separately at the top of
     the rule it was silently overwritten by this line, which sets both block
     margins — the declaration was there, the cascade threw it away, and the
     hero deployed looking exactly as it had. */
  margin-block: 1.625rem 0.75rem;
}
/* The auto end margin is what splits the home hero, and it has to live in this
   shorthand rather than as a margin-bottom above it. Written separately it was
   silently overwritten by a later shorthand setting both block margins: the
   declaration was there, the cascade threw it away, and the hero deployed
   looking exactly as it had. */
.hero--split .hero__title { margin-block: 1.625rem auto; }
/* Bigger and narrower than the old lede: theirs runs to 26px in a 570px
   column, ours ran to 21px across 52ch, which at that size is nearer 640px.
   The wider, smaller line read as body copy set under a headline rather than
   as the second voice in the composition. */
.hero__lede {
  font-size: clamp(1.125rem, 2vw, 1.625rem);
  line-height: 1.4;
  color: var(--text-invert);
  max-width: min(100%, 570px);
  margin: 0;
}
.hero__kicker {
  /* Sans, not the display serif. The kicker answers the headline
     rather than continuing it, and in the same voice at a smaller size it
     read as a stray second line of the same sentence. */
  font-family: var(--body);
  font-weight: 500;
  font-size: var(--t-kicker);
  line-height: 1.25;
  letter-spacing: -0.014em;
  color: var(--marker);
  max-width: 34ch;
}
.hero__cta { margin: 1.1rem 0 0; }
/* In the split hero the button is a flex item on the floor bar, not a block
   trailing the lede, so the stacking margin comes off. */
.hero--split .hero__cta { margin: 0; flex-shrink: 0; }
/* The index and the floor bar were both pinned to the same right edge, so they
   were never not going to meet. Measured: 56px of clearance at 1512, 20px at
   1440, and from 1280 down they overlap by exactly 16px — the index's own
   width — because below the shell width the bar's right edge and the index's
   are the same pixel. The reference has this collision too, which is where it
   came from. The bar now reserves a lane: 16px for the mark and 24px of air.
   Below the breakpoint the index is hidden and the lane closes. */
.hero:has(.hero__index) .hero__bottom,
.hero:has(.hero__index) .hero__credits { padding-right: 2.5rem; }
@media (max-width: 60rem) {
  .hero:has(.hero__index) .hero__bottom,
  .hero:has(.hero__index) .hero__credits { padding-right: 0; }
}
/* The index. A standing mark in the corner of a hero, set on its side.

   It carries a fact rather than a number. The reference build runs
   "SANTIXS / 01" on every page, which is a counter for a set nobody is
   counting — the same dead label the eyebrow rule already rejected. Each page
   passes its own value or gets none at all, so it stays a mark and never
   becomes furniture.

   aria-hidden because a screen reader reading a rotated string in the corner
   of every hero is noise, and nothing here is said only here. */
.hero__index {
  position: absolute;
  right: var(--gutter);
  bottom: clamp(1.25rem, 3vw, 2rem);
  z-index: 2;
  margin: 0;
  writing-mode: vertical-rl;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--slate);
  pointer-events: none;
}
/* Below the breakpoint the hero is short and the corner is where the call to
   action sits. The mark comes off rather than competing with it. */
@media (max-width: 60rem) {
  .hero__index { display: none; }
  /* The rebrand's own phone scale. 8.2vw keeps climbing on a narrow screen and
     would put a 76px word past the edge of a 390px frame. */
  .hero__title { font-size: clamp(3.25rem, 16vw, 4.75rem); }
  .hero { min-height: 72svh; }
  .hero--split { min-height: 80svh; }
}
/* The credits line. A case study's hero closes on a rule with the client's
   mark at one end and what the company is at the other, which is the reference
   build's .case-meta and the right shape for it: the logo used to sit above
   the headline where it competed with the eyebrow for the top of the page, and
   the descriptor trailed underneath the lede as a loose line of small type.
   Together on a rule they read as a caption to the whole hero.

   Rendered whenever a hero carries either one. With only a descriptor, which
   is what the two research pages have, space-between leaves it at the left and
   the rule turns a stray byline into a footer. */
.hero__credits {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  width: 100%;
  margin-top: clamp(1.75rem, 4vw, 3.125rem);
  padding-top: 1.5rem;
  border-top: 1px solid var(--rule-invert);
}
.hero__mark {
  max-width: 11.25rem;
  max-height: 3rem;
  width: auto;
  height: auto;
  object-fit: contain;
  /* Same knockout the client marks take in the logo strip: black then lifted
     to white, so a mark supplied dark-on-transparent and one supplied
     light-on-transparent both come out as one silhouette. */
  filter: grayscale(1) brightness(0) invert(1);
}
/* One exemption, and it is the file's fault rather than the filter's.
   logo-servpro-chula-vista.png is a palette PNG with no tRNS chunk, which is
   to say it has no alpha at all: it is an opaque 81x28 green rectangle with
   the mark printed on it. brightness(0) takes the whole rectangle to black and
   invert(1) lifts the whole rectangle to white, so what rendered in the hero
   was a solid white block. grayscale+invert gives a grey block instead. There
   is no filter that finds a silhouette in an image that has no transparency.

   Shown as supplied until the asset is replaced. A green plate on the ink
   ground is not the treatment the others get, but it is at least the client's
   mark and it is legible, which the block was not. */
.hero__mark[src*="servpro"] { filter: none; }
.hero__meta {
  margin: 0;
  text-align: right;
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.04em;
  color: var(--slate);
}
@media (max-width: 44rem) {
  .hero__credits { flex-direction: column; align-items: flex-start; gap: 1rem; }
  .hero__meta { text-align: left; }
}
.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. */
/* The strip runs on the dark ground with every mark knocked to a white
   silhouette. The note below used to argue the opposite: that the band stayed
   light because the marks are a mix of dark-on- and light-on-transparent
   artwork and only a light ground carried both. That was true of a greyscale
   filter and is not true of this one. grayscale, then brightness(0), then
   invert(1) flattens any artwork to black and then lifts it to white, so both
   kinds of file come out the same and transparency is untouched.

   What it costs: a mark with internal colour becomes one solid shape. The
   torc asterisk and the tissue dots lose their inside. That is the trade for a
   row that reads as one row. */
.logo-strip {
  padding-block: clamp(2rem, 4vw, 3rem);
  background: var(--ink-deep);
  color: var(--text-invert);
}
/* The label sits over the marks, not beside them. In the rail it took 292px
   out of the row and left the marks in a 876px column that they had to wrap
   inside; over the top, the marks get the whole window. */
.logo-strip__head { margin-bottom: 1.25rem; }
.logo-strip__foot { margin-top: 1.25rem; }
.logo-strip .eyebrow { color: var(--slate); }

/* The viewport clips the travel.

   overflow, not overflow-x. When one axis is hidden and the other is visible
   the visible one computes to auto, which quietly turns this into a scroll
   container; on iOS that container is also a touch target, and a strip the
   width of the page that eats a horizontal swipe is a strip that reads as
   broken. Both axes hidden clips and nothing scrolls.

   The fade is two gradient overlays rather than a mask. A mask over an
   animated child forces WebKit to composite the masked layer every frame and
   it is a well-worn source of blank tiles and stalled animation on iOS. Two
   pseudo-elements painted in the ground colour look identical, cost nothing,
   and cannot fail. They are why --ink-deep is named here rather than read from
   --sink: this band sets its own ground and the overlay has to match it. */
.logo-strip__viewport {
  position: relative;
  overflow: hidden;
}
.logo-strip__viewport::before,
.logo-strip__viewport::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  z-index: 2;
  width: clamp(2rem, 6vw, 5rem);
  pointer-events: none;
}
.logo-strip__viewport::before { left: 0; background: linear-gradient(90deg, var(--ink-deep), transparent); }
.logo-strip__viewport::after { right: 0; background: linear-gradient(270deg, var(--ink-deep), transparent); }
.logo-strip__track {
  display: flex;
  width: max-content;
  /* Five seconds a mark, so the pace is the same whatever the list holds. */
  animation: logo-marquee calc(var(--logo-count, 8) * 5s) linear infinite;
  /* Both lines are for iOS. A long-running translate on an uncomposited layer
     is repainted rather than moved, which on a phone is a stutter at best and
     a blank band at worst; these promote it once and leave it there. */
  will-change: transform;
  backface-visibility: hidden;
}
/* Pausing on hover is not decoration. The marks are the only third-party proof
   on the page and a reader who wants to look at one has to be able to stop it. */
.logo-strip__viewport:hover .logo-strip__track,
.logo-strip__viewport:focus-within .logo-strip__track { animation-play-state: paused; }
@keyframes logo-marquee { to { transform: translateX(-50%); } }
/* Reduced motion gets the row it had before: one copy, wrapping, centred and
   still. Turning the animation off alone would leave a track twice as wide as
   the window with half of it clipped and no way to reach it. */
/* Reduced motion gets the row it had before: one copy, wrapping, centred and
   still. This is not an edge case on a phone — Reduce Motion is one of the
   most commonly enabled accessibility settings on iOS, and anyone who has it
   on will see a still row rather than a broken one. */
@media (prefers-reduced-motion: reduce) {
  .logo-strip__track { animation: none; width: 100%; flex-wrap: wrap; justify-content: center; will-change: auto; }
  .logo-strip__track > [aria-hidden='true'] { display: none; }
  .logo-strip__viewport::before,
  .logo-strip__viewport::after { content: none; }
}
.logo-strip__list {
  display: flex;
  align-items: center;
  gap: 1.5rem 2.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
/* Inside the track the row must not wrap, and it carries the gap across the
   join: without the trailing pad the two copies butt together and the loop
   shows as a stutter once every pass. Outside the track it is the ordinary
   wrapping row it always was. */
.logo-strip__track .logo-strip__list { flex-wrap: nowrap; padding-right: 2.5rem; }
.logo-strip__static .logo-strip__list { flex-wrap: wrap; justify-content: center; }
@media (prefers-reduced-motion: reduce) {
  .logo-strip__track .logo-strip__list { flex-wrap: wrap; justify-content: center; padding-right: 0; }
}
.logo-strip__list img {
  height: clamp(1.4rem, 3vw, 2.1rem);
  width: auto;
  object-fit: contain;
  filter: grayscale(1) brightness(0) invert(1);
  opacity: 0.62;
  transition: opacity 0.15s ease;
}
.logo-strip__list img:hover { opacity: 1; }

/* Client marks still carry more weight than the affiliations: these are the
   companies the work was done for, and they are the only third-party proof on
   the page. They keep the same knockout so the row holds together, and get it
   at full opacity rather than held back. */
.logo-strip--full .logo-strip__list img { opacity: 1; }
/* Two exceptions, by name. A knockout flattens everything to one silhouette,
   which is the point, except where a mark has an enclosed counter that was
   only ever separated by colour: LifeScience's block and Passportage's globe
   came out as holes with the ground showing through. grayscale then invert
   lifts a dark mark to light while keeping the luminance differences inside
   it, so the counters survive. A small divergence from the reference build,
   made because those two marks were unreadable without it. */
.logo-strip__list img[src*="lifescience"],
.logo-strip__list img[src*="passportage"] { filter: grayscale(1) invert(1); }
.logo-strip__note { 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(--marker); }
.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(--marker);
}
.card__title {
  grid-column: 1;
  font-size: var(--t-index);
  line-height: 1.08;
  max-width: 17ch;
  color: var(--text);
  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(--marker);
  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; }
}

/* ── Team shapes ──────────────────────────────────────────────────────────
   Three ways to buy this work, drawn. Not a chart: nothing here is measured,
   so nothing here has a scale. The claim is structural and so is the drawing.

   The aspect is locked to the viewBox rather than the height being set loose,
   for the reason recorded at .ink--draw: a fluid height scales the two axes
   differently, the stroke distorts, and the usual cure for that is the one
   property that breaks the draw-on dash in Blink. Matching the ratio means the
   question never comes up. */
.teams {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1.25rem, 3vw, 2.5rem);
  margin: clamp(2rem, 4vw, 3rem) 0 0;
  padding: 0;
  list-style: none;
}
.teams__cell { display: flex; flex-direction: column; align-items: flex-start; }
.teams__svg {
  display: block;
  width: 100%;
  aspect-ratio: 120 / 62;
  overflow: visible;
  margin-bottom: clamp(0.9rem, 2vw, 1.4rem);
}
/* The nodes are hollow by default and one of them is filled. That single fill
   is the whole diagram: it is the person whose name is on the outcome, and the
   roster panel deliberately has none. */
.teams__node {
  fill: none;
  stroke: var(--marker-quiet);
  stroke-width: 2;
}
.teams__node--lit { fill: var(--marker); stroke: var(--marker); }
.teams__cap, .teams__link { fill: none; stroke-width: 2; stroke-linecap: round; }
.teams__label {
  margin: 0;
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 88%;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--marker);
}
.teams__note {
  margin: 0.5rem 0 0;
  max-width: 26ch;
  color: var(--text-soft);
  font-size: var(--t-small);
}
/* One column on a phone, and the drawing comes down with it: at full width a
   62-unit-tall figure becomes a band of empty space with three dots in it. */
@media (max-width: 44rem) {
  .teams { grid-template-columns: minmax(0, 1fr); gap: 1.75rem; }
  .teams__svg { max-width: 13rem; }
}

/* ── The lattice ──────────────────────────────────────────────────────────
   A grid whose cells are separated by single hairlines rather than by gaps.
   The container draws its top and left edge, each cell draws its right and
   bottom, and the shared edges collapse into one line instead of doubling.
   The alternative, a border on all four sides of every cell, draws every
   interior line twice and reads as a table.

   This is the one structural device the site did not have. A stack of cards
   says "here are four things, in order". A lattice says "here are four things
   that partition one surface", which is a different claim and the right one
   for a set with no sequence: the four builds, the two operating models, the
   fit and unfit columns.

   Every colour here reads a ground-following token, so the same lattice is
   correct on paper and on an ink band with no override, and stays correct
   when the default ground flips.

   --lattice-cols sets the column count and --lattice-min the floor height.
   The title carries margin-top: auto, so it sits on the bottom edge with the
   label at the top. That works with or without a floor, because grid already
   stretches every cell to the height of its row: the floor only decides how
   tall the row is when the content is short. Set it for a set of four builds
   that should feel substantial, leave it off for a dense set. */
.lattice {
  display: grid;
  grid-template-columns: repeat(var(--lattice-cols, 2), minmax(0, 1fr));
  border-top: 1px solid var(--marker-quiet);
  border-left: 1px solid var(--marker-quiet);
}
.lattice__cell {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  min-height: var(--lattice-min, 0);
  padding: clamp(1.25rem, 2.4vw, 2.125rem);
  border-right: 1px solid var(--marker-quiet);
  border-bottom: 1px solid var(--marker-quiet);
}
.lattice__label {
  margin: 0;
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 88%;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--marker);
}
.lattice__title {
  margin: auto 0 0.5rem;
  /* Same reason as the spread. In a three-column lattice the text box is 225px
     and --t-display broke these to three and four lines. */
  font-size: var(--t-set);
  line-height: 1.06;
  max-width: 18ch;
}
.lattice__body {
  margin: 0;
  max-width: 42ch;
  color: var(--text-soft);
}
.lattice__cue {
  margin-top: 1.6rem;
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 88%;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--marker);
}
.section--dark .lattice__body { color: var(--slate); }

/* A lattice of disclosures. The cell keeps its own borders and padding; the
   summary carries the column layout so the plus can sit hard against the top
   right of the square rather than trailing the copy. */
.lattice__cell--open { padding: 0; }
.lattice__cell--open > summary {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-content: start;
  gap: 0 1rem;
  min-height: var(--lattice-min, 15rem);
  padding: clamp(1.25rem, 2.4vw, 2.125rem);
  cursor: pointer;
  list-style: none;
}
.lattice__cell--open > summary::-webkit-details-marker { display: none; }
.lattice__cell--open > summary > .lattice__label,
.lattice__cell--open > summary > .lattice__title,
.lattice__cell--open > summary > .lattice__body { grid-column: 1; }
/* The title is pushed to the floor of the square, the same move the plain
   lattice makes, so four cells of different lengths still line their titles up
   against one edge. */
.lattice__cell--open > summary > .lattice__title { margin-top: auto; }
.lattice__cue--mark {
  grid-column: 2;
  grid-row: 1;
  margin: 0;
  justify-self: end;
  font-family: var(--display);
  font-size: 1.375rem;
  font-weight: 400;
  line-height: 1;
  text-transform: none;
  letter-spacing: 0;
  color: var(--marker);
  transition: transform var(--dur-fast) var(--ease);
}
.lattice__cell--open[open] .lattice__cue--mark { transform: rotate(45deg); }
/* The square is only a square while it is shut. Open, the min-height is left
   holding 150px of nothing between the claim and the copy it introduces. */
.lattice__cell--open[open] > summary { min-height: 0; }
/* An open cell takes the whole row. Stretching one square into a column
   instead would drag its neighbour down to match and leave a hole beside the
   thing being read. */
.lattice--opens > .lattice__cell--open[open] { grid-column: 1 / -1; }
.lattice__detail {
  padding: 0 clamp(1.25rem, 2.4vw, 2.125rem) clamp(1.5rem, 3vw, 2.375rem);
  display: grid;
  gap: 0.9rem;
}
.lattice__detail > p { margin: 0; max-width: 68ch; color: var(--text-soft); }
.section--dark .lattice__detail > p { color: var(--slate); }
a.lattice__cell:hover, .lattice__cell--open > summary:hover { background: var(--lift); }

/* The linked cell. The lift is the whole affordance, so it is the only thing
   that moves: no translate, no shadow, no border colour change. */
a.lattice__cell {
  text-decoration: none;
  color: inherit;
  transition: background var(--dur-fast) var(--ease);
}
a.lattice__cell:hover,
a.lattice__cell:focus-visible { background: var(--lift); }

/* Collapsing sets grid-template-columns directly and does NOT re-set
   --lattice-cols, which is the obvious way to write this and does not work.
   The component passes a non-default column count as an inline style, and an
   inline custom property outranks any stylesheet rule for that element, media
   query or not. Setting the variable here is silently ignored and a four
   column lattice stays four columns on a phone, with the headings drawing on
   top of each other. Setting the resolved property instead sidesteps the
   variable entirely.

   The border maths carries over unchanged at any count: the container draws
   top and left, each cell draws right and bottom, so a single stack is the
   degenerate case of the same grid rather than a second layout.

   The floor height is dropped on the way down. A 19rem cell holding one line
   of text is a hole on a phone, and with the floor gone the auto margin on
   the title has nothing to push against, so an explicit top margin restores
   the space it was providing. */
@media (max-width: 60rem) {
  .lattice { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 44rem) {
  .lattice { grid-template-columns: minmax(0, 1fr); }
  .lattice__cell { min-height: 0; }
  .lattice__title { margin-top: 1.75rem; }
}

/* ── 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));
  /* Rows declared on the track, not on each link, so every link's name, copy and
     failure line share one baseline. Each .chain__link is its own grid, and an
     independent 1fr cannot reconcile with its neighbours: at five links the
     copy happened to be even and they aligned by luck, at six they did not. */
  grid-template-rows: auto auto 1fr auto;
  column-gap: clamp(0.75rem, 1.6vw, 1.25rem);
  padding: 2.25rem 0 0;
  position: relative;
}
/* The same straight rule the operating model runs on. This used to be a drawn
   SVG thread with filled nodes sitting on it, and next to .method on the home
   page it read as a second, louder system doing the same job. One rule, one
   node shape, across both. */
.chain::before {
  content: '';
  position: absolute;
  top: 0.375rem;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--rule);
}
.section--dark .chain::before { background: var(--rule-invert); }
/* 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;
  grid-row: span 4;
  gap: 0.35rem;
}
@supports (grid-template-rows: subgrid) {
  .chain__link { grid-template-rows: subgrid; }
}
/* 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: -2.25rem;
  left: 0;
  width: 0.8125rem;
  height: 0.8125rem;
  margin-top: -0.03125rem;
  border: 2px solid var(--marker);
  border-radius: 50%;
  background: var(--ink);
}
.section--alt .chain__link::before { background: var(--paper); }
.section--paper .chain__link::before { background: var(--paper); }
.chain__index {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.12em;
  color: var(--marker);
}
.chain__name {
  font-family: var(--display);
  font-weight: 400;
  font-size: var(--t-card);
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--text);
  margin: 0.25rem 0 0;
}
.chain__what { margin: 0.4rem 0 0; font-size: var(--t-small); line-height: 1.55; color: var(--text-soft); }
.chain__fail {
  font-size: var(--t-small);
  color: var(--alarm);
  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__name { 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: 0.375rem; 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.35rem; left: -1.9rem; margin-top: 0; }
}

/* ── 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);
  border: 1px solid var(--rule);
}
.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; }
/* Announced by measurement, not by viewport. A media query cannot know how
   wide the column ended up: gated at 60rem the hint claimed a sideways
   scroll at 768px where the table fits, and stayed silent at 1024 where the
   rail track leaves the column 18px short of the table's minimum. */
.matrix-hint.is-on { display: block; }
.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(--marker);
  border-bottom-width: 2px;
}
.matrix thead th:first-child { color: transparent; }
.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(--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(--alarm);
  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);
  border: 1px solid var(--rule);
}
.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 {
    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(--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. */
/* The rows are declared here, on the bar, and both the part and the link
   inherit them through subgrid. They used to be declared on .spread__link,
   which made each column its own independent grid: the cues landed on one
   baseline because the note row absorbed the slack, but the notes themselves
   did not. A two-line title and a three-line title put their paragraphs 49px
   apart, and the bar that is supposed to read as one object had a step in it.

   Subgrid rather than display: contents on the link. Both align the rows;
   display: contents has a long history of dropping the element it is on out
   of the accessibility tree, and here that element is the link that makes the
   whole segment clickable. */
.spread {
  display: grid;
  grid-template-columns: repeat(var(--parts, 3), minmax(0, 1fr));
  grid-template-rows: auto auto 1fr auto;
  gap: 3px;
}
.spread__part {
  display: grid;
  grid-row: span 4;
  grid-template-rows: subgrid;
}
.spread__link {
  display: grid;
  grid-row: span 4;
  grid-template-rows: subgrid;
  row-gap: 0.65rem;
  padding-top: 1.1rem;
  /* The seam between segments is 3px and stays 3px: it is a bar divided, not
     three columns spaced apart. So the breathing room goes inside the segment
     instead. Padding sits within the border box, so the marker rule still runs
     the full width of the column and only the words move in. Without it, at
     display size on a 336px column, the last word of one title finishes about
     eight pixels from the first word of the next. */
  padding-right: clamp(0.9rem, 2vw, 1.75rem);
  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);
}
/* Its own size, between --t-card and --t-display, and measured on the live
   page rather than chosen. At --t-card the three services were set smaller
   than the headings above them, on the section carrying the page's argument.
   At --t-display they were too big for the column the rail leaves: 48px broke
   every title to four lines and stranded "and" alone on the third.

   Behind the 10rem rail the text box is 264px. Measured in Chrome with real
   Archivo at 118% width: 48 and 44 give four lines, 42 to 38 give three, 36
   gives two clean ones that break where the phrase breaks — "pipeline you /
   go and create", "pipeline that / comes to you", "the team that / runs
   both". Three titles, two lines each, parallel. So 2.25rem is the ceiling.

   Do not measure this in a browser without Archivo loaded. The fallback is
   narrower and does not honour font-stretch, so it fits a line the real face
   does not, and every number above comes out wrong by one line. */
.spread__title {
  font-size: var(--t-set);
  line-height: 1.02;
  max-width: 11ch;
  transition: color var(--dur-fast) var(--ease);
}
.spread__note { font-size: var(--t-small); color: var(--text-soft); max-width: 26ch; }
.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(--marker);
  transition: transform var(--dur-fast) var(--ease);
}
.spread__part:hover .spread__title { color: var(--marker); }
.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); grid-template-rows: none; gap: 0; }
  /* The note keeps a 26ch measure on the desktop bar because it is sharing the
     width with two others. Stacked, it is not, and the cap was still there:
     224px of copy in a 373px column on a 414px phone, four lines where two
     would do. Measured on the live page in an iframe at 414px, releasing it
     took the whole bar from 738px to 642px. A measure set for one layout is
     not a measure, it is a leftover. */
  .spread__note { max-width: none; }
  /* Both spans have to go back to auto. Left as span 4 against a track list
     of none, the three parts would be placed into implicit rows four at a
     time and the bar would grow eight empty rows. */
  .spread__part { grid-row: auto; grid-template-rows: none; }
  .spread__link { grid-row: auto; grid-template-rows: none; padding-block: 0.85rem 1.3rem; }
  .spread__title { max-width: 16ch; }
}

/* ── 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.14: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(--alarm);
}
.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: var(--rail-cols);
  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(--marker);
}
.numbered__icon { width: 2rem; height: 2rem; opacity: 0.9; }
.numbered__term {
  font-family: var(--display);
  font-weight: 400;
  font-size: var(--t-card);
  line-height: 1.14;
  letter-spacing: -0.018em;
  color: var(--text);
  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(--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: 400;
  font-size: var(--t-stat);
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--text);
  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(--alarm); }
.stat--claret .stat__from { color: var(--alarm); opacity: 1; }
.stat--claret .stat__to,
.stat--claret.stat--reduced .stat__to { color: var(--text); }

.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(--text); }
.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(--marker);
  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(--text); }
.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(--alarm); 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(--marker); }
.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(--marker);
}
.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(--text);
}
/* 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(--marker);
}
.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) auto;
  gap: 0.75rem clamp(1.5rem, 3vw, 2.5rem);
  align-items: start;
  cursor: pointer;
  list-style: none;
}
.layer > summary::-webkit-details-marker { display: none; }
/* One affordance, everywhere. A plus that turns into a cross — the same
   control the phone menu uses and the same one on a metric card — so a reader
   learns it once and never has to read a label to know what a thing does.
   It replaced worded cues ("what it looked like", "how it was built"), which
   were four different sentences doing one job.

   The glyph is aria-hidden and every summary carries an sr-only label instead,
   so what a screen reader announces is still the words. A bare plus is a
   control with no name, and that is not a trade worth making. */
.layer__cue,
.case-metrics__cue {
  font-family: var(--display);
  font-size: 1.375rem;
  font-weight: 400;
  line-height: 1;
  color: var(--marker);
  transition: transform var(--dur-fast) var(--ease);
}
.layer__cue { grid-column: 3; grid-row: 1; align-self: start; justify-self: end; }
.layer[open] > summary .layer__cue { transform: rotate(45deg); }
.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(--text);
}
.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; }
/* A disclosure with no title row: the cue is the whole summary, so the
   two-column grid collapses and the cue takes the one column there is. */
.layer--plain > summary { grid-template-columns: minmax(0, 1fr) auto; }
.layer--plain > summary .layer__cue { grid-column: 2; }
.layer--plain > .layer__body { grid-template-columns: minmax(0, 1fr); }
.layer--plain > .layer__body > * { grid-column: 1; }
.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(--marker);
  text-decoration: none;
  border-bottom: 1px solid var(--marker);
  padding-bottom: 0.2rem;
}
.jump-nav__list a:hover { color: var(--text); border-bottom-color: var(--text); }

.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-size: var(--t-ui);
  letter-spacing: -0.01em;
  color: var(--text);
  cursor: pointer;
  list-style: none;
}
.faq__question:hover { color: var(--marker); }
.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
   -------------------------------------------------------------------------- */

/* The amber band.
 *
 * The site's action colour, used as a ground for the one band whose entire job
 * is the action. It replaces a photograph with a scrim: a picture behind the
 * last thing a reader sees competes with the only decision on the page, and
 * this way the band is unmistakable at a glance from anywhere on the site.
 *
 * Colour follows the ground, as everywhere else. Nothing below names a
 * literal: the tokens are re-pointed once here and every rule, cue and rule
 * line inherits. Ink on this amber measures 9.3:1, so the type clears AAA at
 * every size the band uses. */
.cta-band {
  background: var(--amber);
  color: var(--ink);
  --text: var(--ink);
  --text-soft: color-mix(in srgb, var(--ink) 72%, transparent);
  --marker: var(--ink);
  --marker-quiet: color-mix(in srgb, var(--ink) 30%, transparent);
  --rule: color-mix(in srgb, var(--ink) 26%, transparent);
}
.cta-band .display { color: var(--ink); }

.cta-band__inner {
  display: grid;
  grid-template-columns: var(--rail-cols);
  column-gap: var(--rail-gap);
  row-gap: clamp(1.5rem, 3vw, 2.25rem);
  align-items: start;
}
.cta-band__label {
  grid-column: 1;
  margin: 0;
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink);
}
.cta-band__body { grid-column: 2; }
.cta-band__title {
  margin: 0;
  font-size: var(--t-display);
  line-height: 0.98;
  letter-spacing: -0.03em;
  max-width: 16ch;
}
/* The gap the reference build leaves between the headline and the link is the
   whole composition: the ask, then room to decide, then the way in. */
.cta-band__action { margin: clamp(2.5rem, 7vw, 5.5rem) 0 0; }

@media (max-width: 60rem) {
  .cta-band__inner { grid-template-columns: minmax(0, 1fr); }
  .cta-band__label, .cta-band__body { grid-column: 1; }
  .cta-band__title { max-width: none; }
}

/* --------------------------------------------------------------------------
   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: 400;
  font-size: var(--t-card);
  line-height: 1.16;
  letter-spacing: -0.018em;
  color: var(--text);
  margin-top: 2.75rem;
}
.prose h3 {
  font-family: var(--display);
  font-weight: 400;
  font-size: var(--t-ui);
  color: var(--text);
  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(--marker); 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(--text);
}
.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(--text); }
.tabs__tab--active { color: var(--text); 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(--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: 400;
  font-size: var(--t-card);
  line-height: 1.14;
  letter-spacing: -0.018em;
  color: var(--text);
}
.post-card__title a { text-decoration: none; }
.post-card__title a:hover { color: var(--marker); }
.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: var(--rail-cols);
  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(--marker);
  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(--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(--marker); }

@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-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(--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(--marker); }

/* --------------------------------------------------------------------------
   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(--text); }
.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: var(--rail-cols);
  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-size: var(--t-kicker);
  line-height: 1.25;
  letter-spacing: -0.014em;
}

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

.colophon {
  padding-top: var(--band-tight);
  background: var(--ink-deep);
  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; }
/* The linked note runs a sentence plus a mono arrow link, which does not fit a
   44ch measure without breaking the link across three lines.

   The tight top margin here was for a second note sitting under a first one, as
   a pair. The first note is gone, so this is now the only thing between the
   phase frames and the end of the module and it needs the separation the rest of
   the page uses between a block and the note about it. */
.phases__note--wide { max-width: 62ch; margin-top: clamp(2rem, 5vh, 3.5rem); }
.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(--text); }
.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(--marker);
  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 the old #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 normalizes the
   dash maths to 1, so a long label and a short one trace in the same time.

   Never put vector-effect: non-scaling-stroke on a path that draws this way.
   Blink drops the pathLength normalization when it is on, the dash falls back
   to raw user units, and the stroke gets cut off partway. Safari is unaffected,
   so it renders correctly there and breaks only in Chrome. That is what went
   wrong with the hero arrow.

   The corollary, since the usual reason to reach for non-scaling-stroke is a
   stroke distorting under a fluid height: give the SVG an aspect-ratio matching
   its own viewBox instead. Both axes then scale together, the stroke never
   distorts, and the question never comes up. */
.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); }

/* ---- Arrow links --------------------------------------------------------
   The drawn underline came out here. At this size it did not read as drawn,
   it read as a wobbly border, and it was the one place the ink language was
   working against itself: a mark small enough that the tremor looks like a
   rendering fault rather than a hand. A plain rule does the same job honestly.
   The arrow stays a glyph and still steps on hover — that is the affordance
   the drawn line was never carrying anyway. */
.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='%23e5af2f' 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.25:1 on the ink
     ground, 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='%23e5af2f' stroke-width='2.6' stroke-linecap='round'/></svg>");
}


.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.25:1 on the ink ground. 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); }

/* The chain's number carries no circle at any width. Two marks were landing on
   every link — the node on the rule and a drawn ring round the number — and the
   desktop rule that was meant to suppress the ring lost to a more specific dark
   ground rule, so both shipped. The node is the mark that means something: it is
   a stop on a line, and the line is the argument. .method__num sets its number
   plain for the same reason. */
.chain__index,
.section--dark .chain__index { background-image: none; padding-inline: 0; }

/* ---- 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; }


/* ---- 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(229, 175, 47, 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(229, 175, 47, 0.085);
}
.section--dark .scenery__blob,
.hero .scenery__blob,
.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,
.scenery__contour path {
  fill: none;
  stroke: rgba(140, 47, 30, 0.26);
  stroke-linecap: round;
}
.section--dark .scenery__contour path,
.hero .scenery__contour path,

@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. */
/* The lean is capped by the margin it has to lean into.
   .section__inner IS the shell, so its border-box is the full viewport width
   whenever the viewport is narrower than --shell. Shifting it right by a flat
   2.2vw then pushed its box past the right edge and the document grew a
   horizontal scrollbar: nothing clipped, because the shell's own padding kept
   the content in, but the page scrolled sideways. That was already true below
   1216px and raising --shell to 1368px widened the band it happens in to
   include ordinary 1280px laptops, which is how it surfaced.

   min() against half the leftover margin means the lean is only ever taken out
   of space that exists; max(0px, ...) keeps it from going negative and
   becoming a tuck when there is no margin at all. */
.section--lean  > .section__inner { left: max(0px, min(clamp(0px, 2.2vw, 2.25rem), (100vw - var(--shell)) / 2)); }
.section--lean2 > .section__inner { left: max(0px, min(clamp(0px, 4vw, 4rem), (100vw - var(--shell)) / 2)); }
.section--tuck  > .section__inner { left: clamp(-1.75rem, -2vw, 0px); }


/* ---- The structural mark -------------------------------------------------
   There used to be a drawn hairline here: every editorial divider on the site
   — the card stack, the numbered items, the proof rows, the FAQ, the terms,
   the receipts — had its border zeroed out and a wobbled SVG stroke painted
   in its place. Two dozen selectors, forty wobbles down a page.

   Cut. A rule between two list items is not saying anything; it is separating
   two list items, and a wobble on it is a texture, not a thought. Worse, when
   everything on the page is drawn, nothing on the page reads as drawn, which
   is exactly the budget the real marks need. The dividers are straight lines
   again, off --rule, so they follow the ground like every other colour.

   What stays is the heavier mark: a 2.8px stroke, used where the line is
   doing a job a border cannot — over each part of a spread, over a term
   list, under a caption. Two colours because a data URI cannot read a custom
   property, and a mark on the ink ground has to carry differently. */
:root {
  /* 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='%23e5af2f' stroke-width='2.8' stroke-linecap='round'/></svg>");
}

/* Boxes rather than rules, so they take a real outline. */
.ninebox__grid, .shot {
  border: 1px solid var(--rule);
}
.jump-nav__list a, .proof-row__summary {
  border-bottom-color: transparent !important;
  background-repeat: no-repeat;
  background-position: left bottom;
  background-size: 100% 4px;
}
.jump-nav__list a, .proof-row__summary { background-image: var(--drawn-mark); }
.section--dark .proof-row__summary { background-image: var(--drawn-mark-amber); }
/* 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); }


/* ---- Two operating models -----------------------------------------------
   Two panels that meet, one on each ground. The grounds are the comparison:
   paper against ink says "two options" before a word is read, and because they
   are opposites rather than a highlight, neither is recommended. Colour follows
   the ground inside the ink panel; nothing here names a literal. */
.models {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  border: 1px solid var(--rule);
}
.models__panel {
  display: flex;
  flex-direction: column;
  padding: clamp(1.75rem, 4vw, 3.25rem);
}
/* --lift, not --paper. On the alt ground --paper is only ten points lighter than
   the section, so the left panel disappeared into it and the block read as
   starting at the ink column. --lift is the site's raised surface and it doubles
   the step off the ground. */
.models__panel--paper { background: var(--lift); }
.models__panel--ink {
  background: var(--ink);
  --text: var(--text-invert);
  --text-soft: rgba(238, 240, 246, 0.72);
  --marker: var(--amber);
  --rule: var(--rule-invert);
  color: var(--text);
}
.models__kicker {
  margin: 0;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--marker);
}
.models__name {
  margin: clamp(1.5rem, 3.5vw, 2.5rem) 0 0;
  font-size: var(--t-set);
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: var(--text);
}
.models__intro {
  margin: 0.9rem 0 0;
  font-size: var(--t-ui);
  line-height: 1.55;
  color: var(--text-soft);
  max-width: 42ch;
}
.models__when {
  margin: clamp(1.75rem, 4vw, 2.75rem) 0 0;
  padding-top: clamp(1.25rem, 3vw, 1.9rem);
  border-top: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.12em;
  color: var(--marker);
}
.models__list,
.dash-list {
  margin: 0.9rem 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
}
/* The dash is the marker, drawn in type rather than as a bullet glyph: a round
   bullet is the one mark on a page nobody chose. */
.models__list li,
.dash-list li {
  position: relative;
  padding: 0 0 0.85rem 1.6rem;
  margin-bottom: 0.85rem;
  border-bottom: 1px solid var(--rule);
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--text-soft);
}
.models__list li:last-child,
.dash-list li:last-child { border-bottom: 0; margin-bottom: 0; padding-bottom: 0; }
/* Standing on its own rather than inside a panel, so it is set as statements
   rather than as a list: card size, no marker, no indent.
 
   The dash came off because there is nothing here for it to do. Four items
   with a hairline between each already read as four separate things, and at
   this size the reader is meant to stop on the one that is theirs, not scan a
   set. A marker in front of each line makes them a list of points about one
   subject; without it they are four sentences, each of which is somebody's
   whole situation. */
/* Set to the home page's why-now rows: same size, same face, same rhythm.

   That module states five situations a reader picks themselves out of, and
   this one states four. They are the same object, so they are set the same
   way — --t-card in the display face, a full-width hairline over each, and the
   why-now row's padding either side of it. What was here before was a list of
   small grey points; at this weight each line is somebody's whole situation.

   No measure cap. These are single sentences and the column is theirs; a 46ch
   cap wrapped every one of them to three lines and turned the block back into
   prose. */
.dash-list { max-width: none; }
.dash-list li {
  padding: clamp(1.4rem, 3vw, 2.1rem) 0;
  margin: 0;
  border-top: 1px solid var(--rule);
  border-bottom: 0;
  font-family: var(--display);
  font-size: var(--t-card);
  line-height: 1.25;
  color: var(--text);
}
.dash-list li:last-child { padding-bottom: clamp(1.4rem, 3vw, 2.1rem); }
.dash-list li::before { content: none; }
.models__list li::before {
  content: '\2014';
  position: absolute;
  left: 0;
  color: var(--marker);
}
/* Pushed to the floor so both panels close on the same line whatever their
   conditions run to. */
.models__foot {
  margin: clamp(1.75rem, 4vw, 2.75rem) 0 0;
  margin-top: auto;
  padding-top: clamp(1.75rem, 4vw, 2.75rem);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
}
.models__foot-label {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.models__foot-line { font-size: clamp(1.0625rem, 1.6vw, 1.3125rem); line-height: 1.15; text-align: right; color: var(--text); }
/* Across the floor of both panels, on the ink ground whatever the panels are,
   because the point is that it is the same on either side. */
.models__same {
  grid-column: 1 / -1;
  margin: 0;
  padding: clamp(1.1rem, 2.6vw, 1.6rem) clamp(1.75rem, 4vw, 3.25rem);
  display: flex;
  align-items: baseline;
  gap: clamp(1.25rem, 4vw, 3.5rem);
  flex-wrap: wrap;
  background: var(--ink-deep);
  --text-soft: rgba(238, 240, 246, 0.72);
  --marker: var(--amber);
}
.models__same-label {
  flex: none;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--marker);
}
.models__same-line { font-size: var(--t-small); line-height: 1.5; color: var(--text-soft); }
@media (max-width: 54rem) {
  .models { grid-template-columns: minmax(0, 1fr); }
}



/* ---- The split proof -----------------------------------------------------
   A controlled test where the two grounds are the before and after. Same
   paper-against-ink logic as the operating models: the comparison is legible
   before a word is read, and neither side is a highlight of the other.

   The constants strip is not decoration. The claim is that the break sat at one
   link and not at the links either side of it, so the strip names the ones that
   did not move, in the same words the chain uses for them. */
/* --rule does not follow the ground: it stays paper cream everywhere, so a
   component that wants the quiet hairline on ink has to say so. Re-pointed for
   the whole subtree rather than at each of the six borders inside, and the
   after panel re-points it again for itself. */
.section--dark .psplit { --rule: var(--rule-invert); }
.psplit { border: 1px solid var(--rule); }
.psplit__constants {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 clamp(1.25rem, 4vw, 3rem);
  padding: clamp(0.9rem, 2.2vw, 1.35rem) clamp(1.1rem, 2.6vw, 1.9rem);
  border-bottom: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.psplit__constants-label { color: var(--marker); margin-right: auto; }
.psplit__constant { position: relative; }
.psplit__constant + .psplit__constant::before {
  content: '';
  position: absolute;
  left: calc(clamp(1.25rem, 4vw, 3rem) / -2);
  top: 0.1em;
  bottom: 0.1em;
  border-left: 1px solid var(--rule);
}
.psplit__pair {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: stretch;
}
.psplit__side {
  display: flex;
  flex-direction: column;
  padding: clamp(1.4rem, 3.2vw, 2.4rem);
}
.psplit__side--before { background: var(--lift); }
/* The after side carries the site's marker blue rather than ink, because the
   two sides are the same test and one of them worked. Tokens re-point; nothing
   below names a literal. */
.psplit__side--after {
  background: var(--indigo, #25318d);
  --text: var(--text-invert);
  --text-soft: rgba(238, 240, 246, 0.72);
  --marker: var(--amber);
  --rule: rgba(238, 240, 246, 0.28);
  color: var(--text);
}
.psplit__tag {
  margin: 0 0 clamp(1.1rem, 2.6vw, 1.75rem);
  padding-bottom: 0.7rem;
  border-bottom: 1px solid var(--rule);
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.psplit__tag-mode { color: var(--marker); }
.psplit__claim { margin: 0; font-size: var(--t-card); line-height: 1.1; color: var(--text); }
/* The figure carries .display, and .section--dark .display paints every display
   element the invert text colour — so on ink both figures went white and the
   module stopped saying which number was the win. Restated here, after that
   rule: the result takes the marker, the failure goes quiet. On paper the same
   hierarchy falls out of the tokens (indigo against the after panel's amber)
   and nothing below changes it. */
.section--dark .psplit__figure { color: var(--marker); }
.section--dark .psplit__side--before .psplit__figure { color: var(--text-soft); }
.psplit__figure {
  margin: 0.35rem 0 0;
  font-size: clamp(2.6rem, 5.4vw, 4.4rem);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--marker);
}
.psplit__figure-label {
  margin: 0.4rem 0 0;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.psplit__rows { margin: auto 0 0; padding: clamp(1.75rem, 4vw, 2.75rem) 0 0; }
.psplit__row {
  display: grid;
  grid-template-columns: minmax(0, 8rem) minmax(0, 1fr);
  gap: 1rem;
  padding: 0.7rem 0;
  border-top: 1px solid var(--rule);
}
.psplit__row dt {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.psplit__row dd { margin: 0; font-size: var(--t-small); color: var(--text); }
/* The hinge names what did not change and points at what did. */
.psplit__hinge {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.9rem;
  padding-inline: clamp(0.9rem, 2.4vw, 2rem);
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--marker);
  text-align: center;
}
.psplit__hinge-mark {
  display: grid;
  place-items: center;
  width: 2.6rem;
  height: 2.6rem;
  border-radius: 50%;
  background: var(--amber);
  color: var(--ink);
  font-size: var(--t-ui);
}
.psplit__changed {
  margin: 0;
  display: grid;
  grid-template-columns: minmax(0, 10rem) minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(1rem, 3vw, 2.5rem);
  align-items: start;
  padding: clamp(1.1rem, 2.6vw, 1.75rem) clamp(1.1rem, 2.6vw, 1.9rem);
  border-top: 1px solid var(--rule);
}
.psplit__changed-label {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--marker);
}
.psplit__changed-body { font-size: var(--t-small); color: var(--text-soft); }
.psplit__changed-verdict { font-size: var(--t-ui); line-height: 1.35; color: var(--text); }
/* The full table, one click down.
 *
 * The panels are the glance and this is the reading. A closed <details> is
 * still in the initial HTML, so the table is extractable whether or not anyone
 * opens it — the disclosure is a reading-order decision, not a hiding one.
 *
 * The summary is a row on the module's own bottom edge rather than a button
 * floating under it, so the strip reads as the last band of the panel stack. */
.psplit__setup { border-top: 1px solid var(--rule); }
.psplit__setup > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: clamp(0.9rem, 2.2vw, 1.35rem) clamp(1.1rem, 2.6vw, 1.9rem);
  cursor: pointer;
  list-style: none;
}
.psplit__setup > summary::-webkit-details-marker { display: none; }
.psplit__setup > summary::marker { content: ''; }
.psplit__setup > summary:focus-visible { outline: 2px solid var(--marker); outline-offset: -4px; }
.psplit__setup-label {
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-stretch: 88%;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--marker);
}
.psplit__setup-cue {
  font-family: var(--display);
  font-size: 1.375rem;
  font-weight: 400;
  line-height: 1;
  color: var(--marker);
  transition: transform var(--dur-fast) var(--ease);
}
.psplit__setup[open] > summary .psplit__setup-cue { transform: rotate(45deg); }
.psplit__setup-body {
  padding: 0 clamp(1.1rem, 2.6vw, 1.9rem) clamp(1.4rem, 3vw, 2.2rem);
}
@media (max-width: 60rem) {
  .psplit__pair { grid-template-columns: minmax(0, 1fr); }
  .psplit__hinge { flex-direction: row; padding: 1rem; border-block: 1px solid var(--rule); }
  .psplit__changed { grid-template-columns: minmax(0, 1fr); }
}

/* ---- 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 { 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 { 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. */
  /* The summary keeps a second track for the cue: the plus stays on the title's
     row and hard against the right, rather than dropping to a line of its own
     under the claim where it reads as a stray character. */
  .layer > summary { grid-template-columns: minmax(0, 1fr) auto; }
  .layer__body { grid-template-columns: minmax(0, 1fr); }
  .layer__lede, .layer__body > * { grid-column: 1; }
  .layer__cue { grid-column: 2; grid-row: 1; }
  .layer--split > .layer__body, .layer--split > .layer__body > .layer__col { grid-template-columns: minmax(0, 1fr); 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. */
  /* The nav used to wrap onto a second row under the mark. Four items and a
     call to action do not fit one row on a phone, so the bar became two, and
     the mark, the action and the links all sat on different baselines. A
     disclosure holds the links instead: one bar, one row, everything aligned,
     and the panel opens under it.

     Order matters here. The mark, then the Menu button, then the action, so
     the thing a visitor is most likely to want is the one nearest the thumb
     and the menu never covers it. */
  /* Back to a row below the breakpoint: the centre track is empty here, the
     nav collapses to a toggle, and three grid tracks would leave the brand
     and the two controls spread across a bar with a hole in the middle. */
  .masthead__inner { display: flex; position: relative; flex-wrap: nowrap; gap: 0.9rem; padding-block: 0.4rem; min-height: 0; justify-content: space-between; }
  .masthead__brand img { width: 6.25rem; }
  /* Action first, menu last. The menu is the control a thumb reaches for most
     and the right edge is where the thumb is. */
  .masthead__cta { order: 2; }
  .nav { order: 3; }
  .nav__disclose { display: block; }

  .nav__toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding-block: 0.8rem;
    font-family: var(--mono);
    font-size: var(--t-meta);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-invert);
    cursor: pointer;
    list-style: none;
  }
  /* A plus that becomes a cross, drawn rather than swapped for a second glyph,
     so the control reads as one thing changing state. */
  .nav__toggle::after {
    content: '+';
    font-size: 1.15em;
    line-height: 1;
    color: var(--marker);
    transition: transform var(--dur-fast) var(--ease);
  }
  .nav__disclose[open] > .nav__toggle::after { transform: rotate(45deg); }

  /* Closed, the list goes back to being hidden. Open, it is a panel across the
     full width under the bar rather than an item in the row. */
  .nav__list { display: none; }
  .nav__disclose[open] + .nav__list {
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    z-index: 20;
    display: block;
    padding: 0.25rem var(--gutter) 1rem;
    background: var(--ink);
    border-top: 1px solid var(--rule-invert);
  }
  .nav__disclose[open] + .nav__list > .nav__item { border-bottom: 1px solid var(--rule-invert); }
  .nav__disclose[open] + .nav__list > .nav__item:last-child { border-bottom: 0; }
  .nav__link { display: block; padding-block: 0.85rem; }
  /* 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; }
  /* On a phone the panel is not a panel. It is a nested list inside a
     full-width sheet that is already a panel, so everything that made the
     desktop dropdown float comes off: the width, the fill, the shadow, the
     centring transform. What stays is the rule down the left, which is the
     only thing saying these three belong to the label above them. */
  .nav__submenu {
    position: static;
    width: auto;
    transform: none;
    padding: 0.35rem 0 0.35rem 0.85rem;
    background: none;
    border: 0;
    border-left: 2px solid var(--marker);
    box-shadow: none;
  }
  .nav__submenu::before { content: none; }
  .nav__submenu li + li .nav__sublink { border-top: 0; }
  .nav__sublink { padding-inline: 0; }
  /* The reference sets its phone menu at 24px in the serif. It reads as a menu
     you chose rather than a list you scrolled past, and at this size the
     44px target floor comes for free. */
  .nav__subtitle { font-size: 1.5rem; }
  /* The notes come off. Three descriptions per group is most of a screen on a
     390px phone, and the panel is a place to move through, not read. */
  .nav__subnote { display: none; }
  /* The group label becomes a section heading over its own list, which is the
     shape the reference's phone sheet uses. */
  .nav__group > .nav__link { color: var(--marker); font-size: var(--t-micro); letter-spacing: 0.15em; }
  .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; }
  /* Same split as the desktop hero, a smaller box. 88svh of a phone is nearly
     the whole screen and leaves nothing below the fold to scroll toward, so
     the ceiling comes down and the band ends around 71% of the viewport with
     the next section just visible under it. */
  .hero { 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
   the old cooler #edeef2 showed 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>");
}

/* ---- Band edges ----------------------------------------------------------
   Straight. There used to be a drawn wave here: a 48px crest at the top of an
   arriving band, filled with the colour of the band it was leaving, so the two
   never had to agree about who owned the boundary.

   It was built for a paper site with two ink bands set into it, where the wave
   crossed a real colour change. With the ground flipped dark it fires between
   an ink band and an ink band and fills with --paper, so what it actually drew
   was a cream ribbon through the middle of a dark page, belonging to neither
   side of it. The reference build has no wave anywhere and ends its bands on a
   hairline; a band that changes colour does not need help being read as a
   different band. Removed rather than recoloured, because recoloured it draws
   nothing and is 20 lines of stylesheet drawing nothing. */

/* ---- 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(229, 175, 47, 0.34), rgba(229, 175, 47, 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 there is no swipe at all.

   The note here used to say that an amber wash behind amber type is mush, so
   the swipe went pale instead. The first half was right and the conclusion was
   wrong: the answer was not a different pen, it was no pen. Marked words on a
   dark band are already amber against navy, which is the loudest contrast the
   palette has. A wash on top of that is a second emphasis on something already
   emphasised, and it is not free — the pale swipe lifted the ground behind
   those words to #393b4e and took the amber from 9.32:1 down to 5.5:1. The one
   phrase per page chosen for emphasis was the hardest amber on the page to
   read.

   The swipe stays on paper, where the type is dark and genuinely needs it. */
.section--dark .mark-word,
.hero .mark-word,
.cta-band .mark-word {
  background-image: none;
  padding: 0;
}

/* ---- 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='%23e5af2f' 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(--rule);
  height: 1px;
  top: 2.2rem;
}
/* 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='%230e1126' stroke='%230e1126' stroke-width='2.6'/></svg>");
}
.dumbbell__val {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  font-family: var(--display);
  font-weight: 500;
  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(--alarm); }
.dumbbell__val--to   { left: calc(var(--to) * 1%);   color: var(--text); }
/* 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); }

@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-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);
  border-top: 1px solid var(--rule);
}
/* 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(--alarm);
  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(--marker);
  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(--marker);
    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(--lift);
}
.wall__cell--voice { background: var(--sink); justify-content: space-between; }
.wall__cell--refusal { background: transparent; box-shadow: inset 0 0 0 1px var(--rule); }

.wall__value {
  font-family: var(--display);
  font-size: clamp(1.65rem, 3.4vw, 2.6rem);
  font-weight: 400;
  line-height: 1;
  letter-spacing: -0.015em;
  color: var(--text);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3em;
}
.wall__from { color: var(--alarm); }
.wall__arrow { color: var(--marker); font-size: 0.62em; }
.wall__cell--down .wall__from { color: var(--text); }
.wall__cell--down .wall__to { color: var(--alarm); }

.wall__label {
  font-family: var(--mono);
  font-size: var(--t-meta);
  font-stretch: 92%;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--marker);
}
.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(--marker);
  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(--alarm);
  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(--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; }

/* ── Home page: conviction, signal, three layers, panels, founder ──────────
   All five read their colour off the ground they land on, so the same markup
   works on paper and on ink without a variant class. */

/* The belief. Wide measure, nothing beside it, and the label in the rail track
   so the sentence starts where every other headline on the site starts. */
.conviction__label {
  grid-column: 1;
  margin: 0;
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--marker);
}
.conviction__body { grid-column: 2; }
.conviction__title {
  margin: 0;
  font-size: var(--t-display);
  line-height: 1.02;
  letter-spacing: -0.03em;
  max-width: 20ch;
}
/* Left edge on the headline's, not hung off the right.
   The auto left margin pushed the sentence to the far side of the column, so
   the claim and the sentence that qualifies it started in two different
   places and read as two separate blocks. */
.conviction__copy {
  margin: clamp(1.5rem, 3vw, 2.5rem) 0 0 0;
  font-size: clamp(1.0625rem, 1.5vw, 1.1875rem);
  line-height: 1.6;
  color: var(--text-soft);
  max-width: 52ch;
}

/* Noise into signal. The canvas has no intrinsic size, so it needs a height
   here or it collapses to zero and the script draws into nothing. */
.signal {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: var(--rail-cols);
  column-gap: var(--rail-gap);
  row-gap: clamp(1.5rem, 3vw, 2.25rem);
  align-items: start;
}
.signal__label {
  grid-column: 1;
  margin: 0;
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--marker);
}
.signal__lede {
  grid-column: 2;
  margin: 0 0 0 auto;
  font-size: clamp(1.0625rem, 1.5vw, 1.1875rem);
  line-height: 1.6;
  color: var(--text-soft);
  max-width: 52ch;
}
.signal__canvas {
  grid-column: 1 / -1;
  display: block;
  width: 100%;
  height: clamp(8rem, 18vw, 13.75rem);
}
.signal__axis {
  grid-column: 1 / -1;
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin: 0;
  padding: 0.875rem 0 0;
  border-top: 1px solid var(--rule);
  list-style: none;
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.signal__axis li:last-child { color: var(--marker); }
/* The phone keeps the axis horizontal.
 *
 * It used to stack into a list, and a stacked axis stops being an axis: the
 * whole point of these three words is that the reader maps the left of the
 * curve to the first one and the right of the curve to the last, so volatile
 * and flat get names. Read down a column, they are just three adjectives.
 *
 * Three equal tracks rather than a flex row, so the long first label can wrap
 * to two lines without pushing the other two off their ends. Each label is
 * aligned to the part of the curve it belongs to. */
@media (max-width: 34rem) {
  .signal__axis {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0.5rem;
    letter-spacing: 0.06em;
    line-height: 1.3;
  }
  .signal__axis li { margin: 0; }
  .signal__axis li:nth-child(2) { text-align: center; }
  .signal__axis li:last-child { text-align: right; }
}

/* The phone: one column, labels above what they label.
 *
 * This block used to sit above the rules it is overriding, which is the same
 * as not existing: a media query adds no specificity, so every line in it lost
 * to the identical single-class selector further down the file, and the curve
 * kept its 1fr/3fr rail at 390px with the lede squeezed into a third of the
 * screen. Moved rather than made more specific, because the intent was always
 * "last word on small screens" and source order is how that is said. */
@media (max-width: 60rem) {
  .conviction__label, .conviction__body, .signal { grid-column: 1; }
  .signal { grid-template-columns: minmax(0, 1fr); }
  .signal__label, .signal__lede { grid-column: 1; }
  .conviction__label, .signal__label { margin-bottom: 0.75rem; }
  /* stacked, there is no right margin to hang from */
  .conviction__copy, .signal__lede { margin-left: 0; }
}

/* Motion, system, team. Three cells and a track under them naming what each
   handover is called, so the diagram reads left to right as a sequence of
   ownership rather than three unrelated columns. */
.layers3 {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px;
  background: var(--rule);
  border-block: 1px solid var(--rule);
}
.layers3__cell {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  min-height: clamp(17rem, 26vw, 22rem);
  padding: clamp(1.25rem, 2.4vw, 1.875rem) clamp(1rem, 2vw, 1.625rem);
  background: var(--ground-self, transparent);
}
/* The handover, drawn on the seam between two cells. Three columns with
   nothing between them are three lists; a mark sitting on the join says the
   left one becomes the right one, which is the entire claim of the module. */
.layers3__cell:not(:last-of-type)::after {
  content: '\2192';
  position: absolute;
  z-index: 2;
  top: 50%;
  right: -1rem;
  width: 2rem;
  height: 2rem;
  margin-top: -1rem;
  display: grid;
  place-items: center;
  border: 1px solid var(--rule);
  border-radius: 50%;
  font-family: var(--mono);
  font-size: 0.8125rem;
  color: var(--marker);
}
.section--dark .layers3__cell:not(:last-of-type)::after {
  background: var(--ink);
  border-color: var(--rule-invert);
}
.section--alt .layers3__cell:not(:last-of-type)::after { background: var(--paper); }
.section--dark .layers3 { background: var(--rule-invert); border-color: var(--rule-invert); }
.section--dark .layers3__cell { background: var(--ink-deep); }
.section--alt .layers3__cell { background: var(--paper); }
.layers3__num {
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.12em;
  color: var(--marker);
}
.layers3__name {
  margin: clamp(1.75rem, 4vw, 3.25rem) 0 0;
  font-size: var(--t-set);
  line-height: 1.05;
  letter-spacing: -0.02em;
}
.layers3__what { margin: 0; font-size: var(--t-small); color: var(--text-soft); }
.layers3__parts {
  margin: 0.5rem 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.layers3__parts { margin-top: auto; gap: 0; }
.layers3__parts li {
  padding: 0.6rem 0;
  border-top: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.05em;
  color: var(--text);
}
.section--dark .layers3__parts li { border-top-color: var(--rule-invert); }
.layers3__rule {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: auto 1fr auto 1fr auto;
  align-items: center;
  gap: 1.375rem;
  margin: 0;
  padding: 1rem clamp(1rem, 2vw, 1.625rem);
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.layers3__rule i { height: 1px; background: var(--marker); opacity: 0.55; }
.section--dark .layers3__rule { background: var(--ink-deep); }
.section--alt .layers3__rule { background: var(--paper); }
@media (max-width: 54rem) {
  .layers3 { grid-template-columns: minmax(0, 1fr); }
  .layers3__cell { min-height: 0; }
  /* stacked, the handover points down rather than across */
  .layers3__cell:not(:last-of-type)::after {
    content: '\2193';
    top: auto;
    bottom: -1rem;
    right: clamp(1rem, 2vw, 1.625rem);
    margin-top: 0;
  }
  .layers3__name { margin-top: 1.25rem; }
  .layers3__rule { grid-template-columns: auto 1fr auto; }
  .layers3__rule i:last-of-type, .layers3__rule span:last-child { display: none; }
}

/* Two proof panels. A photograph and one figure set large enough to be the
   point, which the four-stat row it replaced never was. */
.panels {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1rem, 2.4vw, 2rem);
}
.panel {
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: inherit;
  border: 1px solid var(--rule);
  transition: border-color var(--dur-fast) var(--ease);
}
.panel:hover { border-color: var(--marker); }
.panel:focus-visible { outline: 2px solid var(--marker); outline-offset: 3px; }
.panel__shot { margin: 0; overflow: hidden; aspect-ratio: 4 / 3; }
.panel__shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.panel__body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: clamp(1.125rem, 2.2vw, 1.75rem);
}
.panel__label {
  margin: 0;
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.panel__title { margin: 0; font-size: var(--t-set); line-height: 1.05; letter-spacing: -0.02em; }
.panel__stat { margin: 0.5rem 0 0; display: flex; flex-direction: column; gap: 0.25rem; }
.panel__stat strong {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(2.25rem, 4.4vw, 3.25rem);
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--marker);
}
.panel__stat span {
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.panel__note { margin: 0.35rem 0 0; font-size: var(--t-small); color: var(--text-soft); }
.panel__cue {
  margin-top: auto;
  padding-top: 1rem;
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.12em;
  color: var(--marker);
}
@media (max-width: 48rem) { .panels { grid-template-columns: minmax(0, 1fr); } }

/* The same panel turned on its side: copy left, photograph right.
   The body leads in source order so a screen reader and an extractor both get
   the claim before the picture, and `order` does the visual swap only where
   there are two columns to swap between. The shot is squared rather than left at
   its own 3/4: the source frame is a portrait, and a portrait in a half-width
   column sets the panel's height at nearly twice what the copy needs, which is
   how the copy column ends up with a hole in the middle of it. A square crop
   lets the copy set the height. */
.panel--wide {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.85fr);
  align-items: stretch;
}
.panel--wide .panel__body {
  gap: 0.5rem;
  padding: clamp(1.5rem, 3.4vw, 2.75rem);
}
.panel--wide .panel__title { font-size: var(--t-card); }
.panel--wide .panel__shot { aspect-ratio: 1 / 1; }
.panel--wide .panel__note { max-width: 34ch; }
/* A dark panel on a paper band.
 *
 * The proof stays ink while the section around it goes back to paper, so the
 * case sits in the page as an inset rather than as another band. Tokens only:
 * every rule, label and figure inside already reads its colour off --text,
 * --text-soft, --marker and --rule, so re-pointing the four here is the whole
 * variant and nothing inside names a literal. */
.panel--ink {
  background: var(--ink);
  --text: var(--text-invert);
  --text-soft: var(--slate);
  --marker: var(--amber);
  --rule: var(--rule-invert);
  color: var(--text);
}
@media (max-width: 48rem) {
  .panel--wide { grid-template-columns: minmax(0, 1fr); }
  .panel--wide .panel__body { order: 2; }
  .panel--wide .panel__shot { order: 1; aspect-ratio: 4 / 3; min-height: 0; }
}

/* The founder. Portrait left, the answer to "who actually does the work"
   right. The photograph is high key on white, so it sits on paper and is given
   a rule rather than a box: a border round a white image reads as a missing
   asset. */
.founder {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: clamp(1.5rem, 4vw, 3.5rem);
  /* Centred, not top-aligned. The portrait runs 4/5 and the body runs about
     two-thirds of that, so `start` stacked the whole column against the top
     rule and left a third of the row empty underneath it — the module read as
     crammed at the top of a box it did not fill. */
  align-items: center;
}
.founder__shot { margin: 0; }
.founder__shot img {
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  object-position: 50% 22%;
  display: block;
  border-top: 1px solid var(--marker);
}
/* Five stacked elements of five different weights need more than 12px between
   them: at that rhythm the eyebrow sits on top of the headline and the whole
   column reads as one block. */
.founder__body { display: flex; flex-direction: column; gap: clamp(1rem, 2vw, 1.5rem); }
.founder__label {
  margin: 0;
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--marker);
}
.founder__title { margin: 0; font-size: var(--t-set); line-height: 1.05; letter-spacing: -0.025em; }
.founder__copy { margin: 0; font-size: var(--t-body); line-height: 1.6; color: var(--text-soft); max-width: 56ch; }
.founder__quote {
  margin: clamp(0.75rem, 1.6vw, 1.25rem) 0 0;
  padding-left: 1.125rem;
  border-left: 1px solid var(--marker);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.founder__quote p {
  margin: 0;
  font-family: var(--display);
  font-size: clamp(1.125rem, 1.9vw, 1.4375rem);
  line-height: 1.35;
  color: var(--text);
}
.founder__quote cite {
  font-family: var(--mono);
  font-style: normal;
  font-size: var(--t-micro);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.founder__link { margin: clamp(0.5rem, 1.2vw, 1rem) 0 0; }
@media (max-width: 54rem) {
  .founder { grid-template-columns: minmax(0, 1fr); }
  .founder__shot { max-width: 20rem; }
}

/* The four phases on a track. The line runs behind the whole row and each
   phase marks its own point on it, so the module reads as one sequence rather
   than four cards that happen to be adjacent. */
.method {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(1.25rem, 2.8vw, 2.75rem);
  margin: 0;
  padding: 2.25rem 0 0;
  list-style: none;
  position: relative;
}
.method::before {
  content: '';
  position: absolute;
  top: 0.375rem;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--rule);
}
/* --rule does not re-point on a dark ground the way --marker does; a component
   that wants the quiet rule there has to name --rule-invert. Both tracks were
   painting the cream rule on ink while the dividers inside them used the
   invert, so one module carried two rule colours. */
.section--dark .method::before { background: var(--rule-invert); }
.method__step { position: relative; display: flex; flex-direction: column; gap: 0.35rem; }
.method__step::before {
  content: '';
  position: absolute;
  top: -2.25rem;
  left: 0;
  width: 0.8125rem;
  height: 0.8125rem;
  margin-top: -0.03125rem;
  border: 2px solid var(--marker);
  border-radius: 50%;
  background: var(--ink);
}
.section--alt .method__step::before { background: var(--paper); }
.method__num {
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.12em;
  color: var(--marker);
}
.method__name { margin: 0.25rem 0 0; font-size: var(--t-set); line-height: 1.05; letter-spacing: -0.02em; }
.method__meta {
  margin: 0;
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.method__copy { margin: 0.4rem 0 0; font-size: var(--t-small); line-height: 1.55; color: var(--text-soft); }
.method__link { margin: clamp(1.5rem, 3vw, 2.25rem) 0 0; }
@media (max-width: 60rem) {
  .method { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 2.5rem; }
}
@media (max-width: 36rem) {
  .method { grid-template-columns: minmax(0, 1fr); padding-left: 1.5rem; padding-top: 0; }
  .method::before { top: 0; bottom: 0; left: 0.375rem; right: auto; width: 1px; height: auto; }
  .method__step::before { top: 0.35rem; left: -1.5rem; margin-top: 0; }
}
