/* =============================================================================
   Anduin — single page
   E-ink discipline: paper ground, ink hairlines, no shadows, no gradients on
   surfaces, no rounded cards. Structure is carried by 1px rules and space.

   The palette and the .strip / .kicker / .scroll / .fx idiom are the house
   style, shared with the internal handbook so both surfaces read as one brand.
   ========================================================================== */

/* --- Tokens ---------------------------------------------------------------- */
/* The brand colour is #1a2c47, a deep navy. It is used as-is in light mode and
   derived for everywhere it cannot be:
     light accent  #1a2c47  12.42:1 on ground - reads as deep navy, not a bright
                   blue, which is the point. Note it is only 1.28:1 against --ink,
                   so it must never be the only thing distinguishing two pieces of
                   text; it earns its keep on rules, focus rings and single marks.
     dark accent   #8fabd6  same hue (216) and saturation (46%) lifted to L70,
                   giving 8.08:1 on the dark ground. #1a2c47 itself is 1.35:1
                   there - invisible - so it cannot be used directly.
     accent-soft   light #dae3f1 (hue 216 at L90; --ink-2 on it is 6.94:1)
                   dark  #1a2c4747, which is the brand colour itself at 28% alpha.
     fx-accent     see the note by the token: deliberately not --accent.

   PREVIOUS PALETTES, kept for reference. Restore by swapping a set back in
   wholesale; nothing outside this block, favicon.svg, og-cover.png and the
   flow-field fallbacks hardcodes a colour.
     denim  light accent #2a5d9f  accent-soft #dfe6f2  fx-accent 42, 93, 159
            dark  accent #7fb0e8  fx-accent 127, 176, 232
            light accent on ground 5.87:1, dark accent on ground 8.27:1
     pine   light accent #1c6a49  accent-soft #dfeae3  fx-accent 28, 106, 73
            dark  ground #0e1512  surface #141c18  surface-2 #1b2521
                  ink #e2e9e4  ink-2 #a7b5ad  ink-3 #7e8d85
                  line #26322f  line-strong #3a4a45
                  accent #5cc08d  accent-soft #16302447  fx-accent 92, 192, 141
            light accent on ground 5.79:1, dark accent on ground 8.27:1. */
:root {
  --ground: #f2f1ec;
  --surface: #fbfaf7;
  --surface-2: #e9e8e1;
  --ink: #14171a;
  --ink-2: #454a4d;
  --ink-3: #5f6568;
  --line: #dcdad1;
  --line-strong: #bdbab0;
  --accent: #1a2c47;
  --accent-soft: #dae3f1;
  --fill: #8a5a0e;
  --fill-soft: #f2e6cf;
  /* Validation only. Kept distinct from --fill, which marks a pending value:
     a missing answer and an unfilled placeholder are different signals and
     must not look alike. 6.58:1 on ground, 7.13 on surface. */
  --bad: #9e2b21;

  /* canvas reads these so the hero repaints correctly on theme change.
     fx-accent is deliberately NOT --accent. The hero paints particles in the
     accent until they cross the waist rule and in ink after it, so the two have
     to be told apart: #1a2c47 against ink is 1.28:1, which erases the effect
     entirely. This is the same hue and saturation lifted to L38, which restores
     2.51:1 against ink - near the 2.71:1 the previous accent gave. */
  --fx-ink: 20, 23, 26;
  --fx-accent: 52, 88, 141;

  --f-display: Georgia, "Iowan Old Style", "Times New Roman", serif;
  --f-body: "Segoe UI", system-ui, -apple-system, Roboto, "Helvetica Neue", Arial, sans-serif;
  /* Brand face, and the only web font requested. This was two tokens: one for
     the wordmark and a separate monospace voice for labels. The labels have
     since moved onto Montserrat too, so the split described a difference the
     stylesheet no longer made. Only weight 600 ships, so rules using this
     state 600 explicitly — asking for 400 does not synthesise a lighter cut,
     it simply renders the 600 file. */
  --f-mark: Montserrat, "Segoe UI", system-ui, -apple-system, Arial, sans-serif;

  --gutter: 26px;
  --measure: 1160px;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ground: #0d1117;
    --surface: #131a22;
    --surface-2: #1a2230;
    --ink: #e3e8ee;
    --ink-2: #a8b3c0;
    --ink-3: #828d9b;
    --line: #26303c;
    --line-strong: #3a4756;
    --accent: #8fabd6;
    --accent-soft: #1a2c4747;
    --fill: #d8a54f;
    --fill-soft: #33280f66;
    --bad: #e98a80;   /* 7.58:1 on the dark ground */
    --fx-ink: 227, 232, 238;
    --fx-accent: 143, 171, 214;
  }
}
:root[data-theme="dark"] {
  --ground: #0d1117;
  --surface: #131a22;
  --surface-2: #1a2230;
  --ink: #e3e8ee;
  --ink-2: #a8b3c0;
  --ink-3: #828d9b;
  --line: #26303c;
  --line-strong: #3a4756;
  --accent: #8fabd6;
  --accent-soft: #1a2c4747;
  --fill: #d8a54f;
  --fill-soft: #33280f66;
  --bad: #e98a80;
  --fx-ink: 227, 232, 238;
  --fx-accent: 143, 171, 214;
}

/* --- Wordmark face --------------------------------------------------------- */
/* Montserrat SemiBold, self-hosted rather than pulled from Google Fonts: the
   privacy note in the footer promises no third-party embeds, and a gstatic
   request would hand every visitor's IP to a third party. Google's own latin
   subset, 18.7 KB. Montserrat is (c) The Montserrat Project Authors, licensed
   under the SIL Open Font License 1.1 — see fonts/OFL.txt. */
@font-face {
  font-family: "Montserrat";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("../fonts/montserrat-600-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+2000-206F, U+2122, U+2212, U+FEFF, U+FFFD;
}

/* --- Base ------------------------------------------------------------------ */
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; scroll-padding-top: 96px; }
body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--f-body);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
a { color: inherit; }
a:focus-visible, button:focus-visible, summary:focus-visible, input:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.wrap { max-width: var(--measure); margin: 0 auto; padding: 0 var(--gutter); }

/* --- Shared type ----------------------------------------------------------- */
/* Section headings take the brand face, so the wordmark, the section labels,
   the buttons and the protection labels all speak in one voice. Montserrat is
   loaded at 600 only — every --f-mark rule states that weight explicitly, or
   the browser synthesises a fake bold from it. */
.kicker {
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: 11px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-3);
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  margin: 0 0 18px;
}

h1, h2, h3 { font-family: var(--f-display); font-weight: 400; margin: 0; text-wrap: balance; }
h1 {
  font-size: clamp(38px, 6.2vw, 74px);
  line-height: 1.02;
  letter-spacing: -0.028em;
}
h2 {
  font-size: clamp(28px, 3.8vw, 46px);
  line-height: 1.08;
  letter-spacing: -0.022em;
  max-width: 20ch;
}
h3 { font-size: 21px; line-height: 1.25; letter-spacing: -0.012em; }
h1 em, h2 em { font-style: italic; color: var(--ink-2); }

p { margin: 0; max-width: 66ch; }
.lead { font-size: 19px; line-height: 1.55; color: var(--ink-2); max-width: 66ch; }

/* fill-in tokens — anything not yet true is visibly marked, never invented */
.fx {
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: .86em;
  color: var(--fill);
  background: var(--fill-soft);
  padding: 1px 5px;
  border-radius: 2px;
  white-space: nowrap;
}
/* Longer fill-ins - a pending sentence rather than a token like an ABN. The
   base .fx must never wrap, or a placeholder ABN breaks across lines; a pending
   paragraph must wrap, or it runs off the page. Hence the two. */
.fx--long { white-space: normal; display: inline-block; }

/* --- Header ---------------------------------------------------------------- */
.top {
  position: sticky;
  top: 0;
  z-index: 60;
  background: color-mix(in srgb, var(--ground) 88%, transparent);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line);
}
.top-in {
  max-width: var(--measure);
  margin: 0 auto;
  padding: 12px var(--gutter);
  display: flex;
  align-items: center;
  gap: 24px;
}
.brand { display: flex; align-items: center; gap: 10px; text-decoration: none; line-height: 1.1; }
/* The mark is inlined in the markup, not masked from ../img/anduin-mark.svg:
   a CSS mask pointing at an external file is treated as a cross-origin fetch
   under file://, so the logo vanished whenever index.html was opened directly
   rather than served. Inline, it also inherits currentColor for free.
   img/anduin-mark.svg is kept as the canonical asset for other surfaces. */
.brand-mark { width: 28px; height: 28px; flex: none; display: block; fill: currentColor; }
.brand b {
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: 20px;
  letter-spacing: -0.005em;
}
.top nav { margin-left: auto; display: flex; align-items: center; gap: 22px; }
/* Nine items once About and Articles are in, which stopped fitting well before
   the old 900px breakpoint. The nav now sheds in two stages instead of one:
   first the six homepage section anchors, which are secondary from any other
   page anyway, and only then the page links themselves. Contact and the theme
   control never leave. */
@media (max-width: 1120px) { .top nav { gap: 13px; } }
@media (max-width: 1080px) { .top nav a.sec-link { display: none; } }

/* display: contents, so on desktop the links are flex items of .top nav
   exactly as they were before this wrapper existed. The wrapper is only a
   wrapper below 620px, where it becomes the dropdown panel. */
.nav-links { display: contents; }

/* The hamburger. Hidden above 620px, where the real nav is showing, and
   hidden without scripting, where it could not open anything. */
.nav-toggle { display: none; }
.nav-toggle {
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: 5px;
  background: var(--surface);
  color: var(--ink-2);
  cursor: pointer;
  transition: border-color .18s, color .18s;
}
.nav-toggle svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
.nav-toggle:hover { border-color: var(--ink-3); color: var(--ink); }
.nav-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* One button, two glyphs: CSS shows whichever matches the state, so the
   script only ever flips one attribute. */
.nav-toggle .ico-close { display: none; }
.nav-toggle[aria-expanded="true"] .ico-open { display: none; }
.nav-toggle[aria-expanded="true"] .ico-close { display: block; }

@media (max-width: 620px) {
  .nav-toggle { display: inline-flex; }
  html:not(.js) .nav-toggle { display: none; }

  /* The panel. Absolute inside the sticky .top, so it hangs below the header
     and overlays the page instead of adding to its height. left/right 0 makes
     it full width, which is steadier than anchoring a card to one corner on a
     screen this narrow. */
  .js .nav-links {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 6px var(--gutter) 12px;
    background: var(--ground);
    border-bottom: 1px solid var(--line);
    box-shadow: 0 12px 28px rgba(0, 0, 0, .10);
  }
  .js .nav-links.is-open { display: flex; }

  /* 48px rows: a menu is a list of targets, and WCAG 2.2 asks 24px minimum.
     The hairline between them is the site's own separator, not a new idiom. */
  .js .nav-links a:not(.btn),
  .js .nav-links a.btn {
    display: flex;
    align-items: center;
    min-height: 48px;
    padding: 0;
    font-size: 12px;
    letter-spacing: .12em;
    border: none;
    border-bottom: 1px solid var(--line);
    border-radius: 0;
    background: none;
    color: var(--ink-2);
    text-transform: uppercase;
  }
  .js .nav-links a:last-child { border-bottom: none; }
  .js .nav-links a:hover,
  .js .nav-links a:focus-visible { color: var(--ink); }
  /* Contact keeps its weight without keeping its box — a full-width filled
     button inside a list of text links reads as a different kind of thing.
     justify-content is reset because .btn centres its contents, which left
     Contact floating mid-row while every link above it started at the margin. */
  .js .nav-links a.btn { color: var(--accent); justify-content: flex-start; }
  /* The section anchors come back here: they are hidden at 1080px because
     they do not fit a row, but a column has room for them. */
  .js .nav-links a.sec-link { display: flex; }

  /* No scripting: a plain wrapped row rather than nothing at all. */
  html:not(.js) .nav-links { display: flex; flex-wrap: wrap; gap: 10px 16px; }
  html:not(.js) .top nav a.sec-link { display: inline-block; }
}
/* :not(.btn) — the Contact link is the shared button component and must keep
   its own fill, padding and full border rather than the text-link treatment. */
.top nav a:not(.btn) {
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: 11px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
  text-decoration: none;
  padding: 4px 0;
  border-bottom: 1px solid transparent;
}
.top nav a:not(.btn):hover { color: var(--ink); border-bottom-color: var(--accent); }
.theme-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: 11px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-2);
  background: none;
  border: 1px solid var(--line-strong);
  padding: 8px 10px;
  cursor: pointer;
  line-height: 1;
}
.theme-btn:hover { color: var(--ink); border-color: var(--ink-3); }
/* The button holds all three icons and shows the one matching its state. Doing
   it in CSS keeps the script to a single textContent write and means the icon
   can never fall out of step with data-mode. */
.theme-ico {
  display: none;
  width: 14px;
  height: 14px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.theme-btn[data-mode="auto"]  .theme-ico[data-for="auto"],
.theme-btn[data-mode="light"] .theme-ico[data-for="light"],
.theme-btn[data-mode="dark"]  .theme-ico[data-for="dark"] { display: block; }

/* --- Buttons --------------------------------------------------------------- */
/* One outlined button, used everywhere: hero, notes, nav and the form submit.
   The solid accent fill was too heavy — two filled slabs under the hero
   headline fought the type — so the button is back to an outline and takes its
   modernity from proportion and motion instead of weight: a soft 5px radius,
   an inset surface rather than a transparent one, and a trailing arrow that
   advances on hover while the fill sweeps in. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: 11.5px;
  letter-spacing: .12em;
  text-transform: uppercase;
  text-decoration: none;
  padding: 14px 22px;
  /* --ink-3, not --line-strong. On a tint band the button's fill matches the
     band exactly, so the border is the only thing saying "control" — and
     --line-strong reaches just 1.9:1 there, under the 3:1 that boundary needs.
     --ink-3 clears it on every ground the button lands on (3.7–5.3:1). */
  border: 1px solid var(--ink-3);
  border-radius: 5px;
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
  transition: background .2s ease, border-color .2s ease, color .2s ease;
}
/* The arrow is a pseudo-element so no button has to carry it in its markup. */
.btn::after {
  content: "\2192";
  font-size: 13px;
  line-height: 1;
  transition: transform .2s ease;
}
.btn:hover {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--ground);
}
.btn:hover::after { transform: translateX(4px); }
.btn:active { transform: translateY(1px); }
/* Nav-sized variant — same component, trimmed to sit on the header row. */
.btn--sm { padding: 8px 14px; font-size: 10.5px; gap: 8px; }
.btn--sm::after { font-size: 12px; }

/* --- Hero ------------------------------------------------------------------ */
.hero { position: relative; overflow: hidden; }
.hero-fx {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}
/* paper wash over the left half so the serif headline never fights the field */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    100deg,
    var(--ground) 0%,
    var(--ground) 13%,
    color-mix(in srgb, var(--ground) 74%, transparent) 32%,
    color-mix(in srgb, var(--ground) 40%, transparent) 49%,
    transparent 63%
  );
}
.hero-in {
  position: relative;
  z-index: 2;
  max-width: var(--measure);
  margin: 0 auto;
  /* The fact strip used to close the hero off, so .hero-in carried no bottom
     padding. With the strip gone the hero has to end itself — this value keeps
     the pre-existing gap between the CTA row and the first band's rule. */
  padding: clamp(60px, 10vh, 112px) var(--gutter) clamp(74px, 12vh, 132px);
  pointer-events: none;
}
.hero-in a { pointer-events: auto; }
.hero h1 { max-width: 17ch; }
.hero .lead { margin-top: 26px; max-width: 46ch; }
.hero-cta { margin-top: 34px; display: flex; flex-wrap: wrap; align-items: center; gap: 14px 22px; }

/* --- Sections -------------------------------------------------------------- */
/* No rule between bands: the ground alternates plain / tint on every boundary,
   so the change of background already separates them and a hairline on top of
   it only doubled the signal. Any reordering must keep that alternation — two
   plain bands in a row would now run together with nothing to divide them. */
section.band { padding: clamp(64px, 10vh, 108px) 0; }
section.band.tint { background: var(--surface); }
/* Measure. `ch` resolves against the element's OWN font-size, so capping this
   container at 62ch while the .lead inside it renders at 19px produced 53
   characters a line - the bottom of the readable band, and tiring over a long
   paragraph. The container is now sized in rem so it cannot bind, and the
   measure is set on the text itself where the unit means what it says.
   Target is 50-75 characters, ~66 being the usual optimum for sustained
   reading. Sections that pair a head with the chat chain are still governed by
   the .head-row grid column, so that layout is unchanged. */
.head { max-width: 44rem; }
.head p { margin-top: 20px; }

/* --- Ambient chat chain ----------------------------------------------------- */
/* A fictional workplace exchange running down the empty column beside each
   section head, scrubbed by scroll in js/chat-chain.js. It is decorative: it
   shows the shape of the asset the page describes (the argument before the
   decision, the wrong turns before the fix) without asserting anything.

   A grid row rather than absolute positioning, so the stage's height is simply
   the head's height — no magic numbers, and it collapses on its own. */
.head-row {
  display: grid;
  grid-template-columns: minmax(0, 62ch) minmax(0, 1fr);
  gap: clamp(32px, 4vw, 64px);
  align-items: stretch;
}
.chat {
  position: relative;
  min-height: 240px;
  overflow: hidden;
  opacity: 0;                 /* the scrub owns this; 0 until JS says otherwise */
  pointer-events: none;
  /* Soft edges so messages enter and leave the stage rather than being cut off
     by it. The top fade is much shallower than the bottom: a message begins its
     flight out at 80% of the way through, which lands it near the top edge, and
     an 18% fade there hid the whole exit. The bottom still needs depth so new
     messages arrive gently. Both properties: WebKit still needs the prefix. */
  -webkit-mask-image: linear-gradient(180deg, transparent, #000 7%, #000 80%, transparent);
  mask-image: linear-gradient(180deg, transparent, #000 7%, #000 80%, transparent);
}
/* Absolute, and that is load-bearing. In flow, the track's height (which the
   script pads to three times the stage) would size the grid row, which sizes
   the stage, which sizes the padding — a runaway that grew the section to
   3400px. Out of flow, the row is sized by the head alone and the stage clips
   a track of any height. */
.chat-track {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
  will-change: transform;
}
.chat-msg { max-width: 33ch; }
.chat-msg[data-who="b"] { margin-left: auto; }
.chat-msg[data-who="c"] { margin-left: 8%; }
/* Grid, not flex: the name and the time share a row and the role sits on its
   own beneath. Wrapped as flex items the role folded up beside the name and
   read as part of it. */
.chat-meta {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: 1px 10px;
  margin: 0 0 6px;
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: 9px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.chat-meta b { grid-column: 1; font-weight: 600; color: var(--ink-2); }
.chat-meta .chat-ref { grid-column: 2; grid-row: 1; }
.chat-body {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--ink-2);
}

/* Variant C (default) — floating quotation. No frame and no role: just the
   line itself, a big display-face quote mark hanging off it, and a light
   attribution underneath. The mark is the only warm note on the page, which is
   what keeps it playful without tipping into cute-for-its-own-sake. */
/* Centred in the stage, not left-aligned to it. The stage runs from the end of
   the head column to the right edge of the measure, and a 34ch message parked
   at its left edge left the whole right half of that band empty. Every message
   is the same width, so centring keeps them aligned with each other. */
[data-chat="quote"] .chat-track { align-items: center; }

/* One column, every message on the same left edge. They used to alternate sides
   per speaker, which made the chain wander across the stage as it scrolled and
   left nowhere consistent for the eye to sit — and it fought the exit, which
   now throws every message the same way. */
[data-chat="quote"] .chat-msg {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 34ch;
  max-width: 100%;
  margin: 0;
  padding: 2px 0 0 34px;
  will-change: transform;
}
/* Georgia sets a quote mark in about the top quarter of its em box, so the type
   size runs well ahead of the ink you actually see: 55px draws roughly 14px of
   glyph. That is then multiplied by the stage's own 0.40, which is why its
   opacity sits high rather than at the .5 a decorative mark would usually take. */
[data-chat="quote"] .chat-msg::before {
  content: "“";
  position: absolute;
  left: 0;
  top: -14px;
  font-family: var(--f-display);
  font-size: 55px;
  line-height: 1;
  color: var(--accent);
  opacity: .85;
}
[data-chat="quote"] .chat-body {
  font-size: 14.5px;
  line-height: 1.62;
  color: var(--ink-2);
}
/* Attribution sits under the line it belongs to, so `order` rather than a
   different DOM order — the markup stays the same for all three variants. */
[data-chat="quote"] .chat-meta {
  order: 2;
  display: flex;
  gap: 8px;
  margin: 8px 0 0;
  font-size: 8.5px;
  letter-spacing: .16em;
}
[data-chat="quote"] .chat-meta b::before { content: "— "; color: var(--ink-3); }
[data-chat="quote"] .chat-meta .chat-ref { color: var(--ink-3); opacity: .8; }

/* Variant A — hairline box with a tail. Borrows the button's border and radius
   so it reads as part of the same system rather than a pasted-in chat widget. */
[data-chat="box"] .chat-msg {
  position: relative;
  border: 1px solid var(--line-strong);
  border-radius: 5px;
  background: var(--surface);
  padding: 11px 14px 12px;
}
/* Tail: two stacked squares, the inner one filled in the box's own background,
   which draws a 1px outlined notch without any SVG. */
[data-chat="box"] .chat-msg::before,
[data-chat="box"] .chat-msg::after {
  content: "";
  position: absolute;
  bottom: -5px;
  left: 22px;
  width: 9px;
  height: 9px;
  transform: rotate(45deg);
}
[data-chat="box"] .chat-msg::before { background: var(--line-strong); }
[data-chat="box"] .chat-msg::after { bottom: -4px; background: var(--surface); }
[data-chat="box"] .chat-msg[data-who="b"]::before,
[data-chat="box"] .chat-msg[data-who="b"]::after { left: auto; right: 22px; }

/* Variant B — transcript lines. No box; one rule down the left, as if exported
   from the system rather than rendered by a chat client. */
[data-chat="lines"] .chat-track { gap: 20px; padding-left: 18px; border-left: 1px solid var(--line-strong); }
[data-chat="lines"] .chat-msg { max-width: 40ch; margin-left: 0; }
[data-chat="lines"] .chat-body { color: var(--ink-2); }

@media (max-width: 1000px) {
  /* No column to spare, and the chain is decorative — drop it entirely rather
     than stacking it under the copy it is meant to sit beside. */
  .head-row { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .chat { display: none; }
}

/* three-column hairline grid */
.cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(268px, 1fr));
  margin-top: 52px;
  border-top: 1px solid var(--line-strong);
}
.col { padding: 26px 26px 30px; border-right: 1px solid var(--line); }
.col:last-child { border-right: none; }

/* Six-cell variant (the service list). Fixed at 3-up so it reads as two rows
   of three rather than whatever auto-fit happens to produce at a given width;
   the second row gets a top rule to separate it. */
.cols--grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.cols--grid .col:nth-child(3n) { border-right: none; }
.cols--grid .col:nth-child(n + 4) { border-top: 1px solid var(--line); }
.col h3 { margin-bottom: 12px; }
.col p { font-size: 15.5px; color: var(--ink-2); }

/* --- Booking --------------------------------------------------------------- */
/* One inline widget carrying all three lengths, inside a framed box with a bar
   across the top. The same box is used by the floating panel on every other
   page - .book-frame is shared, and each use adds only its own placement - so
   the calendar looks like one component wherever it appears.

   The mount inside is .book-cal, NOT .cal-embed: Cal.com's script injects its
   own stylesheet carrying `.cal-embed { margin: 0 auto; border: 0; min-height:
   300px }`, and that class name is theirs. Naming ours the same handed them
   control of our container - it centred against the page's left-aligned copy,
   dropped the frame and released the reserved height. Do not rename it back. */
.book-frame {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  background: var(--surface);
  overflow: hidden;
}
.book-frame-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 12px 11px 16px;
  border-bottom: 1px solid var(--line);
}
.book-frame-title {
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: 11px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
  margin-right: auto;
}
/* Cal gives you no way back to the list of lengths once one is chosen, so this
   is the way back. Built in script, and hidden until there is something to go
   back from. */
.book-frame-btn {
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: 11px;
  letter-spacing: .06em;
  padding: 6px 11px;
  border: 1px solid var(--line-strong);
  border-radius: 5px;
  background: var(--surface);
  color: var(--ink-2);
  cursor: pointer;
  transition: border-color .18s, color .18s, background .18s;
}
.book-frame-btn--x { padding: 6px 9px; line-height: 1; font-size: 13px; }
.book-frame-btn:hover { border-color: var(--accent); color: var(--ink); background: var(--accent-soft); }
.book-frame-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The booker's own growth is contained here rather than pushing the page. */
.book-frame-body { overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
.book-frame-body iframe { display: block; width: 100%; border: 0; }

/* The booking page's use: in the flow of the page, not floating. */
.book-inline {
  /* Wider than the page's 46rem measure on purpose. Cal's booker switches to a
     stacked layout below ~56rem: the month grid, then the times underneath it,
     which made the widget jump from 560px to 1468px the moment a date was
     picked. Measured at 46/56/64/72rem - 56rem and above hold at ~570px with
     the times beside the grid. 60rem leaves headroom above that threshold. */
  max-width: 60rem;
  margin-top: 34px;
}
/* Reserves height so the page does not jump when the widget arrives. */
.book-inline .book-cal { min-height: 570px; }

.js .book-plain { display: none; }
.book-plain { max-width: 46rem; margin-top: 34px; }
/* The widget cannot run without scripting, so the whole box is hidden there
   and the plain links carry the page instead. */
html:not(.js) .book-inline, html:not(.js) .book-note { display: none; }

.book-note { margin-top: 16px; font-size: 13.5px; color: var(--ink-3); max-width: 60ch; }

/* The three lengths. Cal's own list shows only "15 min meeting" with no
   description, so the guidance has to live here. A hairline list rather than
   buttons: it informs, it is not a second control competing with the widget. */
.book-lengths {
  list-style: none;
  margin: 26px 0 0;
  padding: 0;
  max-width: 44rem;
  border-top: 1px solid var(--line);
}
.book-lengths li {
  display: grid;
  grid-template-columns: 7.5rem minmax(0, 1fr);
  gap: 4px 20px;
  padding: 13px 0;
  border-bottom: 1px solid var(--line);
}
.book-lengths b {
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: 12px;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--ink-2);
  padding-top: 2px;
}
.book-lengths span { font-size: 15.5px; line-height: 1.5; color: var(--ink-2); }
@media (max-width: 520px) {
  .book-lengths li { grid-template-columns: minmax(0, 1fr); gap: 3px; }
}

.book-legend {
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: 12px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-2);
  margin-bottom: 14px;
}
.book-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.book-list a {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 18px 18px 20px;
  border: 1px solid var(--line-strong);
  border-radius: 5px;
  background: var(--surface);
  text-decoration: none;
  color: var(--ink);
  transition: border-color .18s, background .18s;
}
.book-list b { font-size: 16px; font-weight: 600; letter-spacing: -0.008em; }
.book-list span { font-size: 13.5px; line-height: 1.45; color: var(--ink-3); }
.book-list a:hover { border-color: var(--accent); background: var(--accent-soft); }

@media (max-width: 640px) { .book-inline .book-cal { min-height: 620px; } }

/* The floating book button. Built by js/float-book.js, so it only exists where
   it can work; there is no markup for it and no no-JS state to style.

   Deliberately quiet. This site's register is hairlines and restraint, and a
   high-contrast bubble in the corner would be the loudest thing on the page by
   some distance. It borrows .btn's proportions and border rather than its
   uppercase mark face, sits below the header in the stacking order (60), and
   only fills on hover. */
.book-float {
  position: fixed;
  right: 22px;
  bottom: 22px;
  z-index: 50;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 12px 17px;
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: 12px;
  letter-spacing: .02em;
  border: 1px solid var(--ink-3);
  border-radius: 6px;
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
  /* Lifted off the page just enough to read as floating, not as a card. */
  box-shadow: 0 1px 2px rgba(0, 0, 0, .05), 0 6px 18px rgba(0, 0, 0, .07);
  transition: background .2s ease, border-color .2s ease, box-shadow .2s ease;
}
/* .ico is the column-icon rule, where the icon sits above a heading and carries
   margin-bottom: 12px. In a centred flex row that margin pushes the icon 6px
   above the label - measured. Reset it, and take the label's colour so the two
   halves of the button read as one thing. */
.book-float .ico { width: 15px; height: 15px; flex: none; margin: 0; color: inherit; }
.book-float:hover {
  background: var(--accent-soft);
  border-color: var(--accent);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .06), 0 10px 26px rgba(0, 0, 0, .10);
}
.book-float:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* Narrow screens: the label would crowd the content, and thumbs reach the
   bottom-right corner. Tightened rather than reduced to an icon, because an
   unlabelled circle in the corner does not say what it does. */
@media (max-width: 520px) {
  .book-float { right: 14px; bottom: 14px; padding: 11px 14px; font-size: 11.5px; }
}

/* The floating button's use of the same box: positioned, lifted, and bounded
   in height. Deliberately NOT Cal's modal - that dims the whole page and takes
   it over, which is a heavy response to "show me some times" and loses the
   page behind it. This expands upward from the button and leaves the page
   readable beside it.

   MUST keep the [hidden] rule below. The hidden attribute only carries
   `display: none` from the UA stylesheet, which any class rule setting display
   beats on specificity - so .book-frame's `display: flex` alone leaves the
   panel on screen permanently and makes the close button do nothing. */
.book-pop[hidden] { display: none; }
.book-pop {
  position: fixed;
  right: 22px;
  bottom: 86px;
  z-index: 55;
  /* Sized past the ~56rem threshold where Cal stops stacking, where there is
     room for it; the body scrolls where there is not. */
  width: min(920px, calc(100vw - 44px));
  max-height: min(680px, calc(100vh - 150px));
  box-shadow: 0 2px 6px rgba(0, 0, 0, .06), 0 18px 50px rgba(0, 0, 0, .16);
}
.book-pop .book-frame-body > div { min-height: 520px; }

/* Phones: there is no "beside the page" to preserve, so it takes the width it
   needs and sits above the button. */
@media (max-width: 720px) {
  .book-pop {
    right: 12px;
    left: 12px;
    bottom: 74px;
    width: auto;
    max-height: calc(100vh - 130px);
  }
}

/* --- Articles -------------------------------------------------------------- */
/* Long-form reading, which the rest of the site does not do: the homepage is
   scanned in blocks, an article is read in sequence. Measure comes down to ~68ch
   and leading opens up, but the type stack and rules stay the house ones. */
.article-head { max-width: 46rem; }
/* Headings carry margin: 0, so without this the lead sits flush against the
   h1's descenders. .head does the same thing for the sections that use it;
   .article-head simply never got the equivalent. */
.article-head h1 + p { margin-top: 20px; }
.article-meta {
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: 10.5px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-3);
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  margin-top: 20px;
}
.article-meta span + span::before { content: "\00b7"; margin-right: 16px; color: var(--line-strong); }

/* 40rem lands body copy near 73 characters at 16.5px; 42rem ran to 77, just
   past the comfortable ceiling. */
.prose { max-width: 40rem; margin-top: 44px; }
.prose > * + * { margin-top: 20px; }
.prose h2 {
  font-size: 27px;
  line-height: 1.16;
  letter-spacing: -0.016em;
  /* The global h2 caps at 20ch, which would cut the section rule short at the
     end of the text rather than carrying it across the column. Headings here are
     short enough not to need the cap. */
  max-width: none;
  margin-top: 54px;
  padding-top: 26px;
  border-top: 1px solid var(--line);
}
.prose h3 { font-size: 19px; margin-top: 34px; }
.prose p, .prose li { font-size: 16.5px; line-height: 1.68; color: var(--ink-2); max-width: none; }
.prose strong { color: var(--ink); font-weight: 600; }
.prose ul, .prose ol { padding-left: 22px; display: flex; flex-direction: column; gap: 9px; }
.prose li::marker { color: var(--ink-3); }
/* Pulled-out statutory point. Not a quotation - a thing the reader should not
   skim past - so it takes the accent rule rather than quote marks. */
.prose .callout {
  border-left: 2px solid var(--accent);
  background: var(--accent-soft);
  padding: 16px 20px;
  font-size: 16px;
}
.prose .callout p { font-size: 16px; }
.prose .callout p + p { margin-top: 10px; }
.prose .source {
  font-size: 13.5px;
  color: var(--ink-3);
  border-top: 1px solid var(--line);
  padding-top: 14px;
}

/* --- Download gate --------------------------------------------------------- */
/* The article itself is public and indexable; this exchanges the formatted PDF
   for a way to follow up. Nothing here is hidden from a crawler. */
.gate {
  max-width: 42rem;
  margin-top: 56px;
  border: 1px solid var(--line-strong);
  background: var(--surface);
  padding: 30px 30px 34px;
}
.gate h2 { font-family: var(--f-mark); font-weight: 600; font-size: 15px; letter-spacing: .04em; margin: 0 0 10px; }
.gate > p { font-size: 15.5px; color: var(--ink-2); }
.gate-fields { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; margin: 22px 0 6px; }
.gate .field { margin: 0; }
.gate-consent { display: flex; gap: 10px; align-items: flex-start; margin-top: 18px; font-size: 13.5px; color: var(--ink-3); }
.gate-consent input { margin-top: 3px; flex: none; }
.gate-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; margin-top: 22px; }
.gate-done { display: none; }
.gate.is-done .gate-form { display: none; }
.gate.is-done .gate-done { display: block; }
@media (max-width: 700px) { .gate-fields { grid-template-columns: minmax(0, 1fr); } }

/* --- Article list ---------------------------------------------------------- */
.posts { margin-top: 46px; border-top: 1px solid var(--line-strong); }
.post {
  display: grid;
  grid-template-columns: 150px minmax(0, 1fr);
  gap: 6px 28px;
  padding: 28px 0;
  border-bottom: 1px solid var(--line);
}
.post-kind {
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-3);
  padding-top: 5px;
}
.post-body { display: flex; flex-direction: column; gap: 9px; }
.post h3 { font-size: 23px; line-height: 1.2; }
.post h3 a { text-decoration: none; border-bottom: 1px solid var(--line-strong); }
.post h3 a:hover { border-bottom-color: var(--accent); }
.post p { font-size: 15.5px; color: var(--ink-2); }
.post-tags { font-family: var(--f-mark); font-weight: 600; font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-3); }
@media (max-width: 700px) { .post { grid-template-columns: minmax(0, 1fr); gap: 10px; } .post-kind { padding-top: 0; } }

/* --- People (about page) --------------------------------------------------- */
/* Three partners, so the grid is fixed at 3-up rather than auto-fit: a partner
   sitting alone on a second row reads as an afterthought. Below 860px it goes
   to one column and the portrait shrinks, because a stacked column of tall
   portraits pushes the bios off the screen. */
.people {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0;
  margin-top: 52px;
  border-top: 1px solid var(--line-strong);
}
.person {
  padding: 30px 28px 34px;
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.person:last-child { border-right: none; }
/* 4:5 portrait. The placeholder carries a hairline and a label so an unfilled
   slot reads as deliberately pending rather than as a broken image. */
.person-shot {
  aspect-ratio: 4 / 5;
  background: var(--surface-2);
  border: 1px solid var(--line);
  margin-bottom: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
/* <picture> is an inline wrapper with no height of its own, so `height: 100%`
   on the img inside it has nothing to resolve against and object-fit never
   engages. The two portrait photographs hid this by overflowing on width; the
   landscape one letterboxed with grey bands above and below it. The wrapper
   has to become a block that fills the frame before any of the rules below
   mean anything. */
.person-shot picture { display: block; width: 100%; height: 100%; }

/* The three photographs do not share an aspect ratio - measured 0.75, 0.67 and
   1.11 against a 4/5 frame - so cover is doing real cropping, not tidying.
   The vertical anchor is high because a face sits in the upper third of every
   one of them; centring the crop takes the top of a head off the tallest and
   leaves the shortest looking down at its own tie. */
.person-shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 22%;
  display: block;
}
.person-shot span {
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.person h3 { font-size: 20px; }
.person-role {
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: 10.5px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.person-bio { font-size: 15px; color: var(--ink-2); }
.person-link { font-size: 14px; margin-top: auto; padding-top: 10px; text-align: right; }
/* Icon-only. A 36px target rather than the 20px glyph: WCAG 2.2 AA asks for
   24x24 and a lone icon has no text beside it to enlarge the hit area. */
.person-link a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  margin-right: -8px;         /* optical: aligns the glyph, not its padding */
  border-radius: 4px;
  color: var(--ink-3);
  text-decoration: none;
  transition: color .18s, background .18s;
}
.person-link a:hover { color: var(--accent); background: var(--accent-soft); }
.person-link a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.person-link svg { width: 20px; height: 20px; }

/* The about page leads with an h1 in a band rather than the hero, so it takes
   heading proportions instead of the hero's display size. */
.about-h1 { font-size: clamp(30px, 4vw, 48px); line-height: 1.07; letter-spacing: -0.022em; max-width: 20ch; }
/* Current page in the nav */
.top nav a.here { color: var(--ink); border-bottom-color: var(--ink-3); }

@media (max-width: 860px) {
  .people { grid-template-columns: minmax(0, 1fr); }
  .person { border-right: none; border-bottom: 1px solid var(--line); }
  .person:last-child { border-bottom: none; }
  .person-shot { aspect-ratio: 3 / 2; }
}

/* --- Pairs ----------------------------------------------------------------- */
.pairs { margin-top: 26px; border-top: 1px solid var(--line); }
.pair {
  display: grid;
  grid-template-columns: 168px 1fr;
  gap: 18px;
  padding: 11px 0;
  border-bottom: 1px solid var(--line);
  align-items: baseline;
}
.pair b {
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: 10.5px;
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.pair span { font-size: 15px; color: var(--ink-2); }

/* --- Cell grid (delivery routes, source inventory) --------------------------
   Only the top and left edges are drawn on the container; every cell draws its
   own right and bottom. That avoids the doubled 1px line you get when a
   fully-bordered container meets fully-bordered cells.
   Column counts are explicit rather than auto-fit so the final row is never
   ragged — an open-ended row leaves the grid looking unfinished. Item counts
   are chosen to divide evenly: 6 cells at 3-up and at 2-up. */
.routes {
  display: grid;
  margin-top: 46px;
  border-top: 1px solid var(--line);
  border-left: 1px solid var(--line);
  background: var(--surface);
}
.routes--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

.route { padding: 20px 22px 22px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); }
/* Lucide geometry, drawn on a 24 grid at stroke 2. Rendered at 20px that lands
   near 1.67px on screen, which sits with the hairline rules rather than shouting
   over them. Quiet ink: the icon is a wayfinding aid, not the content, so it
   takes the supporting colour rather than the heading's.
   One rule for every icon on the page - the system grid and the three column
   sets - so they cannot drift apart. */
.ico {
  display: block;
  width: 20px;
  height: 20px;
  margin-bottom: 12px;
  color: var(--ink-3);
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The columns set their headings at reading size with more air around them than
   the compact system cells, so the icon needs a little more room to match. */
.col .ico { margin-bottom: 14px; }
/* The system name is what you scan for, so it gets the body face at reading
   size rather than the mono metadata voice used by .kicker and .strip. Mono at
   12px with wide tracking reads as code, which is wrong for a product name and
   was the main reason this grid was hard to skim. */
.route b {
  display: block;
  font-family: var(--f-body);
  font-weight: 600;
  font-size: 15.5px;
  line-height: 1.3;
  letter-spacing: -0.008em;
  color: var(--ink);
  margin-bottom: 4px;
}
/* Supporting fragment, deliberately quiet and deliberately short — a sentence
   here multiplied by twelve cells is a wall, not a list. */
.route span {
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--ink-3);
  display: block;
}
/* Secondary set: same component, less air, so it reads as the shorter list. */

/* --- Note ------------------------------------------------------------------ */
.note {
  margin-top: 40px;
  border-left: 2px solid var(--accent);
  background: var(--accent-soft);
  padding: 18px 22px;
}
.note p { font-size: 15px; color: var(--ink-2); max-width: 78ch; }
.note strong { color: var(--ink); font-weight: 600; }
/* A note that ends in a call to action puts the button at the end of the
   message rather than on a row beneath it. Flex, not an inline button: a .btn
   has real padding and would prise the paragraph's line box apart mid-sentence.
   Wrapping is left on so the button drops below the text on narrow screens. */
.note--cta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 18px 28px;
}
.note--cta p { flex: 1 1 34ch; margin: 0; }
.note--cta .btn { flex: none; }

/* --- Disclosures ----------------------------------------------------------- */
.answers { margin-top: 46px; border-top: 1px solid var(--line-strong); }
details { border-bottom: 1px solid var(--line); }
summary {
  list-style: none;
  cursor: pointer;
  padding: 20px 40px 20px 0;
  position: relative;
  font-family: var(--f-display);
  font-size: 20px;
  letter-spacing: -0.012em;
}
summary::-webkit-details-marker { display: none; }
summary::after {
  content: "";
  position: absolute;
  right: 6px;
  top: 50%;
  width: 9px;
  height: 9px;
  margin-top: -6px;
  border-right: 1px solid var(--ink-3);
  border-bottom: 1px solid var(--ink-3);
  transform: rotate(45deg);
  transition: transform .2s ease;
}
details[open] summary::after { transform: rotate(-135deg); margin-top: -2px; }
details p { padding: 0 0 22px; color: var(--ink-2); font-size: 15.5px; }

/* --- Second tint ----------------------------------------------------------- */
/* Was .supply: an inverted band that painted itself --ink, i.e. near-black in
   light mode. It only ever needed to be *a third ground*, not an inversion, and
   the inversion cost it a private set of band-fg colours for its heading, lead,
   hairlines and button. --surface-2 is the token that already meant "one step
   off the page", and it sits clear of both neighbours in either theme (22 and
   11 channel steps in light, 14 and 25 in dark), so this band now needs
   nothing but a background and every child keeps the page's own colours. */
section.band.tint-2 { background: var(--surface-2); }

/* --- Contact --------------------------------------------------------------- */
.form { margin-top: 44px; max-width: 620px; }
.field { border-bottom: 1px solid var(--line-strong); padding: 8px 0 0; margin-bottom: 22px; }
.field label {
  display: block;
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: 9.5px;
  letter-spacing: .15em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 6px;
}
.field input, .field textarea {
  width: 100%;
  border: none;
  background: none;
  color: var(--ink);
  font-family: var(--f-body);
  font-size: 16px;
  padding: 4px 0 10px;
  resize: vertical;
}
.field input:focus, .field textarea:focus { outline: none; }
.field:focus-within { border-bottom-color: var(--accent); }
/* Prose, not a label — the brand face only ships at 600, and a five-line note
   set in semibold 11px reads as emphasis rather than as fine print. */
.form-note { font-family: var(--f-body); font-size: 11.5px; color: var(--ink-3); margin-top: 18px; line-height: 1.7; }
.form-status { font-family: var(--f-mark); font-weight: 600; font-size: 12px; color: var(--accent); margin-top: 16px; min-height: 1.2em; }
/* Every field is required, so one line says it once rather than four asterisks
   saying it four times. */
.form-hint {
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: 10.5px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 22px;
}
/* The browser flags an empty required field on submit; mirror that on the
   field's own underline so the error is visible where the problem is, not only
   in the bubble. :user-invalid rather than :invalid so nothing turns red before
   anyone has typed. */
.field:has(:user-invalid) { border-bottom-color: var(--bad); }
.field:has(:user-invalid) label { color: var(--bad); }

/* --- Footer ---------------------------------------------------------------- */
/* Tinted, not ruled. Contact is a plain band and the footer used to sit on the
   same ground with only a hairline between them — and the hairlines are gone.
   Taking --surface continues the page's plain/tint alternation, so the footer
   separates itself by the same mechanism every other boundary uses. */
footer {
  background: var(--surface);
  padding: 56px 0 0;
  font-family: var(--f-body);
  /* Was 11.5px in --ink-3, which measured 4.43:1 - under the 4.5 AA needs for
     text this size. Larger and one step darker clears it with room to spare and
     makes the footer readable rather than merely present. */
  font-size: 13px;
  line-height: 1.7;
  color: var(--ink-2);
}
/* Two columns: the legal text, and how to reach us. The name is no longer here
   - it sits centred beneath both, where a colophon belongs. */
.foot {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 36px 80px;
  align-items: start;
  padding-bottom: 44px;
}
.foot-privacy { max-width: 58ch; }
.foot-privacy a { color: var(--accent); }

/* Contact. Rows are deliberately taller than the text needs: at 15.5px with
   10px of vertical padding each row is ~43px, which is a real touch target on a
   phone rather than a line of text you have to aim at. */
.foot-links {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.foot-links a, .foot-place {
  display: inline-flex;
  align-items: center;
  gap: 11px;
  font-size: 15.5px;
  line-height: 1.4;
  padding: 10px 0;
  color: var(--ink-2);
  text-decoration: none;
}
.foot-links a {
  border-bottom: 1px solid transparent;
  transition: color .18s, border-color .18s;
}
.foot-links a:hover { color: var(--ink); border-bottom-color: var(--line-strong); }
.foot-ico {
  width: 17px;
  height: 17px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  color: var(--ink-3);
  transition: color .18s;
}
.foot-links a:hover .foot-ico { color: var(--accent); }

/* A brand mark is a filled glyph, not a hairline drawing, so it has to opt out
   of the stroke treatment every other icon here uses. LinkedIn's mark is the
   only one on the site; drawing it in outline would make it unrecognisable,
   which defeats the point of using the mark at all. */
.foot-ico--brand, .ico--brand {
  fill: currentColor;
  stroke: none;
}
/* Icon-only, so the row is the icon. Squared off and slightly larger than the
   labelled icons beside it, which sit next to 15.5px text and borrow its
   height; alone it has to hold the line on its own. */
.foot-links .foot-link--icon {
  width: 40px;
  height: 40px;
  /* NOT centred. The mail, phone and location rows below all start their icon
     at the left edge of the list, and a centred glyph in a 40px box sat 12px
     to the right of that column - close enough to read as a mistake rather
     than a decision. Left-aligned, the icons line up and the 40px box is
     purely the touch target. */
  justify-content: flex-start;
  padding: 0;
  gap: 0;
}
.foot-links .foot-link--icon .foot-ico { width: 19px; height: 19px; }
/* No underline to grow: the border-bottom treatment the text links use would
   sit under a square icon as a stray dash. */
.foot-links .foot-link--icon:hover { border-bottom-color: transparent; }
.foot-links .foot-link--icon:hover .foot-ico { color: var(--accent); }
/* Where we are, not somewhere to go, so it is not a link and does not respond
   to the pointer. */
.foot-place { cursor: default; }

/* Colophon. Centred under everything, separated by the same hairline the rest
   of the page uses for boundaries, and set in the brand face at label size so it
   reads as a signature rather than as another line of content. */
.foot-base {
  border-top: 1px solid var(--line);
  margin: 0;
  padding: 26px 0 36px;
  text-align: center;
  max-width: none;
}
.foot-base b {
  font-family: var(--f-mark);
  font-weight: 600;
  font-size: 11.5px;
  letter-spacing: .18em;
  color: var(--ink-3);
}

@media (max-width: 760px) {
  /* One column, and the contact list carries a rule so it still reads as its
     own group once it sits under the privacy text rather than beside it. */
  .foot { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .foot-id { white-space: normal; border-top: 1px solid var(--line); padding-top: 22px; margin-top: 26px; }
}

/* --- Scroll reveal --------------------------------------------------------- */
/* Gated on the `js` class the head script sets, so the hidden state only ever
   exists where something is guaranteed to undo it. With scripting off - or if
   reveal.js simply fails to load - the content is visible rather than a blank
   page below the hero. */
.js .reveal { opacity: 0; transform: translateY(28px); transition: opacity .6s ease, transform .6s ease; }

/* --- Responsive ------------------------------------------------------------ */
@media (max-width: 900px) {
  :root { --gutter: 20px; }
  /* Section anchors are already gone by this width and the page links go at 620;
     both are handled by the tiered rules up with the nav itself. */
  /* The nav collapses to Contact here, so the theme control drops its word and
     runs on the icon alone. Losing the text frees the room to make it a proper
     touch target, which at 6px padding it was not. The button keeps its
     aria-label, so nothing is lost for anyone not reading the glyph. */
  .theme-lab { display: none; }
  .theme-btn { padding: 0; width: 40px; height: 40px; }
  .theme-ico { width: 16px; height: 16px; }
  .hero::after {
    background: linear-gradient(
      180deg,
      var(--ground) 0%,
      color-mix(in srgb, var(--ground) 72%, transparent) 46%,
      transparent 100%
    );
  }
  .col { border-right: none; border-bottom: 1px solid var(--line); }
  .col:last-child { border-bottom: none; }
  .cols--grid { grid-template-columns: 1fr; }
  .cols--grid .col:nth-child(n + 4) { border-top: none; }
  .pair { grid-template-columns: 1fr; gap: 2px; }
}

/* Cell grids step down before the columns get too narrow to read. */
@media (max-width: 1000px) {
  .routes--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 620px) {
  .routes--3 { grid-template-columns: 1fr; }
}

/* --- Reduced motion -------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .reveal { opacity: 1 !important; transform: none !important; transition: none; }
  /* The scrub never binds under reduced motion, so the stage would stay at
     opacity 0. Show it held still instead — the content is the point, the
     movement is not. */
  .chat { opacity: .34 !important; }
  .chat-track { transform: none !important; }
  .chat-msg { opacity: 1 !important; }
  * { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}
