*,
*:before,
*:after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  text-decoration: none;
  object-fit: cover;
}

/* TYPE ROLES
   Poppins (--font-display) is for display headings at --text-4xl (30px) and
   above. Inter (--font-sans) carries card titles and labels below that, facts,
   navigation and every form control. Literata is article prose. A small
   heading that wants more presence gets weight (--weight-*), not Poppins. */

html {
  scroll-behavior: smooth;
}

ul {
  list-style: none;
}

body {
  font-family: var(--font-sans);
  font-size: 16px;
  color: var(--color-black);
  line-height: var(--leading-normal);
  font-weight: 400;

  /* Nothing previously asked for kerning or ligatures. Inter carries both,
     plus an optical size axis the browser drives from the rendered size. */
  font-kerning: normal;
  font-variant-ligatures: common-ligatures contextual;
  font-optical-sizing: auto;
  font-variant-numeric: proportional-nums;

  /* Real weights are shipped, so a synthesised one is a defect rather than a
     fallback: it would silently mask a missing face. */
  font-synthesis: none;

  text-rendering: optimizeLegibility;

  /* The footer sits at the bottom of the viewport on a page shorter than it.
     The 404 was the first such page once its masthead became the band; the
     photograph had been padding the page past the fold. */
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

main {
  flex: 1 0 auto;
}

hr {
  border: 1px solid var(--color-hairline);
}

/* Every container keeps the one gutter (tokens.css), so the logo, the band
   and each section start on the same left edge at every width. */
.container {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

.container--narrow {
  max-width: 930px;
  margin: 0 auto;
  padding: 0 var(--gutter);
}

/* Articles are wider than .container--narrow because they carry a rail beside
   the text. Deliberately not .container: that one is the site chrome's and
   carries overrides an article must not inherit — 855px at 1024 and none at
   920, both of which would collapse the rail's column at the wrong moment. */
.container--article {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

.title {
  font-family: var(--font-display);
  font-size: var(--type-title);
  font-weight: 600;
  color: var(--color-white);
}

.title--black {
  font-weight: 600;
  color: var(--color-black);
}

/* HEADER AND PRIMARY MENU
   ---------------------------------------------------------------------------
   Audited 2026-10-20 (docs/visual-assessment/2026-10-20-menu.md). The
   dropdowns were white panels placed by fixed offsets (right: 279px, 140px)
   under a rotated-square nub. The focus ring inside them was cyan on white,
   1.69:1; the panel had no edge over --color-surface, 1.10:1; and the ten
   services wrapped into a 736px column that ran past the fold at 1280x720.
   The panels now continue the ink chrome, hang from the header's bottom edge,
   and are anchored to their item or to the menu, never to a measured offset.
   --------------------------------------------------------------------------- */

.header {
  background-color: var(--color-ink);
  width: 100%;
  padding: var(--space-2) 0;
}

.sub-menu__list {
  display: none;
}

/* Practice headings inside the services panel and the drawer: the cyan kicker
   the bands use on dark ("About StarDevs", "Founded"). They were
   --color-muted, 4.99:1 on the old white panel but 4.09:1 on the ink drawer —
   under 4.5:1 for 12px text, and no PAIRS entry covered that ground. */
.sub-menu__group {
  display: block;
  padding: var(--space-2) var(--space-4) var(--space-1);
  color: var(--color-cyan);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  white-space: nowrap;
}

/* Not the menu's: the practice and "Related services" headings on the
   services pages, which shared the old dropdown's comment. */
.services-practice {
  margin: var(--space-8) 0 var(--space-4);
  font-family: var(--font-sans);
  font-size: var(--type-subsection);
  line-height: var(--leading-snug);
  font-weight: var(--weight-semibold);
  color: var(--color-white);
}

.sub-menu__link {
  display: block;
  padding: var(--space-2) var(--space-4);
  color: var(--color-white);
  font-size: var(--text-base);
  white-space: nowrap;
}

.sub-menu__link:hover,
.sub-menu__link:active {
  color: var(--color-cyan);
}

/* The service you are on, in the menu's own current-page language: an
   underline in the link's colour. 2px, not the top level's 3px, because the
   label is 16px, not 18-20px. */
.sub-menu__link[aria-current] {
  color: var(--color-cyan);
  text-decoration: underline;
  text-decoration-thickness: var(--border-2);
  text-underline-offset: var(--space-1);
}

@media (min-width: 1025px) {
  .menu__item--about {
    position: relative;
  }

  .sub-menu__list {
    position: absolute;
    /* The item ends at the nav's bottom edge, above the header's padding. The
       panel starts below that padding, on the header's own edge. */
    top: calc(100% + var(--space-2));
    z-index: 5;
    padding: var(--space-2) 0;
    background-color: var(--color-ink);
    border: var(--border-1) var(--border-style) var(--color-cyan-50);
    border-top-color: var(--color-cyan);
  }

  /* A transparent bridge over that padding. Without it the pointer leaves the
     item on its way down and the panel closes under it. The old hover rule
     did this with a 30px padding-bottom, and drew a second underline. */
  .sub-menu__list::before {
    content: "";
    position: absolute;
    right: 0;
    bottom: 100%;
    left: 0;
    height: calc(var(--space-2) + var(--border-1));
  }

  /* Hover alone left the panels unreachable from the keyboard (WCAG 2.1.1):
     the links inside were display:none, so Tab skipped them. :focus-within
     opens the same list; Escape dismisses it without moving focus (1.4.13),
     which index.js records as .menu-dismissed until focus or the pointer
     leaves. */
  .menu
    > .menu__item:is(:hover, :focus-within):not(.menu-dismissed)
    > .sub-menu__list {
    display: grid;
  }

  /* The trigger stays lit while the pointer is inside its panel. */
  .menu > .menu__item:is(:hover, :focus-within) > .menu__link {
    color: var(--color-cyan);
  }

  /* Under About, the entries line up with the label above them. */
  .sub-menu__list--about {
    left: calc(-1 * var(--space-4));
    min-width: max-content;
  }

  /* Services sets the two practices side by side and hangs from the menu's
     right edge, which is the container's, so it never runs off the viewport. */
  .sub-menu__list--services {
    right: 0;
    grid-template-columns: repeat(2, max-content);
  }

  .sub-menu__link:hover {
    background-color: var(--color-navy-soft);
  }
}

/* ACCESSIBILITY
   ---------------------------------------------------------------------------
   The project had no :focus rule at all. Nothing suppressed the UA ring, but
   it was never designed either, so it is close to invisible on the cyan button
   and on the near-black header. These give every control a ring drawn in the
   brand cyan, with a white outer ring so it also reads on dark grounds.
   --------------------------------------------------------------------------- */

:focus-visible {
  /* Indigo, not cyan. Cyan measures 1.69:1 on white and 1.54:1 on
     --color-surface, well under the 3:1 SC 1.4.11 requires of a UI boundary,
     and the white-halo compensation below was only ever scoped to the dark
     chrome — i.e. to the grounds where cyan already measured 12:1 and needed
     no help. Every focusable control on a light ground had an effectively
     invisible ring. No single colour works on both: indigo is 11.45:1 on
     white and 1.79:1 on ink, the exact inverse of cyan. Light grounds are the
     majority surface, so they get the default and the dark chrome overrides. */
  outline: 3px solid var(--color-indigo);
  outline-offset: 2px;
}

/* On the dark header and dropdown the cyan ring sits on a near-black ground;
   the white halo keeps it visible against both. */
.header :focus-visible,
.footer :focus-visible,
.consent :focus-visible,
.services-item:focus-visible,
.services-row:focus-visible {
  outline-color: var(--color-cyan);
  box-shadow: 0 0 0 5px rgb(255 255 255 / 0.35);
}

/* Available to assistive technology, not painted. Used for the form labels
   this design carries as placeholders only — a placeholder is not an
   accessible name, and it disappears on the first keystroke. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Skip link: the first focusable element on every page. Off-screen until
   focused, because ~20 nav links precede the content on all 8 pages. */
.skip-link {
  position: absolute;
  left: 8px;
  top: -100%;
  z-index: 10;
  padding: 12px 20px;
  background-color: var(--color-cyan);
  color: var(--color-ink);
  font-weight: 600;
  text-decoration: none;
}

.skip-link:focus-visible {
  top: 8px;
  outline-offset: 2px;
}

/* scroll-behavior: smooth is set globally at the top of this file with no way
   to opt out. Honour the OS setting. */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

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

/* BURGER MENU */

.menu-btn {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  color: inherit;
  font: inherit;
  cursor: pointer;
  /* Bars stay 30px; the box grows to the touch-target floor around them.
     Each span is centred on the button with top/left 50% + transform, so
     enlarging the button re-centres them for free. */
  width: var(--touch-target);
  height: var(--touch-target);
  position: relative;
  z-index: 2;
  overflow: hidden;
  display: none;
}

.menu-btn span {
  width: 30px;
  height: 2px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background-color: var(--color-white);
  transition: all var(--duration-base) var(--ease-base);
}

.menu-btn span:nth-of-type(2) {
  top: calc(50% - 8px);
}

.menu-btn span:nth-of-type(3) {
  top: calc(50% + 8px);
}

/* NAV */
.nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: 0;
}

/* The menu runs the nav's full height, and each item with it, so an item's
   box reaches the header's padding and its panel can hang from there. */
.menu {
  position: relative;
  display: flex;
  align-self: stretch;
}

.menu__item {
  display: flex;
  align-items: center;
}

.menu__item + .menu__item {
  margin-left: var(--space-10);
}

/* inline-flex trims the whitespace prettier leaves inside "Services", which
   otherwise set its chevron a space further out than About's. */
.menu__link {
  display: inline-flex;
  align-items: center;
  color: var(--color-white);
  font-size: var(--text-md);
}

/* One state language. Hover, press and an open panel turn the link cyan; the
   current page adds an underline in the link's colour. The old hover drew a
   3px border at the header's bottom edge instead, so hovering the current
   page showed two lines. */
.menu__link:hover,
.menu__link:active,
.menu__link[aria-expanded="true"] {
  color: var(--color-cyan);
}

/* The page you are on is underlined, not only announced: the cue is a line, so
   it survives without colour (WCAG 1.4.1). */
.menu__link[aria-current] {
  text-decoration: underline;
  text-decoration-thickness: var(--border-3);
  text-underline-offset: var(--space-2);
}

/* The chevron says a link opens a panel, which nothing said on desktop. A mask
   on currentColor, as the logo is, so it follows the link through every
   state; the image it replaces carried its own cyan and could not. Sized in
   em so it scales with the label. */
.menu__link--about::after,
.menu__link--services::after {
  content: "";
  width: 0.375em;
  height: 0.625em;
  margin-left: var(--space-2);
  background-color: currentColor;
  -webkit-mask: url("images/arrow-right.svg") no-repeat center / contain;
  mask: url("images/arrow-right.svg") no-repeat center / contain;
  transform: rotate(90deg);
  transition: transform var(--duration-base) var(--ease-base);
}

.menu__link--about[aria-expanded="true"]::after,
.menu__link--services[aria-expanded="true"]::after {
  transform: rotate(-90deg);
}

/* The site's one call to action, in the chrome: the hero's cyan button. */
.menu__cta {
  white-space: nowrap;
}

/* The logo was 13 inline <path> elements pasted into the header and footer of
   every page — 190 KB of identical, uncacheable markup across the site, a
   third of all the HTML shipped. It is now one cached file, painted through a
   mask so CSS still controls its colour and the hover and active states
   survive; an <img> could not be recoloured. */
.logo__img {
  /* inline-block, not block: the inline <svg> this replaces sat on the text
     baseline and carried its descender gap, so a block element makes every
     page seven pixels shorter. Matching the old box keeps the refactor
     pixel-identical. */
  display: inline-block;
  vertical-align: baseline;
  width: 172px;
  height: 66px;
  background-color: var(--color-white);
  -webkit-mask: url("images/logo.svg") no-repeat center / contain;
  mask: url("images/logo.svg") no-repeat center / contain;
}

.logo:hover .logo__img {
  background-color: var(--color-mint);
}

.logo:active .logo__img {
  background-color: var(--color-green);
}

@media all and (max-width: 1024px) {
  .container {
    max-width: var(--container-tablet);
  }

  .menu-btn {
    display: block;
  }

  .menu-btn.active span:nth-of-type(1) {
    display: none;
  }

  .menu-btn.active span:nth-of-type(2) {
    top: 50%;
    transform: translate(-50%, 0%) rotate(45deg);
  }

  .menu-btn.active span:nth-of-type(3) {
    top: 50%;
    transform: translate(-50%, 0%) rotate(-45deg);
  }

  /* The drawer is a sheet from the header's bottom edge to the viewport's,
     scrolling inside itself while the page stays put. It used to hang absolute
     under the static header: scrolling carried it, and its close button, off
     screen while it stayed open, and the page showed below its last row.
     index.js measures the header into --drawer-top and sets body.menu-open
     while the drawer is open. */
  body.menu-open {
    overflow: hidden;
  }

  .menu__burger {
    position: fixed;
    top: var(--drawer-top);
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 4;
    flex-direction: column;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--space-2) var(--gutter) var(--space-8);
    background-color: var(--color-ink);
    border-top: var(--border-1) var(--border-style) var(--color-cyan);
    visibility: hidden;
  }

  .menu.menu__burger.active {
    visibility: visible;
  }

  /* Rows share the logo's left edge: the column is the container's. They
     were indented 62px at every width, in line with nothing. */
  .menu__item {
    display: block;
    flex: none;
    width: 100%;
    max-width: calc(var(--container-tablet) - 2 * var(--gutter));
    margin-inline: auto;
  }

  .menu__item + .menu__item {
    margin-left: auto;
    border-top: var(--border-1) var(--border-style) var(--color-cyan-50);
  }

  /* The whole row is the target, not only the word: the links were inline,
     so "About" answered a tap on 57x24px of a 60px row. */
  .menu__link {
    display: flex;
    justify-content: space-between;
    min-height: var(--touch-target);
    padding: var(--space-2) 0;
    font-size: var(--text-lg);
    font-weight: var(--weight-medium);
    line-height: var(--leading-snug);
  }

  .menu__link--about::after,
  .menu__link--services::after {
    transform: none;
  }

  .menu__link--about[aria-expanded="true"]::after,
  .menu__link--services[aria-expanded="true"]::after {
    transform: rotate(90deg);
  }

  /* The submenu toggle is handled in index.js with preventDefault(), not by
     disabling pointer events — that suppressed the click for a mouse while
     leaving Enter working for a keyboard, so the two input modes got opposite
     behaviour. */
  .sub-menu__list.open {
    display: block;
    padding: 0 0 var(--space-2) var(--space-4);
  }

  .sub-menu__group {
    padding: var(--space-2) 0 0;
  }

  /* Children at the touch-target pitch, closer than their parents: they were
     80px apart against the parents' 60px, so they read as scattered rather
     than grouped. */
  .sub-menu__link {
    display: flex;
    align-items: center;
    min-height: var(--touch-target);
    padding: 0;
    white-space: normal;
  }

  .menu__item--cta {
    padding-top: var(--space-6);
  }

  .menu__item + .menu__item--cta {
    border-top: 0;
  }

  /* Two classes: .headline__button sets width: fit-content later in the file. */
  .menu .menu__cta {
    width: auto;
    text-align: center;
  }
}

@media (max-width: 920px) {
  .container {
    max-width: none;
  }
}

/* The phone header is 64px: the burger's 44px target with the header's 10px
   above and below it. It was 93px around the 172px logo, 11% of an 844px
   screen before a word of the page (docs/visual-assessment/2026-10-20-mobile.md,
   S8). The logo keeps its proportions at 128px wide; the header stays static,
   so nothing it covers can hide a focused control. */
@media (max-width: 425px) {
  .header {
    padding: 0;
  }

  .nav {
    min-height: calc(var(--touch-target) + 2 * var(--space-2));
  }

  .header .logo__img {
    display: block;
    width: 128px;
    height: auto;
    aspect-ratio: 172 / 66;
  }
}

/* HEADLINE */

.picture--1 {
  background-image: var(--gradient-hero), url("./images/headline-bg.jpg");
}

.headline {
  padding: 120px 0;
  background-size: cover;
  background-position: bottom center;
  background-repeat: no-repeat;
  min-height: 400px;
}

.headline__title {
  font-family: var(--font-display);
  /* Was a fixed 60px, which the 1024 and 425 breakpoints left in place while
     dropping the size to 40px — a 1.5 ratio on a display face, so a two-line
     service title read as two separate headings. The ratio holds now. The
     size is the --type-title role; three breakpoint rules here used to
     set 40px and then 30px, which put the h1 under the section headings. */
  font-size: var(--type-title);
  line-height: var(--leading-tight);
  margin-bottom: 45px;
  max-width: 1034px;
}

.headline__description {
  font-size: 20px;
  line-height: 26px;
  margin-bottom: 90px;
  max-width: 600px;
  color: var(--color-white);
}

/* The masthead. Two forms, one rule: the photograph (.picture--1) is the home
   page's alone; every interior page uses the band — About, Blog, the articles,
   Privacy, Services, Careers, 404 and the ten service details. Services,
   Careers and 404 kept the photograph by omission until 2026-09-25, which put
   two design generations a click apart (docs/site-visual-audit.md). */
.headline--compact {
  background-color: var(--color-ink);
  min-height: 0;
  padding: var(--space-12) 0 var(--space-10);
}

.headline--compact .headline__title {
  margin-bottom: var(--space-3);
}

.headline--compact .headline__description {
  margin-bottom: 0;
}

/* The band: the services gradient (the site's one gradient, so the band
   belongs to the same family as the home page), a kicker in cyan, and a column
   of facts a reader wants before scrolling. Was --blog, after the first page
   that used it. */
.headline--band {
  background: var(--gradient-band);
  /* At a phone's 2.625 device pixels to the CSS pixel the band's foot falls
     between two device pixels (726px on Services is 1905.75), and the white
     canvas showed through as a light hairline, #2f2f31 between two blacks on
     a Pixel 10 emulation. The next section overlaps the band by one CSS
     pixel, so there is no gap to show through. */
  margin-bottom: -1px;
}

.headline__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-8) var(--space-16);
  align-items: end;
}

.headline__kicker {
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-cyan);
  margin: 0 0 var(--space-3);
}

.headline__facts {
  display: grid;
  gap: var(--space-4);
  margin: 0;
  padding-left: var(--space-6);
  border-left: var(--border-1) var(--border-style) var(--color-cyan-50);
  font-family: var(--font-sans);
  color: var(--color-white);
}

.headline__fact dt {
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-cyan);
  margin-bottom: 2px;
}

.headline__fact dd {
  margin: 0;
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  max-width: 22ch;
}

.headline__fact-line {
  display: block;
}

/* A name in a run of names breaks at the dot between two, never inside one:
   the joined string put "Game" and "development" on two lines at 390px. */
.headline__fact-item {
  white-space: nowrap;
}

.headline__feed {
  color: var(--color-white);
  text-decoration: underline;
  text-decoration-color: var(--color-cyan);
  text-underline-offset: 0.2em;
}

.headline__feed:hover {
  color: var(--color-cyan);
}

.headline__button {
  padding: 10px 20px;
  display: block;
  /* Was max-width: 140px, which is narrower than "Back to home" at this size,
     so the 404's button wrapped onto two lines. */
  width: fit-content;
  font-size: 18px;
  line-height: 27px;
  color: var(--color-ink);
  background-color: var(--color-cyan);
  transition: all var(--duration-base) var(--ease-base);
}

.headline__button:hover {
  background-color: var(--color-cyan-dark);
}

/* Home keeps its photograph, while the message and both routes occupy the
   opening screen. The photograph remains decorative rather than a spacer. */
.picture--1 .headline__title {
  margin-bottom: var(--space-4);
}

.picture--1 .headline__description {
  margin-bottom: var(--space-6);
}

.home-hero__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
}

.home-hero__secondary {
  color: var(--color-white);
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  text-decoration: underline;
  text-underline-offset: var(--space-1);
}

.home-hero__secondary:hover {
  color: var(--color-mint);
}

/* The hero runs most of a screen and pins the founder's record to its lower
   edge, so the proposition and the proof share the opening. The cap keeps a
   1440px photograph from stretching on a tall wide screen. The 425px block
   below still sizes the phone hero by its content. */
.headline.home-hero {
  display: flex;
  flex-direction: column;
  min-height: min(78svh, 760px);
  padding: var(--space-16) 0 var(--space-8);
}

.home-hero__inner {
  display: flex;
  flex: 1;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-12);
  width: 100%;
}

/* A practice's name never breaks inside itself; the line breaks at the dot. */
.home-hero__practice {
  white-space: nowrap;
}

.home-hero__record {
  padding-top: var(--space-5);
  border-top: var(--border-1) var(--border-style) var(--color-cyan-50);
}

.home-hero__record-label {
  margin-bottom: var(--space-4);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-blush);
}

/* The facts as a row of figures: the value set large above its label. The
   markup keeps dt before dd, so a screen reader hears the label first. */
.headline__facts--row {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-8);
  max-width: var(--container-list);
  padding-left: 0;
  border-left: 0;
}

.headline__facts--row .headline__fact {
  display: flex;
  flex-direction: column-reverse;
  justify-content: flex-end;
  gap: var(--space-1);
}

.headline__facts--row dd {
  max-width: none;
  font-family: var(--font-display);
  font-size: var(--text-4xl);
  line-height: var(--leading-tight);
  font-weight: var(--weight-semibold);
}

.headline__facts--row dt {
  max-width: 26ch;
  margin-bottom: 0;
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  letter-spacing: normal;
  text-transform: none;
  color: var(--color-blush);
}

/* Nothing else spaces the band's button from the description: --compact zeroes
   the description's margin, so the ten service mastheads shipped it flush. */
.headline--compact .headline__button {
  margin-top: var(--space-6);
}

@media (max-width: 425px) {
  .headline {
    padding: 50px 0 120px;
    height: 70vh;
    background-position: bottom right;
  }

  .headline__description {
    display: none;
  }

  .headline.picture--1 {
    height: auto;
    min-height: 0;
    padding: var(--space-10) 0 var(--space-12);
  }

  .picture--1 .headline__description {
    display: block;
    font-size: var(--text-base);
    line-height: var(--leading-normal);
  }

  .headline--compact {
    height: auto;
  }

  .headline--compact .headline__description {
    display: block;
  }

  .headline__grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .headline__facts {
    grid-template-columns: repeat(3, auto);
    justify-content: start;
    gap: var(--space-6);
    padding-left: 0;
    border-left: 0;
    padding-top: var(--space-4);
    border-top: var(--border-1) var(--border-style) var(--color-cyan-50);
  }

  /* Facts that are names, not numbers, want a line each on a phone. */
  .headline__facts--names {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-3);
  }

  .headline__facts--names dd {
    max-width: none;
  }

  /* Three 30px figures do not fit across a 320px phone ("110M+" alone is
     about 95px), so the record becomes a ledger: figure, then label, a line
     each. The wrappers step aside (display: contents, on a div, which carries
     no role to lose) so every figure shares one column; dense packing puts
     each dd back on its own dt's row. */
  .headline__facts--row {
    grid-template-columns: max-content minmax(0, 1fr);
    grid-auto-flow: dense;
    align-items: baseline;
    gap: var(--space-3) var(--space-4);
    padding-top: 0;
    border-top: 0;
  }

  .headline__facts--row .headline__fact {
    display: contents;
  }

  .headline__facts--row dt {
    grid-column: 2;
  }

  .headline__facts--row dd {
    grid-column: 1;
  }
}

/* SERVICES */

.services {
  background: var(--gradient-services);
  padding: 100px 0;
  color: var(--color-blush);
}

/* Three tracks and a token gap. Was flex space-between over 380px cards,
   which left the fourth Cloud-Native card alone and flush-left with a gap that
   matched nothing. Below 1024 the flex rules further down take over. */
.services-items {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-8);
}

.services-item {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  background-color: var(--color-navy-soft);
  padding: 20px 24px;
  color: var(--color-blush);
}

.services-item:hover {
  cursor: pointer;
  background-color: var(--color-navy);
}

.services-item__pretitle {
  font-weight: 400;
  font-size: 12px;
  line-height: 18px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  padding-bottom: 5px;
  /* The eyebrow names the practice, not "Services" (which every card on the
     Services page said). Shrink-wrapped so the rule under it hugs the words;
     the old 64px cap would have wrapped "AI and Agent Engineering" three deep. */
  align-self: flex-start;
  border-bottom: 1px solid var(--color-cyan-50);
}

.services-item__title {
  font-family: var(--font-sans);
  font-weight: var(--weight-medium);
  font-size: var(--text-2xl);
  line-height: var(--leading-28);
}

.services-item__description {
  font-size: 13px;
  line-height: 20px;
}

/* Geometry in the markup, paint here — the same contract as .diagram-box and
   .diagram-line. Ten services used to share three icon files, each with
   #7BFFD1 baked in, at three different sizes: 60x60, 43x52 and 39x47, so the
   card's last row shifted by up to 13px depending on which service it was.
   An inline <svg> with a viewBox and no width/height has no intrinsic size,
   so .services-inner__icon is load-bearing rather than cosmetic.

   The weight is in CSS pixels, not drawing units. At 1.5 units the line was
   1.5px on a 48px card and 1.125px in a 36px phone row, where the small parts
   ran together; non-scaling-stroke holds 1.25px at every size the mark is
   drawn. Round ends and corners are the logo's terminals, and the one white
   star per mark is its white point. The hover rules below recolour the line
   only, so the star stays the constant. */
.service-icon {
  display: block;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.25px;
  stroke-linecap: round;
  stroke-linejoin: round;
  color: var(--color-mint);
}

.service-icon > * {
  vector-effect: non-scaling-stroke;
}

.service-icon__star {
  fill: var(--color-white);
  stroke: none;
}

.services-inner__icon {
  width: 48px;
  height: 48px;
}

.services-item:hover .services-inner__icon {
  color: var(--color-cyan);
}

.services-inner {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 10px;
}

.services-inner__more {
  color: var(--color-white);
  display: flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
}

.services-inner__more svg {
  fill: currentColor;
}

/* The whole card is the link, so :hover on this span barely fires — and the
   old rules targeted `svg path`, which lit the chevron while leaving the words
   unchanged. Hover the card, colour both. */
.services-item:hover .services-inner__more {
  color: var(--color-mint);
}

.services-item:active .services-inner__more {
  color: var(--color-green);
}

/* PRACTICE PANELS — the home page's services block.

   Two panels rather than ten cards. The cards remain on services.html, which
   is the page for a catalogue; on the home page they were four rows deep and
   pushed the articles past most sessions' scroll depth. */

.services.services--home {
  padding: var(--space-24) 0;
}

/* A home section's head: its heading on the left and the way to the whole
   list on the right, on one baseline, so the section's one link sits where
   the eye finishes the heading rather than below the last item. */
.home-section__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2) var(--space-8);
  margin-bottom: var(--space-12);
}

.home-section__head .services-title--home,
.home-section__head .information__lead,
.home-section__head .information__all {
  margin: 0;
}

/* Each practice is a ruled row: its number, name and lead on the left, its
   services as a ruled list on the right. */
.practice {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--space-6) var(--space-16);
  padding-top: var(--space-8);
  border-top: var(--border-1) var(--border-style) var(--color-cyan-50);
}

.practice + .practice {
  margin-top: var(--space-16);
}

.practice__index {
  margin-bottom: var(--space-3);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-label);
  color: var(--color-mint);
}

.practice__name {
  font-family: var(--font-sans);
  font-size: var(--text-3xl);
  line-height: var(--leading-snug);
  font-weight: var(--weight-semibold);
  color: var(--color-white);
}

.practice__lead {
  margin-top: var(--space-2);
  max-width: var(--measure-wide);
  font-size: var(--text-lg);
  line-height: var(--leading-normal);
  color: var(--color-mint);
}

.practice__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.practice__item + .practice__item {
  border-top: var(--border-1) var(--border-style) var(--color-cyan-50);
}

/* The whole row is the link and the touch target. */
.practice__link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  min-height: var(--touch-target);
  padding: var(--space-3) 0;
  color: var(--color-white);
  font-size: var(--text-lg);
  line-height: var(--leading-snug);
}

.practice__item:first-child .practice__link {
  padding-top: 0;
}

.practice__link svg {
  flex: none;
  color: var(--color-mint);
}

.practice__link:hover {
  color: var(--color-cyan);
}

.practice__link:hover svg {
  color: var(--color-cyan);
}

/* A bare text link: the row is the touch target. */
.practice__all {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-target);
  color: var(--color-mint);
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
}

.practice__all:hover {
  text-decoration: underline;
}

/* The founder, right under the hero: a pictured, named person beside the
   roles and the recognition. The portrait's file is 297x283, so it is shown
   at 120px, sharp at 2x. */
.home-founder {
  padding: var(--space-20) 0;
  background-color: var(--color-surface);
  color: var(--color-ink);
}

/* The heading comes first in the markup, so the section is named before the
   person in it; the grid moves the person into the left column. */
.home-founder__inner {
  display: grid;
  grid-template-columns: minmax(0, 4fr) minmax(0, 8fr);
  grid-template-rows: auto 1fr;
  grid-template-areas:
    "who title"
    "who body";
  column-gap: var(--space-16);
  align-items: start;
}

.home-founder__title {
  grid-area: title;
}

.home-founder__who {
  grid-area: who;
}

.home-founder__body {
  grid-area: body;
}

/* Beats .person__portrait's 144px, which comes later in the file. */
.person__portrait.home-founder__portrait {
  width: 120px;
  height: 120px;
  margin-bottom: var(--space-4);
}

.home-founder__name {
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
}

.home-founder__role {
  max-width: 28ch;
  margin-top: var(--space-1);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  color: var(--color-muted);
}

.home-founder__title {
  font-family: var(--font-display);
  font-size: var(--type-section);
  line-height: var(--leading-snug);
  font-weight: var(--weight-semibold);
  margin-bottom: var(--space-4);
}

.home-founder__text {
  max-width: var(--measure-wide);
  font-size: var(--text-xl);
  line-height: var(--leading-normal);
}

.home-founder__awards {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-8);
  margin: var(--space-6) 0;
  padding: var(--space-4) 0 0;
  border-top: var(--border-1) var(--border-style) var(--color-hairline);
  list-style: none;
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
}

.home-founder__link {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-target);
  color: var(--color-indigo);
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  text-decoration: underline;
  text-underline-offset: var(--space-1);
}

@media (max-width: 768px) {
  .services.services--home {
    padding: var(--space-16) 0;
  }

  .practice {
    grid-template-columns: minmax(0, 1fr);
  }

  .practice + .practice {
    margin-top: var(--space-12);
  }

  .home-founder {
    padding: var(--space-12) 0;
  }

  .home-founder__inner {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: none;
    grid-template-areas:
      "title"
      "who"
      "body";
    row-gap: var(--space-6);
  }

  .home-founder__title {
    margin-bottom: 0;
  }

  .person__portrait.home-founder__portrait {
    width: 96px;
    height: 96px;
  }

  .home-section__head {
    margin-bottom: var(--space-8);
  }
}

/* SERVICES DEVOPS */

.services {
  position: relative;
}

/* The detail pages are long-form documents — 900 to 1,200 words — and had no
   layout structure at all: every block carried its own max-width, so the right
   edge walked down the page 580 -> 1240 -> 513 -> 1240 -> 580 while the left
   stayed put. That alternation is what read as unfinished.

   This is the article's grid (style.css, `.article`), ported. Its comment
   records the same defect being fixed there and never brought across: "the
   measure was a max-width on .article itself, which left the text flush
   against the left edge ... which is what made the column look stranded."

   Two tracks and two edges. Everything defaults to the text track; the blocks
   that genuinely want the width opt into `full` by name.

   font-size here is load-bearing: em resolves against this element, so
   without it --measure-technical would compute against the inherited 16px
   and the track would come out 320px narrower than the prose it holds.

   services.html shares the .services class and must not get this, hence the
   --detail modifier rather than a bare .services .container. */
/* The masthead has to share the body's width or the h1 starts 130px left of
   the first paragraph under it, which is the kind of misalignment that reads
   as unfinished even when nobody can say why. */
.headline--service .container {
  max-width: 1000px;
}

/* The deck was 20px — the same size as body copy — under a display h1. A deck
   that matches the paragraphs it introduces is not a deck. */
.headline--service .headline__description {
  font-size: var(--text-2xl);
  line-height: var(--leading-snug);
  max-width: 34ch;
}

.breadcrumb {
  margin-bottom: var(--space-4);
}

/* Centred, because below 768 the link is a 44px touch box and the current
   page is a line of text: aligned to the top, "Services" sat 13px below its
   own separator and the practice name on the same row. */
.breadcrumb__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  list-style: none;
  margin: 0;
  padding: 0;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-cyan);
}

/* The separator is drawn, not typed: a literal "/" in the markup would be
   read out by a screen reader between every crumb. */
.breadcrumb__item + .breadcrumb__item::before {
  content: "/";
  margin-right: var(--space-2);
  color: var(--color-cyan-50);
}

.breadcrumb__link {
  color: var(--color-cyan);
  text-decoration: underline;
  text-underline-offset: 0.25em;
}

.breadcrumb__link:hover {
  color: var(--color-white);
}

/* A service detail's mark, set beside its breadcrumb as a lockup. The ten
   bands differed only in their words, and the mark that tells them apart came
   first in the related cards at the foot of the page
   (docs/visual-assessment/2026-10-08-design-critique.md, item 4). Beside the
   breadcrumb rather than above it: on a phone the breadcrumb is already two
   lines, 44px, so a 36px mark beside it adds nothing to a band that was
   trimmed to let the first paragraph into the opening screen; stacked, it
   would have added 46px. The mark is aria-hidden, so the breadcrumb's name is
   unchanged. */
.headline__signature {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.headline__signature .breadcrumb {
  min-width: 0;
  margin-bottom: 0;
}

.headline__mark {
  width: 48px;
  height: 48px;
}

.services--detail .container {
  /* 1000 - 2x10 padding = 980px of content, which is exactly the article's
     body column (1240 container - 220 rail - 40 gap). One document width for
     the whole site, rather than marketing pages running 260px wider than the
     writing.

     This is the change that actually moves pixels. At 1240 the stages grid
     gave each cell 600px, and 600px at 18px is ~79 characters — past the
     45-75 band tokens.css sets out. At 980 the cells are 470px, ~62
     characters. The panels drop from a cramped 3-up to a 2-up at 480px. */
  max-width: 1000px;

  /* em resolves against this element, so the track has to be declared at the
     size the prose actually sets at. Without it --measure computes against
     the inherited 16px and the track comes out 116px narrower than the text
     it holds. */
  font-size: var(--text-lg);
  display: grid;
  grid-template-columns:
    [full-start text-start] min(100%, var(--measure)) [text-end]
    minmax(0, 1fr) [full-end];
}

.services--detail .container > * {
  grid-column: text;
}

.services--detail .services-lede,
.services--detail .services-stages,
.services--detail .services-list--panels,
.services--detail .services-list--ruled,
.services--detail .services-cta {
  grid-column: full;
}

.services-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--type-section);
  line-height: var(--leading-snug);
  margin-bottom: 90px;
}

/* .services-title is the page title at 50px. A section heading inside the
   body needs the same family and weight at a readable step down — this is
   also what retires .services-subtitle, which servicesProduct.html emitted
   with no rule anywhere, so its three subheads rendered as browser default. */
/* .services already pads the section; the first heading inside it must not
   add its own top margin on top of that, or the band and the first words are
   separated by ~150px of nothing. */
.services .container > .services-title--section:first-child {
  margin-top: 0;
}

/* Five sections ran together as one continuous dark field across ~5,000px,
   separated by whitespace alone. The rule gives the page chapters: it spans
   the full track, so it also re-states the document's width every time the
   reader meets one. :first-child is exempt — the masthead is already the
   break above it. */
.services--detail .container > .services-title--section {
  grid-column: full;
  padding-top: var(--space-10);
  border-top: var(--border-1) var(--border-style) var(--color-cyan-50);
}

.services--detail .container > .services-title--section:first-child {
  padding-top: 0;
  border-top: 0;
}

.services-title--section {
  font-size: var(--type-section);
  line-height: var(--leading-snug);
  margin: var(--space-12) 0 var(--space-5);
}

/* ---- Service detail: engagement stages, evidence, figure, CTA ---------- */

/* An engagement is bought on what happens and in what order, so the stages
   are an ordered list and every one of them names what it produces. */
.services-stages {
  /* No marker: the eyebrow inside each stage already says "Stage 1", and the
     decimal marker beside it printed the number twice. A rule above each
     stage, not a panel, so a sequence reads differently from the grid of
     controls above it. 340px is the narrowest cell that holds an 18px stage
     body at a readable measure; auto-fill gives 3/2/2/1 columns at the four
     breakpoints without a media query, and four stages fill 2×2 from 768 up. */
  list-style: none;
  margin: var(--space-6) 0 var(--space-8);
  display: grid;
  /* Two fixed columns, not auto-fill: four stages on the desktop's three
     auto-filled columns left the fourth alone on a row of its own. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-6) var(--space-8);
}

.services-stage {
  padding-top: var(--space-4);
  border-top: var(--border-1) var(--border-style) var(--color-cyan-50);
  /* Stage bodies inherited the 16px root size beside 20px prose. One step
     down from the prose, the same as the panels, so the page has two text
     sizes rather than three. */
  font-size: var(--text-md);
  line-height: var(--leading-normal);
}

.services-stage__span {
  display: block;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-cyan);
}

.services-stage__name {
  display: block;
  font-family: var(--font-sans);
  font-size: var(--text-xl);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
}

.services-stage__output {
  display: block;
  margin-top: var(--space-2);
  color: var(--color-mint);
}

/* The light chapter. A detail page was one dark field from the band to the
   call to action: 5,909-6,985px at 390 and 3,651-4,566px at 1440, broken by
   hairlines alone (docs/design-improvement-spec.md, P2). "How the engagement
   runs" is the chapter every page shares, so it becomes its own section on
   the light panel ground, and the dark run splits in two. Its accents move to
   the light-ground set: cyan, mint and green fail on light (tokens.css), so
   the eyebrow, the rule and the links are indigo, and the "You get" line,
   mint on dark, is ink at a heavier weight. Every pair is in check-contrast.

   Where two detail sections meet, each gives up most of its 100px padding:
   the band edge is now the chapter break that the heading's rule and margin
   used to be, and the two together cost about what they did. */
.services--light {
  background: var(--color-surface);
  color: var(--color-black);
}

.services--detail:has(+ .services--detail) {
  padding-bottom: var(--space-12);
}

.services--detail + .services--detail {
  padding-top: var(--space-12);
}

.services--light .services-title {
  color: var(--color-ink);
}

.services--light .services-stage {
  border-top-color: var(--color-indigo);
}

.services--light .services-stage__span {
  color: var(--color-indigo);
}

.services--light .services-stage__name {
  color: var(--color-ink);
}

.services--light .services-stage__output {
  color: var(--color-ink);
  font-weight: var(--weight-medium);
}

.services--light .services-evidence__link {
  color: var(--color-indigo);
  text-decoration-color: var(--color-indigo);
}

.services--light .services-evidence__link:hover {
  color: var(--color-ink);
}

/* The proof block. There are no clients to name and no testimonials, so the
   evidence a service page can offer is the article that argues its method. */
.services-evidence {
  margin: var(--space-8) 0;
  padding-left: var(--space-6);
  border-left: var(--border-1) var(--border-style) var(--color-cyan-50);
  max-width: var(--measure-wide);
}

.services-evidence__link {
  color: var(--color-white);
  text-decoration: underline;
  text-decoration-color: var(--color-cyan);
  text-underline-offset: 0.2em;
}

.services-evidence__link:hover {
  color: var(--color-cyan);
}

/* The ask is a band, not a footnote: the page's one panel that runs the full
   container, with the sentence and the button on one line. */
.services-cta {
  margin-top: var(--space-12);
  padding: var(--space-6);
  background-color: var(--color-navy-soft);
  border: var(--border-1) var(--border-style) var(--color-cyan-50);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-8);
}

.services-cta .services__text {
  margin-bottom: 0;
}

.services-cta .headline__button {
  flex: none;
}

/* The opening argument: prose at its measure on the left, the diagram beside
   it on the right, so the first screen below the band carries both. The
   figure grows to 560px here — at its old 520px cap it was a third of the
   container on its own line. */
/* Explicit track widths, not an em measure: the container declares
   font-size: 20px so that its own --measure track resolves correctly, and an
   em here would inherit that and resolve 34em to 680px, leaving the figure
   220px. Px tracks make the two independent.
   The figure's track is 460px because the drawing is 440 units wide and its
   smallest label is 13 units: inside the frame's 20px padding a 460px track
   draws it 420px wide, so that label is 12.4px and the 15-unit ones 14.3px.
   At 380px they were 10.0px and 11.6px on every desktop width, under the
   12px floor the phone drawings are held to. The prose keeps 470px, about 47
   characters at 20px, inside the 45-75 band. */
.services-lede {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 460px);
  column-gap: var(--space-10);
  align-items: start;
}

.services-lede .services-figure {
  margin: 0;
  max-width: none;
  width: min(100%, 560px);
  justify-self: end;
}

/* .article-figure cannot be reused here: it carries grid-column: full, which
   is inert outside the article grid and would let the diagram fall back to
   the full 1240px container. */
.services-figure {
  margin: var(--space-8) 0;
  max-width: 520px;
}

/* .article-diagram's own ground is --color-ink, and --gradient-services runs
   through --color-ink at 65.67% — the panel would have no edge across most
   of the section. navy-soft is the site's panel-on-dark, and white, cyan and
   mint are all already registered against it in check-contrast's PAIRS. */
.article-diagram--panel {
  background-color: var(--color-navy-soft);
}

/* .article-caption is --color-muted, which is 4.09:1 on this ground — below
   the 4.5:1 floor. Blush is 18.72:1. */
.services-caption {
  font-size: var(--text-2xs);
  line-height: var(--leading-loose);
  color: var(--color-blush);
  margin-top: var(--space-3);
  text-wrap: pretty;
}

.services__text {
  font-size: var(--text-lg);
  /* Was a flat 43px from the legacy leading ladder — a ratio of 2.15, nearly
     double-spaced. At the length these pages are growing to that set ~3,400px
     of wall; --leading-normal is what the rest of the site reads at. */
  line-height: var(--leading-normal);
  margin-bottom: var(--space-4);
}

.services-list {
  margin-top: 25px;
  list-style-type: disc;
  margin-left: 40px;
  margin-bottom: 40px;
  max-width: 1034px;
}

.services-list__item {
  font-size: var(--text-lg);
  line-height: var(--leading-normal);
  margin-bottom: var(--space-3);
}

/* Two shapes for the two lists a service page carries, so the controls and
   the deliverables stop looking like the same list twice. Both grids share
   the stages' 340px cell (3/2/2/1 columns) and the whole container width,
   which is what the single 580px column left empty on desktop. */

/* The controls: panels on the site's panel-on-dark, the ground .services-item
   already uses. The bold lead becomes the panel's title — the markup drops
   its trailing colon for that reason. Blush on navy-soft is registered in
   check-contrast's PAIRS. */
.services-list--panels {
  list-style: none;
  margin: var(--space-6) 0 var(--space-8);
  max-width: none;
  display: grid;
  /* min(): a 340px track floor in a 290px column (a 320px phone) pushed every
     service page to 355px wide, a sideways scroll that fails WCAG 1.4.10 at
     its own reference width. The floor now yields to the column. */
  grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr));
  gap: var(--space-4);
}

/* The fill alone is 1.22:1 against --color-ink and 1.08:1 over the violet end
   of the gradient, so eight panels read as one dark field. The hairline is
   what makes them surfaces; it composites to ~3.4:1, above the 3:1 floor for
   a UI boundary. */
.services-list--panels .services-list__item {
  margin: 0;
  max-width: none;
  padding: var(--space-5);
  background-color: var(--color-navy-soft);
  border: var(--border-1) var(--border-style) var(--color-cyan-50);
  font-size: var(--text-md);
}

.services-list--panels .services-list__item > strong {
  display: block;
  margin-bottom: var(--space-2);
  font-family: var(--font-sans);
  font-size: var(--text-xl);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
  color: var(--color-white);
}

/* The deliverables: the same grid, ruled rather than boxed, so an outcome
   reads as a line item and not as another control. */
.services-list--ruled {
  list-style: none;
  margin: var(--space-6) 0 var(--space-8);
  max-width: none;
  display: grid;
  /* Every page lists four deliverables, so two columns make a 2×2 and
     three would strand the last one. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4) var(--space-8);
}

.services-list--ruled .services-list__item {
  margin: 0;
  max-width: none;
  padding-top: var(--space-4);
  border-top: var(--border-1) var(--border-style) var(--color-cyan-50);
  font-size: var(--text-md);
}

.services-information {
  /* The bottom used to come from the cards' own 40px margin. */
  padding: 60px 0 var(--space-8);
}

/* .services-practice is also the grouped-catalogue heading on the dark
   .services band (services.html), where white is correct. Here the section
   carries no background, so the heading sits on the page ground instead. */
.services-information .services-practice {
  color: var(--color-ink);
}

/* SERVICE ROWS — the catalogue on a phone.
   A link per service, the form the home page's practice list and the menu
   drawer already use: the mark, the name, a chevron that means "go to the
   page" because the row is one. It was an accordion of 150px rows with a 40px
   gap and a doubled rule, ten services to ~1,900px at 390, behind a chevron
   that pointed right and opened in place
   (docs/visual-assessment/2026-10-20-mobile.md, finding 6). The summary the
   accordion revealed opens the service page, which says it at length. */
.services-row {
  display: none;
}

@media (max-width: 768px) {
  .services-row {
    display: flex;
    flex: 0 0 100%;
    align-items: center;
    gap: var(--space-3);
    min-height: var(--space-12);
    padding: var(--space-2) 0;
    border-top: var(--border-1) var(--border-style) var(--color-cyan-50);
    color: var(--color-white);
    font-size: var(--text-lg);
    font-weight: var(--weight-semibold);
    line-height: var(--leading-snug);
  }

  .services-row:last-child {
    border-bottom: var(--border-1) var(--border-style) var(--color-cyan-50);
  }

  /* One list: the catalogue grid's 40px gap would part the rows. The related
     cards on a service page keep theirs. */
  .services-items:not(.services-items--cards) {
    gap: 0;
  }

  /* 36 rather than 35: at a 48-unit viewBox that is exactly 0.75, so every
     4-unit step in the drawing lands on a whole 3px boundary. */
  .services-row__icon {
    width: 36px;
    height: 36px;
  }

  .services-row__name {
    flex: 1;
    min-width: 0;
  }

  .services-row::after {
    content: "";
    flex: none;
    width: 0.375em;
    height: 0.625em;
    background-color: var(--color-cyan);
    -webkit-mask: url("images/arrow-right.svg") no-repeat center / contain;
    mask: url("images/arrow-right.svg") no-repeat center / contain;
  }

  .services-row:hover {
    color: var(--color-cyan);
  }

  .services-row:hover .services-row__icon {
    color: var(--color-cyan);
  }

  .services-row:active {
    color: var(--color-green);
  }
}

@media (max-width: 1024px) {
  /* Back to flex below the grid's width. The wrap is load-bearing: without
     it the ten phone rows (accordions then) ran in one row and the page was
     1553px wide on a phone (caught by nix run .#shots -- compare,
     2026-09-25). */
  .services-items {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-evenly;
  }

  .services-item {
    max-width: 260px;
    margin: 5px;
    display: flex;
  }

  /* 1004px minus the 680px prose column and the 80px gap leaves 244px, too
     narrow for the diagram's 13px labels, so the figure drops below. */
  .services-lede {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--space-6);
  }

  .services-lede .services-figure {
    justify-self: start;
    width: min(100%, 520px);
  }
}

@media (min-width: 769px) and (max-width: 1024px) {
  /* The catalogue cards fill the section gutter at this width. Related cards
     keep their own layout. */
  .services-items:not(.services-items--cards) {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-8);
  }

  .services-items:not(.services-items--cards) .services-item {
    max-width: none;
    margin: 0;
  }
}

@media (max-width: 768px) {
  .services {
    padding: 50px 0;
  }

  .services-cta {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-4);
  }

  .services-title {
    margin-bottom: 40px;
  }

  .services-item__description {
    padding: 49px 1px 42px 44px;
  }

  .services-inner__more svg {
    margin-left: auto;
  }

  .services-item {
    display: none;
  }

  /* services.html swaps its cards for link rows here, so the cards are
     hidden wholesale. related() has no accordion form and emits cards only,
     which left the foot of every service detail page empty on a phone while
     .services-information's padding still reserved the room. Scope the
     exception to the related container rather than weakening the hide. */
  .services-items--cards .services-item {
    display: flex;
    width: 100%;
    max-width: none;
  }
}

@media (max-width: 425px) {
  .services-items {
    display: flex;
  }

  /* Two defects lived here. The list selector was (0,1,0) while
     .services-list--panels .services-list__item is (0,2,0), so on a phone
     the bullet lists rendered 18px while the prose beside them rendered
     14px — the lists were larger than the body text. And the leading went
     1.55 -> 1.43 as the type got smaller, which is backwards: 14px wants
     more leading than 20px, not less.

     Both fixed by naming the modifiers and settling on one mobile size with
     the inherited ratio. */
  .services__text,
  .services-list--panels .services-list__item,
  .services-list--ruled .services-list__item,
  .services-stage {
    font-size: var(--text-base);
    line-height: var(--leading-normal);
  }

  .services-stages,
  .services-list--ruled {
    grid-template-columns: minmax(0, 1fr);
  }

  .services-item {
    margin: 5px;
  }
}

/* INFORMATION: the article list on the blog index and the home page.
   One list, one entry style; _partials/article-card.html is the only markup.
   A row per article: the picture at a fixed shape on the left, the words on
   the right, a hairline between rows, so four entries read as one list rather
   than four islands. */

.information {
  color: var(--color-black);
  padding: var(--space-10) 0 var(--space-20);
}

/* The section label above the list on the home page. */
.information__title {
  font-family: var(--font-display);
  font-size: var(--type-section);
  line-height: var(--leading-40);
  font-weight: var(--weight-semibold);
  color: var(--color-black);
  margin-bottom: var(--space-2);
}

.information__lead {
  max-width: var(--measure-wide);
  margin-bottom: var(--space-8);
  font-size: var(--text-lg);
  line-height: var(--leading-normal);
  color: var(--color-muted);
}

/* Home's article section: the same generous run as the practices above it,
   and no opening rule, since it follows a dark section. */
.information.information--home {
  padding: var(--space-24) 0;
}

/* How Home and Services end (_partials/closing.html): the heading at the
   title size, then the address and the number, set large. Light ground, so
   the step into the black footer is a clean one. */
.closing {
  padding: var(--space-20) 0;
  background-color: var(--color-surface);
  color: var(--color-ink);
}

.closing__inner {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--space-6) var(--space-16);
  align-items: baseline;
}

.closing__title {
  font-family: var(--font-display);
  font-size: var(--type-title);
  line-height: var(--leading-tight);
  font-weight: var(--weight-semibold);
}

.closing__body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
}

.closing__text {
  margin-bottom: var(--space-2);
  font-size: var(--text-lg);
  line-height: var(--leading-normal);
}

.closing__email {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-target);
  color: var(--color-indigo);
  font-size: var(--type-subsection);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
  text-decoration: underline;
  text-decoration-thickness: var(--border-1);
  text-underline-offset: var(--space-1);
  overflow-wrap: anywhere;
}

.closing__phone {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-target);
  color: var(--color-ink);
  font-size: var(--text-lg);
}

.closing__email:hover,
.closing__phone:hover {
  color: var(--color-navy);
}

@media (max-width: 768px) {
  .information.information--home {
    padding: var(--space-16) 0 var(--space-12);
  }

  .closing {
    padding: var(--space-16) 0;
  }

  .closing__inner {
    grid-template-columns: minmax(0, 1fr);
  }
}

.information__all {
  display: inline-block;
  margin-top: var(--space-8);
  color: var(--color-indigo);
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
}

.information__all:hover {
  text-decoration: underline;
}

.information__contact {
  display: inline-block;
  margin-left: var(--space-8);
  color: var(--color-indigo);
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  text-decoration: underline;
  text-underline-offset: var(--space-1);
}

@media (max-width: 425px) {
  .information__contact {
    display: block;
    margin-left: 0;
    margin-top: var(--space-4);
  }
}

/* The list's way out, the same on Home and on the Blog index: the two links
   share a row with a real gap and wrap as a pair. The Blog index ran from its
   last card straight into the footer, the one page in the site with no
   closing of its own (docs/design-improvement-spec.md, P2); it adds a
   sentence and a rule, so the block reads as the end of the list rather than
   as another card. */
.information__next {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4) var(--space-8);
  max-width: var(--container-list);
  margin-top: var(--space-8);
}

.information__next .information__all,
.information__next .information__contact {
  margin: 0;
}

.information__next--end {
  padding-top: var(--space-8);
  border-top: var(--border-1) var(--border-style) var(--color-hairline);
}

.information__next-text {
  flex-basis: 100%;
  margin: 0;
  color: var(--color-ink);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
}

.information-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: var(--container-list);
}

.information-inner {
  display: grid;
  grid-template-columns: 280px minmax(0, 1fr);
  grid-template-rows: auto 1fr;
  gap: 0 var(--space-8);
  align-items: start;
  padding: var(--space-8) 0;
  border-top: var(--border-1) var(--border-style) var(--color-hairline);
}

/* The home page's variant: the newest article leads at full width in the
   stacked form, and the next two sit beside it. blog.html keeps the plain
   list, where a single readable column is the right shape for scanning
   every article. */
.information-list--featured {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 var(--space-8);
  max-width: none;
}

.information-list--featured .information-inner:first-child {
  grid-column: 1 / -1;
  padding-top: 0;
  border-top: 0;
  /* The lead spans both columns, so it must not keep the 280px thumbnail the
     narrow list uses: that made the featured article carry the smallest
     picture on the page. It tracks the column width instead, and the clamp
     keeps it between the list's own thumbnail and half the lead's width. */
  grid-template-columns: clamp(280px, 34%, 460px) minmax(0, 1fr);
}

/* The two beneath stack image over text, which is the arrangement the card
   already uses below 768px. One column is not enough on its own: the base
   rule pins .information-inner__item to grid-column 2, which conjures an
   implicit second track and starves the image's minmax(0, 1fr) down to zero
   width. The placement has to be restated, exactly as the <=768 block does. */
.information-list--featured .information-inner:not(:first-child) {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: none;
}

.information-list--featured .information-inner:not(:first-child) img {
  grid-column: 1;
  grid-row: 1;
  width: 100%;
  height: auto;
  margin-bottom: var(--space-4);
}

.information-list--featured
  .information-inner:not(:first-child)
  .information-inner__head {
  grid-column: 1;
  grid-row: 2;
}

.information-list--featured
  .information-inner:not(:first-child)
  .information-inner__body {
  grid-column: 1;
  grid-row: 3;
}

.information-inner:first-child {
  padding-top: 0;
  border-top: 0;
}

/* Every picture the same shape, whatever the file's own proportions; the
   width and height in the markup still reserve the box before it loads. */
.information img {
  grid-column: 1;
  grid-row: 1 / span 2;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 10;
  object-fit: cover;
}

/* The card is three siblings in reading order — heading and meta, picture,
   then excerpt, topics and action — so the markup order is the visible order
   wherever the picture sits. Wide rows put the picture in the first column
   spanning both text blocks; the stacked forms reorder by row. */
.information-inner__head,
.information-inner__body {
  grid-column: 2;
  max-width: 60ch;
}

.information-inner__head {
  grid-row: 1;
}

.information-inner__body {
  grid-row: 2;
}

.information-inner__title {
  font-family: var(--font-sans);
  font-size: var(--text-2xl);
  line-height: var(--leading-snug);
  font-weight: var(--weight-semibold);
  margin: 0 0 var(--space-2);
}

.information-inner__title a {
  color: var(--color-black);
}

.information-inner__title a:hover {
  color: var(--color-indigo);
}

.information-inner__meta {
  font-size: var(--text-sm);
  color: var(--color-muted);
  margin: 0 0 var(--space-3);
}

.information-inner__text {
  margin: 0 0 var(--space-3);
}

.information-inner__read {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: 24px;
  color: var(--color-indigo);
  font-weight: 600;
}

.information-inner__read:hover {
  text-decoration: underline;
}

@media (max-width: 1024px) {
  .information-inner {
    grid-template-columns: 220px minmax(0, 1fr);
    column-gap: var(--space-6);
  }
}

@media (max-width: 768px) {
  .information {
    padding-bottom: var(--space-12);
  }

  /* Two columns of a 425px viewport is a 183px card. Below 768 the featured
     list is the plain stacked list, and the lead stops being special. */
  .information-list--featured {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }

  .information-list--featured .information-inner:first-child {
    grid-template-columns: minmax(0, 1fr);
  }

  .information-inner {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: none;
    padding: var(--space-6) 0;
  }

  .information img {
    grid-row: 1;
    margin-bottom: var(--space-4);
  }

  .information-inner__head {
    grid-column: 1;
    grid-row: 2;
  }

  .information-inner__body {
    grid-column: 1;
    grid-row: 3;
  }

  .information-inner__title {
    font-size: var(--text-lg);
  }

  /* blog.html's plain list (no --featured modifier) keeps the image/text row
     through tablet width instead of stacking here — a full-width thumbnail
     before any title text nearly doubled the page's height at 768px. It
     collapses to the stacked card at 425px instead, below. The home page's
     featured list is untouched; the rules above still govern it. */
  .information-list:not(.information-list--featured) .information-inner {
    grid-template-columns: 220px minmax(0, 1fr);
    grid-template-rows: auto 1fr;
    column-gap: var(--space-6);
    padding: var(--space-8) 0;
  }

  .information-list:not(.information-list--featured) img {
    grid-row: 1 / span 2;
    margin-bottom: 0;
  }

  .information-list:not(.information-list--featured) .information-inner__head {
    grid-column: 2;
    grid-row: 1;
  }

  .information-list:not(.information-list--featured) .information-inner__body {
    grid-column: 2;
    grid-row: 2;
  }

  .information-list:not(.information-list--featured) .information-inner__title {
    font-size: var(--text-2xl);
  }
}

/* On a phone the Blog index leads each card with its title and date: the
   picture used to open the card, which pushed the first title to y=914, below
   the fold. The picture follows the heading block; the excerpt, topics and
   action follow the picture. Scoped to the plain list so the home page's
   featured cards keep their picture-first stack. */
@media (max-width: 425px) {
  .information-list:not(.information-list--featured) .information-inner {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: none;
    row-gap: var(--space-4);
    padding: var(--space-6) 0;
  }

  .information-list:not(.information-list--featured) .information-inner__head {
    grid-column: 1;
    grid-row: 1;
  }

  .information-list:not(.information-list--featured) img {
    grid-row: 2;
  }

  .information-list:not(.information-list--featured) .information-inner__body {
    grid-column: 1;
    grid-row: 3;
  }

  .information-list:not(.information-list--featured) .information-inner__meta {
    margin-bottom: 0;
  }

  .information-list:not(.information-list--featured) .information-inner__title {
    font-size: var(--text-lg);
  }
}

/* BLOG */

.blog-information {
  padding-bottom: var(--space-16);
}

/* The masthead: the title alone on the ink band. Every article used to share
   one stock photograph under a white title with no scrim, which read as a
   template and fought the title on a phone. The band is the site's own colour
   and the type is the only picture. */
.blog-headline {
  background: var(--color-ink);
  margin: 0 0 var(--space-12);
  padding: var(--space-16) 0;
}

.blog-headline__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--type-title);
  line-height: var(--leading-tight);
  max-width: 20ch;
  margin: 0;
}

.blog-information__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 50px;
  line-height: 65px;
  margin-bottom: 40px;
}

.information-text {
  font-size: 24px;
  line-height: 43px;
  font-weight: 400;
  margin-bottom: 60px;
}

.information-text:last-child {
  font-size: 16px;
  line-height: 28px;
}

@media (max-width: 1024px) {
  .blog-headline {
    padding: var(--space-12) 0;
  }

  .blog-information__title {
    font-size: 30px;
    line-height: 40px;
  }
}

@media (max-width: 425px) {
  .blog-headline {
    padding: var(--space-10) 0;
  }
}

/* Headings inherit the UA default of bold (700). The display face ships 500
   and 600, so a 700 request resolves to 600 — the right result, but only by
   accident. Stated explicitly instead. */
h1,
h2,
h3,
h4,
h5,
h6 {
  font-weight: var(--weight-semibold);
}

/* Form controls do not inherit font-family: the UA stylesheet gives them
   Arial. The burger and accordion buttons were already reset when they became
   <button>; these are the ones that were missed, so both submit buttons and
   every input rendered in Arial while the page around them did not. */
button,
input,
textarea,
select {
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
}

/* MEASURE AND LINE BREAKING
   ---------------------------------------------------------------------------
   Nothing on this site constrained line length except the one article. The
   careers detail page — the second-largest body of prose here — was running at
   113 characters per line; a book column sets 45-75. These rules give every
   block that holds sentences a measure, and give the browser what it needs to
   break lines well.

   ch resolves against the element's own font-size, so each rule declares the
   size it measures at.
   --------------------------------------------------------------------------- */

.careers__text,
.services__text,
.services-list__item,
.about-wrap__text,
.team__text,
.contacts-item__text,
.information-text {
  /* .article-p and .article-list used to be in this group. They are governed
     by the article grid's text track now, which is the wider technical
     measure; leaving them here capped them at the marketing measure and the
     grid change had no visible effect. */
  max-width: var(--measure);
}

/* Prose wants hyphenation and widow control; headings want balance. Both are
   ignored by browsers that do not implement them, so neither needs a guard. */
.article-p,
.careers__text,
.services__text,
.about-wrap__text,
.team__text,
.information-text,
.headline__description {
  hyphens: auto;
  /* Without a floor, a 68-character measure hyphenates two-syllable words —
     "com-mands", "archi-tectural" — several lines in a row. Seven characters
     minimum, four before the break and three after. */
  hyphenate-limit-chars: 8 4 4;
  text-wrap: pretty;
  hanging-punctuation: first last;
}

/* List lines are short and often carry a bold name; "busi-ness workflows"
   under **Tencent** is not worth the ragged edge it saves. */
.article-list li {
  hyphens: manual;
  text-wrap: pretty;
}

.title,
.headline__title,
.blog-headline__title,
.about-title,
.team-title,
.careers__title,
.services-title,
.blog-information__title,
.contacts-header,
.article-h3 {
  text-wrap: balance;
}

/* Long URLs in the article's research notes are the one place text can exceed
   its box. */
.article-list a,
.article-p a {
  overflow-wrap: anywhere;
}

/* Figures that line up in a column need to be the same width. */
.article-table {
  font-variant-numeric: tabular-nums slashed-zero;
}

/* ARTICLE
   ---------------------------------------------------------------------------
   Long-form blog post. The existing blog template only ever carried a single
   block of body text, so headings, lists, tables and figures needed styles.
   Everything below is built from the tokens.
   --------------------------------------------------------------------------- */

/* The deck: one sentence that says what the piece argues. Set apart from the
   body it introduces — larger, italic, in the heading colour, and closed by a
   short rule — so it reads as the subtitle it is and not as paragraph one. */
.article-standfirst {
  font-size: var(--text-xl);
  line-height: 1.45;
  font-style: italic;
  color: var(--color-ink);
  max-width: var(--measure-wide);
  margin-bottom: var(--rhythm-double);
  padding-bottom: var(--rhythm);
  position: relative;
}

.article-standfirst::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: var(--space-12);
  height: var(--border-2);
  background: var(--color-indigo);
}

/* Article prose is set in Literata, not Inter. Inter is an interface face —
   excellent at 14-16px in a nav or a table, and used here for exactly those.
   Several thousand words of argument is a different job, which is what a
   screen-optimised reading serif is for. The byline, the download buttons,
   the tables and the captions deliberately stay on --font-sans: they are
   furniture around the piece rather than the piece. */
.article,
.article-p,
.article-list,
.article-standfirst,
.article-h2,
.article-h3 {
  font-family: var(--font-serif);
}

/* <code> had no face at all: no code, pre, kbd or monospace selector existed
   anywhere, so the one <code> on the site resolved to whatever fontconfig
   offered — which differed between a local build and CI. */
code,
kbd,
samp,
pre {
  font-family: var(--font-mono);
  /* The UA default shrinks monospace to ~0.8125em, which is visibly small
     against the surrounding text; JetBrains Mono has a tall x-height and does
     not need the reduction. */
  font-size: 0.9em;
  font-variant-ligatures: none;
}

.article {
  /* em resolves against this element's own font-size, so the measure has to
     be declared at the size the paragraphs actually render at. Without the
     font-size here, the measure was computed against the inherited 16px
     while the text set at 18px, giving a 76-character line. */
  font-size: var(--text-md);

  /* Three tracks. The centre one carries the measure and the two rails
     centre it; the diagram and the tables opt out into the full container.
     Previously the measure was a max-width on .article itself, which left
     the text flush against the left edge of a centred 930px box with 390px
     of nothing beside it — and trapped the diagram at 522px, where its 15px
     labels painted at 8.9px.

     The text track starts at the column's left edge rather than being
     centred in it, so prose, tables and figures share one left margin and
     the figures extend right from it. Centring the text left 229px of
     nothing on each side of it while every table beside it used the full
     980px, which is what made the column look stranded.

     minmax(0, 1fr) rather than 1fr: a plain 1fr has an auto minimum, so a
     wide table would force the rail open. */
  display: grid;
  grid-template-columns:
    [full-start text-start] min(100%, var(--measure-technical)) [text-end]
    minmax(0, 1fr) [full-end];
}

.article > * {
  grid-column: text;
}

.article-figure,
.article-table-wrap {
  grid-column: full;
}

/* The column is 40em so that tables and the diagram have room; prose does
   not want it. At 18px Literata 34em is about 70 characters, inside the 45–75
   that reading comfort asks for; the 40em line ran to 90. */
.article-p,
.article-list,
.article-quote,
.article-h2,
.article-h3 {
  max-width: var(--measure-wide);
}

.article-p {
  font-size: var(--text-md);
  line-height: var(--leading-loose);
  margin-bottom: var(--rhythm);
}

.article-h2 {
  font-size: var(--type-subsection);
  line-height: var(--leading-30);
  font-weight: var(--weight-semibold);
  color: var(--color-ink);
  margin-top: var(--space-10);
  margin-bottom: var(--space-4);
  text-wrap: balance;
  /* Landing room for the rail's contents links. */
  scroll-margin-top: var(--space-6);
}

.article-h3 {
  font-size: var(--text-lg);
  line-height: var(--leading-26);
  font-weight: var(--weight-semibold);
  color: var(--color-ink);
  margin-top: var(--space-6);
  margin-bottom: var(--space-2);
}

.article-list {
  margin: 0 0 var(--rhythm) var(--space-4);
  padding-left: var(--space-2);
  font-size: var(--text-md);
  line-height: var(--leading-loose);
}

.article-list li {
  list-style: disc;
  margin-bottom: var(--rhythm-half);
}

ol.article-list li {
  list-style: decimal;
}

/* These sit in the grid's full track, so they are wider than the measure
   without scrolling. The overflow is the narrow-viewport safety valve: below
   about 950px a three-column table can still exceed its track. */
.article-table-wrap,
.article-figure {
  overflow-x: auto;
  margin: 0 0 var(--rhythm-double);
}

.article-table {
  border-collapse: collapse;
  width: 100%;
  font-size: var(--text-2xs);
  line-height: var(--leading-20);
}

.article-table th,
.article-table td {
  text-align: left;
  padding: var(--space-2) var(--space-3);
  border-bottom: var(--border-1) var(--border-style) var(--color-hairline);
  vertical-align: top;
}

.article-table th {
  font-weight: var(--weight-semibold);
  color: var(--color-ink);
  border-bottom-color: var(--color-ink);
}

/* On a phone each row is a group: its first cell heads it, and every other
   cell carries its column's name, which build-site.py writes into data-label
   from the table's own header row. Three ~120px columns had wrapped cells to
   five to seven lines and one table to 1,028px
   (docs/visual-assessment/2026-10-20-mobile.md, finding 12). The header row
   stays in the page for assistive technology, off screen; the PDF and
   Markdown exports keep real tables. */
@media (max-width: 425px) {
  .article-table thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .article-table,
  .article-table tbody,
  .article-table tr,
  .article-table td {
    display: block;
  }

  .article-table {
    border-top: var(--border-1) var(--border-style) var(--color-ink);
  }

  .article-table tr {
    padding: var(--space-3) 0;
    border-bottom: var(--border-1) var(--border-style) var(--color-hairline);
  }

  .article-table td {
    padding: 0;
    border-bottom: 0;
  }

  .article-table td + td {
    margin-top: var(--space-2);
  }

  .article-table td:first-child {
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    color: var(--color-ink);
  }

  /* A screen reader reads this label as part of the cell. That is kept on
     purpose: where the table's semantics survive display: block, a reader
     moving through the table hears the column twice; where they do not
     (Safari has dropped them, and ARIA in HTML allows no role on a row, a
     header or a cell to restore them, see build-site.py), the label is the only column
     cue left. Hiding it with an empty alternative (content: ... / "") would
     trade a repetition for a loss. */
  .article-table td:not(:first-child)::before {
    content: attr(data-label);
    display: block;
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    color: var(--color-muted);
  }
}

.article-figure {
  margin-bottom: var(--space-8);
}

.article-diagram {
  display: block;
  width: 100%;
  max-width: 100%;
  height: auto;
  background-color: var(--color-ink);
  padding: var(--space-4);
}

/* Phone variants. A diagram is drawn for the width it is read at, and the
   desktop drawings scale down past legibility: the Agent Stack's 880-unit
   viewBox put its labels at 5.6px on a 390 phone and the service diagrams'
   440 units put theirs at 8.6px at 320
   (docs/visual-assessment/2026-10-20-mobile.md, findings 2 and 7). Each
   diagram carries a portrait sibling, 260 units wide; with this padding it
   is drawn 1:1 on a 320px phone, so its 13-unit labels are 13px there and
   grow from it, up to the 400px cap (18.5px) so a tablet does not draw it
   at poster size. The swap is display, so the hidden drawing leaves the
   accessibility tree, and each keeps its own title and description.
   check-diagram-labels holds the floor: no label under 12px at 320.
   The exports read the first drawing in the figure, the desktop one.

   Every drawing swaps at 768, not at the 425 phone breakpoint: the largest
   phones are 428-440px wide, and a desktop drawing has to clear 12px at the
   narrowest width it shows. A service drawing (440 units, in a figure capped
   at 520px) clears it from a 476px viewport, and in the desktop lede's 460px
   track. The Agent Stack is drawn 720 units wide for the same reason: the
   article's column is narrowest at 769 (no rail yet) and at 1025 (the rail
   back), about 705px of drawing at both, which puts its 13-unit labels at
   12.7px. At its old 880 units it needed its own swap at 1024 and still
   drew 10.4px labels at 1025. check-responsive sweeps every width. */
.article-diagram--phone {
  display: none;
  max-width: 400px;
  padding: var(--space-3);
}

@media (max-width: 768px) {
  .article-diagram--desktop {
    display: none;
  }

  .article-diagram--phone {
    display: block;
  }
}

/* A path too, for a container drawn with openings in it: the diagram kit's
   sandbox wall, broken where a gate sits in it and where the trace leaves. */
.diagram-box rect,
.diagram-box path {
  fill: none;
  stroke: var(--color-cyan);
  stroke-width: 1.5;
}

.diagram-line path {
  fill: none;
  stroke: var(--color-mint);
  stroke-width: 1.5;
}

.diagram-head path {
  fill: var(--color-mint);
}

.diagram-label text {
  fill: var(--color-white);
  font-family: var(--font-sans);
  font-size: 15px;
  font-weight: var(--weight-medium);
}

.diagram-label--small text {
  font-size: 13px;
  font-weight: var(--weight-regular);
}

/* The diagram kit (src/_partials/diagram-kit.html). A glyph is a group scaled
   to its placed size, and the macro writes the stroke width that undoes the
   scale onto the group itself, so its line is the diagram's own 1.5 units at
   any size. That is why no stroke-width is set here: a CSS stroke-width beats
   the attribute and would scale with the glyph. No vector-effect either,
   which WeasyPrint does not implement, so the PDF would draw a different
   weight from the page. The star is the one solid, and the only white. */
.diagram-glyph {
  fill: none;
  stroke: var(--color-mint);
  stroke-linecap: round;
  stroke-linejoin: round;
}

.diagram-star {
  fill: var(--color-white);
  stroke: none;
}

/* Flows drawn as lines take their kind from the line: dashed is the way
   back, a run of short round dashes is a trace: zero-length dots at a 1.5-unit
   line were too faint to read at 320px. */
.diagram-line--back path {
  stroke-dasharray: 6 5;
}

.diagram-line--trace path {
  stroke-dasharray: 2 5;
  stroke-linecap: round;
}

/* No article carried a raster image until now, so .article-figure had never
   had to size one. The figures sit in the grid's full track, which is wider
   than the prose; a landscape render fills it, but a square or portrait one
   at 980px would be taller than the viewport, so those get their own cap. */
.article-figure img {
  display: block;
  width: 100%;
  height: auto;
}

.article-figure--inset img {
  max-width: 560px;
  margin-inline: auto;
}

/* The LinkedIn cards are 1200x1500 and dense; shown small they are a visual
   summary, and the link opens the readable original. */
.article-figure--poster img {
  max-width: 620px;
  margin-inline: auto;
  border: var(--border-1) var(--border-style) var(--color-hairline);
}

.article-figure__zoom {
  display: block;
}

.article-figure__zoom:hover img {
  opacity: 0.92;
}

.article-figure__open {
  white-space: nowrap;
}

.article-caption {
  font-size: var(--text-2xs);
  line-height: var(--leading-loose);
  color: var(--color-muted);
  margin-top: var(--rhythm-half);
  /* The caption is part of the figure, so it takes the figure's width: the
     full column under a full-width picture, the picture's own width under an
     inset or a poster. A caption narrower than its picture read as a
     paragraph that had wandered. */
  max-width: none;
  text-wrap: pretty;
}

.article-figure--inset .article-caption {
  max-width: 560px;
}

.article-figure--poster .article-caption {
  max-width: 620px;
  margin-inline: auto;
}

/* Links, pulled quotes and footnote marks arrived with the second batch of
   articles; the first article carried none of the three. The global reset
   strips underline and colour from every link, so without this an inline
   link in body text is invisible. */
.article a {
  color: var(--color-indigo);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

.article-quote {
  margin: 0 0 var(--space-4);
  padding-left: var(--space-4);
  border-left: var(--border-2) var(--border-style) var(--color-indigo);
  font-size: var(--text-lg);
  line-height: var(--leading-30);
  color: var(--color-ink);
}

.article-quote p {
  margin: 0;
}

.article-fnref {
  font-size: var(--text-xs);
  line-height: 0;
  margin-left: 2px;
}

/* The section's own address, shown when the heading is hovered or the link
   is focused; the id was already there for the contents list. */
.article-anchor {
  margin-left: 0.4em;
  color: var(--color-muted);
  text-decoration: none;
  opacity: 0;
}

.article-h2:hover .article-anchor,
.article-anchor:focus-visible {
  opacity: 1;
}

/* Topics: three words that say what the piece is about. Not links yet; a
   filter arrives when there is enough to filter. Square, like every other box
   on the site: they were pills (999px), which the flat rule does not allow
   (docs/visual-assessment/2026-10-20-mobile.md, finding 10). */
.topics {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  margin: 0 0 var(--space-3);
  padding: 0;
}

.topics__item {
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  line-height: var(--leading-normal);
  color: var(--color-muted);
  border: var(--border-1) var(--border-style) var(--color-hairline);
  padding: 1px var(--space-2);
}

.article-byline .topics {
  margin: var(--space-2) 0 0;
}

/* The way back from a note to the sentence that cited it. */
.article-fnback {
  margin-left: var(--space-1);
  text-decoration: none;
}

/* A link that leaves the site says so before it is followed; the marker is
   decorative, the visually hidden span in the markup carries the meaning. */
.article a[href^="http"]::after {
  content: " \2197";
  font-size: 0.8em;
}

/* THE END OF THE PIECE: author, neighbours, one next step. */

.article-end {
  grid-column: full;
  margin-top: var(--rhythm-double);
  padding-top: var(--rhythm);
  border-top: var(--border-1) var(--border-style) var(--color-hairline);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
}

.article-end__author {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-bottom: var(--rhythm);
}

/* Round: a portrait is the site's one rounded shape, recorded as the exception
   to the flat rule in CLAUDE.md and the style profile. A face cropped to a
   circle reads as a person; the same crop square reads as a thumbnail. */
.article-end__portrait {
  width: 72px;
  height: 72px;
  border-radius: 50%;
  flex: none;
}

.article-end__who {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
}

.article-end__name {
  font-weight: var(--weight-semibold);
  color: var(--color-ink);
}

.article-end__role {
  color: var(--color-muted);
}

.article-end__link {
  color: var(--color-ink);
  text-decoration: underline;
}

.article-end__nav {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4);
  margin-bottom: var(--rhythm);
}

/* The whole card is the link; the article's underline rule would otherwise
   underline the label, the title and the date as three separate fragments. */
.article .article-end__card {
  text-decoration: none;
}

.article-end__card {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-4);
  border: var(--border-1) var(--border-style) var(--color-hairline);
  color: var(--color-ink);
}

.article-end__card--newer {
  grid-column: 2;
  text-align: right;
}

.article-end__card:hover .article-end__title {
  color: var(--color-indigo);
}

.article-end__label {
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
}

.article-end__title {
  font-family: var(--font-sans);
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
  text-wrap: balance;
}

.article-end__meta {
  color: var(--color-muted);
}

.article-end__cta {
  margin: 0;
  color: var(--color-ink);
}

.article-end__cta a {
  color: var(--color-indigo);
  text-decoration: underline;
}

@media (max-width: 768px) {
  .article-end__nav {
    grid-template-columns: minmax(0, 1fr);
  }

  .article-end__card--newer {
    grid-column: 1;
    text-align: left;
  }
}

/* BYLINE AND DOWNLOADS
   ---------------------------------------------------------------------------
   Every article credited "StarDevs" in a hand-typed line that had already
   drifted a weekday away from the ISO date it duplicated. Both now render
   from _data/site.json through _partials/byline.html.

   Deliberately not .team-founder__*: that component is a full-width founder
   feature built for a 910px container, and reusing it inside a 522px text
   track would mean overriding its layout, width, size and leading — a
   rewrite wearing the old names. The portrait asset is shared; the component
   is new.
   --------------------------------------------------------------------------- */

.article-byline {
  display: flex;
  align-items: flex-start;
  margin-bottom: var(--rhythm-double);
  padding-bottom: var(--rhythm);
  border-bottom: var(--border-1) var(--border-style) var(--color-hairline);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
}

.article-byline__portrait {
  flex-shrink: 0;
  width: var(--space-12);
  height: var(--space-12);
  margin-right: var(--space-3);
}

.article-byline__who {
  display: flex;
  flex-direction: column;
}

.article-byline__name {
  color: var(--color-ink);
  font-weight: var(--weight-semibold);
}

.article-byline__role,
.article-byline__meta {
  color: var(--color-muted);
}

.article-downloads {
  margin-bottom: var(--rhythm-double);
  font-size: var(--text-sm);
}

.article-downloads__label {
  color: var(--color-muted);
  margin-right: var(--space-2);
}

.article-downloads__link {
  display: inline-block;
  padding: var(--space-1) var(--space-3);
  margin-right: var(--space-2);
  border: var(--border-1) var(--border-style) var(--color-ink);
  color: var(--color-ink);
  font-weight: var(--weight-semibold);
}

.article-downloads__link:hover {
  background-color: var(--color-ink);
  color: var(--color-white);
}

/* THE ARTICLE RAIL
   ---------------------------------------------------------------------------
   The byline and the downloads used to sit in the text track, stacked between
   the standfirst and the first paragraph: on a 1440 screen that pushed the
   opening sentence to the fold, so the first thing an article showed was who
   wrote it and how to leave. They move to a rail beside the text, which also
   spends the 194px of empty margin the centred measure left on either side.

   The rail is a sibling of <article class="article">, not a child. That is
   load-bearing twice over: article_export.parse_article only walks the
   article element, so nothing here reaches the PDF or the Markdown or the
   word count that feeds "N min read"; and .article's own three-track grid is
   left completely untouched, so the measure and the full-bleed span keep
   resolving exactly as before — now against a 980px column rather than 910.

   Rules for the rail's own copy are scoped under .article-aside rather than
   written onto .article-byline/.article-downloads, because a stub article once
   still carries a byline inline, in the body, with no rail around it.
   --------------------------------------------------------------------------- */

.article-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--article-rail);
  column-gap: var(--space-8);
}

/* Both explicit: the rail comes first in the source so that the collapsed
   layout puts the byline above the body and reading order matches paint
   order, which it would not if this relied on auto-placement or `order`. */
.article-layout > .article {
  grid-column: 1;
  grid-row: 1;
}

.article-aside {
  grid-column: 2;
  grid-row: 1;
  /* Without this the item stretches to the row's height and sticky has no
     distance to travel — the rail would simply sit still at the top. */
  align-self: start;
  position: sticky;
  top: var(--space-6);
  /* No max-height here. Clipping the rail itself cut off whatever came last
     — Share — and a flex column that hands the surplus to the contents list
     only moved the problem into a nested scroll area. The contents list is
     capped instead, which keeps the rail short enough to fit unaided. */
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
}

.article-aside__module {
  padding-bottom: var(--rhythm);
  margin-bottom: var(--rhythm);
  border-bottom: var(--border-1) var(--border-style) var(--color-hairline);
}

.article-aside__module:last-child {
  padding-bottom: 0;
  margin-bottom: 0;
  border-bottom: 0;
}

/* The first use of the eyebrow pairing tokens.css describes at --text-xs and
   --tracking-label. Uppercase is done with the glyphs, not text-transform,
   would be better still — but these three strings are written in the markup
   and read the same either way. */
.article-aside__label {
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  line-height: var(--leading-normal);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
  margin: 0 0 var(--space-2);
}

/* The rail's byline turns vertical: 220px cannot hold a 60px portrait and a
   two-line job title beside it. */
.article-aside .article-byline {
  display: block;
  margin-bottom: 0;
  padding-bottom: 0;
  border-bottom: 0;
  font-size: var(--text-sm);
}

.article-aside .article-byline__portrait {
  margin: 0 0 var(--space-2);
}

.article-aside .article-downloads {
  margin-bottom: 0;
  font-size: var(--text-sm);
}

.article-aside .article-downloads__label {
  display: block;
  margin: 0 0 var(--space-2);
}

/* Stacked, and full width of the rail, so the two buttons present one edge
   rather than a ragged pair. */
.article-aside .article-downloads__link {
  display: block;
  margin: 0 0 var(--space-2);
  text-align: center;
}

.article-aside .article-downloads__link:last-child {
  margin-bottom: 0;
}

/* In the rail the disclosure is always open, so its summary should read as
   the same eyebrow the other modules carry rather than as a control. The
   collapsed layout re-dresses it below. */
.article-toc-summary {
  list-style: none;
  cursor: pointer;
}

.article-toc-summary::-webkit-details-marker {
  display: none;
}

/* The wrapper around downloads, contents and share. At rail widths its own
   summary is hidden (below) and index.js keeps it open, so the rail is three
   modules exactly as before. Below 1024px it is the one disclosure the
   collapsed layout shows. */
.article-tools-disclosure__body {
  padding-top: var(--rhythm);
}

.article-tools-summary {
  list-style: none;
  cursor: pointer;
  padding-bottom: 0;
  margin: 0;
}

.article-tools-summary::-webkit-details-marker {
  display: none;
}

@media (min-width: 1025px) {
  .article-tools-summary {
    display: none;
  }

  .article-tools-disclosure__body {
    padding-top: 0;
  }
}

.article-toc,
.article-share {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Seventeen headings is taller than the window on its own, and a rail that
   outgrows the viewport pins its own foot out of reach for the whole article.
   Capping the list is what keeps the other three modules — Share especially —
   on screen. 22em holds about nine entries. */
.article-toc {
  max-height: 22em;
  overflow-y: auto;
  /* The cut used to land mid-line with nothing to say the list went on. The
     fade says it; the padding puts the last entry above the fade when the
     list is scrolled to its end. */
  padding-bottom: 2.5em;
  mask-image: linear-gradient(to bottom, #000 calc(100% - 2.5em), transparent);
}

.article-toc__item {
  margin-bottom: var(--space-2);
}

.article-toc__item:last-child,
.article-share__item:last-child {
  margin-bottom: 0;
}

/* The rail sits outside .article, so none of that block's link colours,
   underlines or visited rules reach it. Quieter on purpose: this is
   navigation around the piece, not a citation inside it. */
/* Each entry is its own line, so it is a target in its own right: padded
   to the 24px WCAG 2.2 asks of anything that is not inline text. */
.article-toc__link[aria-current] {
  color: var(--color-ink);
  font-weight: var(--weight-semibold);
}

.article-toc__link {
  display: inline-block;
  padding-block: var(--space-1);
  color: var(--color-muted);
}

/* Underlined, not merely coloured: against the rail's own ink body text these
   would otherwise be distinguished by nothing at all, and ink-on-ink is 1:1 —
   WCAG 1.4.1 wants 3:1 before colour alone may carry a link. The contents
   list opposite is exempt as a navigation block, and is muted besides. */
.article-share__link,
.article-aside .article-byline__name {
  color: var(--color-ink);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

.article-toc__link:hover,
.article-share__link:hover,
.article-aside .article-byline__name:hover {
  color: var(--color-ink);
  text-decoration: underline;
  text-underline-offset: 0.15em;
  text-decoration-thickness: var(--border-2);
}

.article-share__item {
  margin-bottom: var(--space-2);
}

/* A button so it is announced and operated as one; the rest of its look is
   borrowed from the download links, which are the rail's other controls. */
.article-share__copy {
  display: block;
  width: 100%;
  padding: var(--space-1) var(--space-3);
  border: var(--border-1) var(--border-style) var(--color-ink);
  background-color: transparent;
  color: var(--color-ink);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  text-align: center;
  cursor: pointer;
}

.article-share__copy:hover {
  background-color: var(--color-ink);
  color: var(--color-white);
}

/* The global ring is cyan, which measures 1.69:1 on white and fails SC
   1.4.11 on every light section. Indigo is 11.5:1. The site-wide fix is a
   separate job; this at least makes the article's own links focusable
   visibly. */
.article a:focus-visible,
.article-aside a:focus-visible,
.article-share__copy:focus-visible,
.article-downloads__link:focus-visible {
  outline-color: var(--color-indigo);
}

/* Colour alone is not enough: indigo against the black body text measures
   1.83:1, well under the 3:1 that WCAG 1.4.1 requires before a link may be
   distinguished by colour without another cue. Hence the underline, and a
   thickness change rather than only a colour change on hover. */
.article a:visited {
  color: var(--color-navy);
}

.article a:hover {
  color: var(--color-ink);
  text-decoration-thickness: var(--border-2);
}

/* Below the rail's own breakpoint there is no second column to put it in:
   980 + 40 + 220 needs room the 1024 ladder step no longer has. It becomes a
   band above the body — which is where the source order already puts it, so
   nothing has to be reordered. */
@media (max-width: 1024px) {
  .article-layout {
    display: block;
  }

  .article-aside {
    position: static;
    margin-bottom: var(--rhythm-double);
    padding-bottom: var(--rhythm);
    border-bottom: var(--border-1) var(--border-style) var(--color-hairline);
  }

  /* Wide again, so the modules sit side by side rather than as one tall
     column the reader has to scroll past to reach the article. */
  .article-aside__module {
    padding-bottom: 0;
    margin-bottom: var(--rhythm);
    border-bottom: 0;
  }

  .article-aside .article-byline {
    display: flex;
    align-items: flex-start;
  }

  .article-aside .article-byline__portrait {
    margin: 0 var(--space-3) 0 0;
  }

  /* Closed by default here, so it has to look like something to open. The
     inner "On this page" toggle only matters at rail widths — nested inside
     this one, its own summary is hidden and it stays open (below), so
     opening this disclosure shows the list directly.

     A row, not an eyebrow: at 12px muted capitals with a "+" it was the
     faintest thing on an article's first screen, over the downloads and the
     contents (docs/visual-assessment/2026-10-20-mobile.md, finding 11). It is
     the drawer's row now: 44px, ink, the chevron at its edge on
     currentColor, pointing down once open. */
  .article-tools-summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    min-height: var(--touch-target);
    border-top: var(--border-1) var(--border-style) var(--color-hairline);
    font-size: var(--text-sm);
    color: var(--color-ink);
  }

  .article-tools-summary::after {
    content: "";
    width: 0.5em;
    height: 0.875em;
    background-color: currentColor;
    -webkit-mask: url("images/arrow-right.svg") no-repeat center / contain;
    mask: url("images/arrow-right.svg") no-repeat center / contain;
    transition: transform var(--duration-base) var(--ease-base);
  }

  .article-tools-disclosure[open] .article-tools-summary::after {
    transform: rotate(90deg);
  }

  .article-tools-disclosure .article-toc-summary {
    display: none;
  }

  /* No rail to fit inside here, and a scroll region nested in the page is a
     worse way to read a list than simply letting it run. */
  .article-toc {
    margin-top: 0;
    max-height: none;
    overflow-y: visible;
    padding-bottom: 0;
    mask-image: none;
  }

  .article-aside__module--contents {
    margin-top: var(--space-2);
  }
}

@media (max-width: 768px) {
  .article-standfirst {
    font-size: var(--text-lg);
  }

  /* The loose leading exists to make a ~93-character line easy to track back
     from. Below this width the text is narrower than the measure — about 45
     characters on a phone — and the same leading just looks airy and makes
     the page longer to scroll. */
  .article-p,
  .article-list {
    line-height: var(--leading-normal);
  }
}

/* CAREERS */
.careers {
  padding: 104px 0;
}

.careers__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--type-section);
  line-height: var(--leading-snug);
  /* Was an inline style on the heading. */
  margin-bottom: var(--space-8);
}

.careers__text {
  font-size: 16px;
  line-height: 28px;
}

/* The empty state that replaced two placeholder job cards. */
.careers-roles {
  margin-top: var(--space-16);
}

/* The contact form's submit. The careers application form and its rules
   left with the placeholder role; see docs/site-visual-audit.md. */
.submit-button {
  padding: 10px 20px;
  font-weight: 400;
  font-size: 18px;
  line-height: 27px;
  background: var(--color-navy);
  color: var(--color-white);
  border: none;
  display: block;
  margin-left: auto;
  margin-top: 38px;
  cursor: pointer;
}

.submit-button:hover {
  background-color: var(--color-indigo);
}

/* ABOUT */

.about {
  padding: var(--space-12) 0 var(--space-20);
  background-color: var(--color-surface);
}

.about-title {
  font-family: var(--font-display);
  font-size: var(--type-section);
  line-height: var(--leading-snug);
  max-width: 710px;
  margin-bottom: var(--space-6);
}

.about-wrap {
  max-width: var(--measure-wide);
}

.about-wrap__text + .about-wrap__text {
  margin-top: var(--space-3);
}

/* TEAM: the founder card, the bio, the statement. */

.team {
  padding: var(--space-16) 0;
}

/* The same size as the other section titles on the page. */
.team-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--type-section);
  line-height: var(--leading-snug);
  margin-bottom: var(--space-8);
}

.team__text + .team__text {
  margin-top: var(--space-4);
}

.person {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  margin-bottom: var(--space-8);
}

/* Round, the flat rule's one exception: see .article-end__portrait. */
.person__portrait {
  width: 144px;
  height: 144px;
  object-fit: cover;
  border-radius: 50%;
  flex: none;
}

.person__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.person__who {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
}

.person__name {
  font-family: var(--font-sans);
  font-size: var(--text-2xl);
  line-height: var(--leading-snug);
  font-weight: var(--weight-semibold);
  color: var(--color-ink);
}

.person__role,
.person__based {
  color: var(--color-muted);
}

.person__links {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin: 0;
  padding: 0;
  font-size: var(--text-sm);
}

.person__links a {
  color: var(--color-ink);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

.person__links a:hover {
  color: var(--color-indigo);
}

.team-quote {
  margin: var(--space-8) 0 0;
  max-width: var(--measure-wide);
}

.team-quote p + p {
  margin-top: var(--space-3);
}

.team-more {
  margin-top: var(--space-4);
  max-width: var(--measure-wide);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
}

.team-more summary {
  cursor: pointer;
  color: var(--color-ink);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

.team-more .article-quote {
  margin-top: var(--space-3);
}

.team-quote figcaption {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  color: var(--color-muted);
  margin-top: var(--space-2);
}

/* TRACK RECORD: numbers, roles, schooling and credentials. */

.record {
  padding: var(--space-12) 0 var(--space-16);
  border-top: var(--border-1) var(--border-style) var(--color-hairline);
}

.record-title {
  font-family: var(--font-display);
  font-size: var(--type-section);
  line-height: var(--leading-snug);
  margin-bottom: var(--space-8);
}

.numbers {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-6);
  margin: 0 0 var(--space-10);
  padding: 0;
}

.numbers__item dt {
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
}

.numbers__item dd {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-4xl);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
  color: var(--color-ink);
}

.roles {
  list-style: none;
  display: grid;
  gap: var(--space-6);
  margin: 0 0 var(--space-10);
  padding: 0;
}

.roles__item {
  display: grid;
  grid-template-columns: 9em minmax(0, 1fr);
  column-gap: var(--space-6);
  row-gap: var(--space-1);
  padding-left: var(--space-4);
  border-left: var(--border-2) var(--border-style) var(--color-hairline);
}

.roles__when {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  color: var(--color-muted);
}

.roles__org {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
}

.roles__title {
  font-weight: var(--weight-regular);
  color: var(--color-muted);
}

.roles__line,
.roles__awards {
  grid-column: 2;
  margin: 0;
  max-width: var(--measure-wide);
}

.record-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-8);
}

.eyebrow,
.record-h3 {
  margin: 0 0 var(--space-3);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
}

.creds {
  list-style: none;
  display: grid;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
}

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

.record-next {
  margin: var(--space-10) 0 0;
  padding-top: var(--space-6);
  border-top: var(--border-1) var(--border-style) var(--color-hairline);
  font-weight: var(--weight-semibold);
}

.record-next a {
  color: var(--color-indigo);
}

.record-next a:hover {
  text-decoration: underline;
}

.record-next span {
  margin: 0 var(--space-2);
  color: var(--color-muted);
}

@media (max-width: 768px) {
  .numbers {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .roles__item {
    grid-template-columns: minmax(0, 1fr);
  }

  .roles__line,
  .roles__awards {
    grid-column: 1;
  }

  .record-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

@media (max-width: 425px) {
  .team {
    padding: var(--space-8) 0;
  }

  .person {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-4);
  }

  /* The four figures stay two-up from the tablet rule above, so they scan as
     one group instead of four sections. The quote drops to body size and
     normal leading: the statement is long, and at --text-lg it was most of a
     screen on its own. Same colour, so no new contrast pair. */
  .numbers {
    gap: var(--space-4);
    margin-bottom: var(--space-8);
  }

  .team-quote .article-quote {
    font-size: var(--text-md);
    line-height: var(--leading-normal);
  }
}

/* CONTACTS */

.contacts .eyebrow {
  margin-top: var(--space-6);
}

.contacts-item__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin: 0;
}

/* A button, so the profile's 44px button height at every width: it was 32px,
   and on a phone the one control on the email row. */
.contacts-item__copy {
  min-height: var(--touch-target);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--color-ink);
  background: transparent;
  border: var(--border-1) var(--border-style) var(--color-ink);
  padding: var(--space-1) var(--space-3);
  cursor: pointer;
}

.contacts-item__copy:hover {
  background: var(--color-ink);
  color: var(--color-white);
}

.contacts-item__note {
  font-size: var(--text-sm);
  color: var(--color-muted);
  margin: var(--space-3) 0 0;
}

.contacts {
  background: var(--color-surface);
  padding: 78px;
}

.contacts-items {
  display: flex;
  justify-content: space-between;
  gap: var(--space-10);
}

/* The one heading of the contact block, the same size as the page's others. */
.contacts-header {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--type-section);
  line-height: var(--leading-snug);
  margin-bottom: var(--space-6);
}

.contacts-item__text {
  font-weight: 400;
  font-size: 16px;
  line-height: 28px;
  margin-bottom: var(--space-4);
}

.contacts-item__phone {
  color: var(--color-black);
}

.contacts-item__email {
  color: var(--color-black);
}

.contacts-item__message {
  /* Now a <label> rather than a <p>, so it needs an explicit block display to
     sit above its field as it did before. */
  display: block;
  margin-bottom: 5px;
}

.contacts-item__textarea {
  margin-bottom: 20px;
  height: 175px;
  width: 100%;
  max-width: 460px;
  border: none;
  resize: none;
  padding: 16px 20px;
  border: 1px solid transparent;
  font-family: var(--font-sans);
}

.contacts-item__textarea:hover {
  border: 1px solid var(--color-indigo);
}

.contacts-item__input {
  border: none;
  height: 45px;
  width: 100%;
  max-width: 460px;
  padding: 10px;
  font-family: var(--font-sans);
  border: 1px solid transparent;
}

.contacts-item__input:hover {
  border: 1px solid var(--color-indigo);
}

@media (max-width: 1024px) {
  .about-wrap__item {
    max-width: 40%;
  }

  .team-founder {
    max-width: 60%;
  }

  .team-item__image {
    max-width: 260px;
    height: 283px;
  }

  .contacts-items {
    flex-direction: column;
  }

  .contacts-item:last-child {
    margin-top: 40px;
  }

  .submit-button {
    margin-left: 0;
  }
}

/* The band on a phone: 50px over the kicker, 40px under the last fact. The 768
   rule below gives every .headline 96px top and bottom, and it won here too,
   later in source and as specific as .headline--compact, so Services, About,
   Blog, Privacy and 404 opened on a band that filled a Pixel 10's first screen
   (Services 764px, 96px of it empty under the header). The audit left that
   alone on purpose until the owner saw it on the phone (2026-10-26). The
   doubled class outranks the generic rule without touching it; a service
   detail trims further below. Careers' facts still run three across. */
@media (max-width: 425px) {
  .headline.headline--compact {
    padding: var(--space-10) 0 var(--space-8);
  }

  .headline--short .headline__facts--names {
    grid-template-columns: repeat(3, auto);
    justify-content: start;
    gap: var(--space-4);
  }
}

/* A service detail is a 900-1,200 word document, and on a phone its band ran
   701px — the first body heading started at y=844, just outside the viewport.
   The 768 rule below gives every band 96px top and bottom; this modifier
   trims only the detail pages' band. The breadcrumb, title, deck, action and
   all three facts stay, in order. Specificity beats the generic rule without
   touching it. */
@media (max-width: 425px) {
  .headline.headline--service {
    padding: var(--space-8) 0 var(--space-4);
  }

  /* The phone row's size, so the mark is one of two sizes wherever it is. */
  .headline__mark {
    width: 36px;
    height: 36px;
  }

  /* Let the first paragraph of a service detail enter the opening phone
     viewport. Later chapter spacing keeps its existing rhythm, so this is the
     first detail section only, not the light chapter or the one after it. */
  .headline--service + .services--detail {
    padding-top: var(--space-6);
  }

  .headline--service
    + .services--detail
    .container
    > .services-title--section:first-child {
    margin-bottom: var(--space-4);
  }
}

@media (max-width: 768px) {
  .headline {
    padding: 96px 0px;
  }

  .about-wrap {
    display: flex;
    justify-content: space-around;
  }

  .team-inner {
    flex-direction: column;
  }

  .team-founder {
    max-width: none;
  }

  .team-items {
    display: flex;
    flex-wrap: wrap;
  }

  .team-item {
    margin-bottom: 20px;
  }

  /* Vertical padding only: the section's 40px sides sat on top of the
     container's gutter, so "Talk to us" started 35px right of the band. */
  .contacts {
    padding: var(--space-8) 0;
  }
}

@media (max-width: 425px) {
  .about-wrap {
    display: flex;
    justify-content: center;
    flex-direction: column;
  }

  .about-wrap__item {
    max-width: none;
  }

  .about-wrap__item:last-child {
    margin-top: 15px;
  }

  .team-item {
    display: flex;
    justify-content: space-between;
  }

  .team-item__image {
    max-width: 150px;
    height: 150px;
  }

  .team-item__wrap {
    max-width: 50%;
  }
}

/* FOOTER
   One <nav> on a grid: the logo's own track, then five equal columns for the
   header's five top-level items. The grid replaces a space-between flex row
   whose 240px services column left "Blog" and "Contact" 12px apart at 1024
   while the logo column carried 100px of air; and it replaces the 140px
   per-column cap that broke the phone number over two lines at every width
   above a phone. Sizes are the tokens the rest of the site uses; the block
   used to carry its own 13/16/22/33px ladder. */
.footer {
  background-color: var(--color-black);
  color: var(--color-white);
  padding: var(--space-8) 0;
  font-size: var(--text-2xs);
  line-height: var(--leading-normal);
}

.footer__nav {
  display: grid;
  grid-template-columns: auto repeat(5, minmax(0, 1fr));
  column-gap: var(--space-8);
  row-gap: var(--space-6);
  align-items: start;
}

/* The wordmark sits in the bottom third of the logo's 66px box, so the box
   starts above the titles' line for the two baselines to meet. Kept on every
   width: it is a 172px mask, not a cost, and the footer is the last thing a
   reader sees. */
.footer-logo {
  display: block;
  margin-top: calc(-1 * var(--space-6));
  margin-right: var(--space-8);
}

.footer__item-title {
  font-family: var(--font-sans);
  font-weight: var(--weight-semibold);
  font-size: var(--text-xl);
  line-height: var(--leading-snug);
  margin: 0 0 var(--space-4);
}

.footer__item-title a,
.footer-list-link a {
  color: var(--color-white);
}

/* The email address has no break opportunity. It fits its 125px column on a
   320 phone by 10px, and the reader-set text spacing WCAG 1.4.12 asks a page
   to survive (0.12em between letters) makes it 143px, which scrolled every
   page 3px sideways. It breaks only when it would otherwise overflow. */
.footer-list-link a {
  overflow-wrap: anywhere;
}

.footer__item-title a:hover,
.footer-list-link a:hover {
  color: var(--color-mint);
}

.footer-list-link a:active {
  color: var(--color-green);
}

.footer-list-link + .footer-list-link {
  margin-top: var(--space-3);
}

/* A phone number that breaks at its hyphen reads as two numbers. */
.footer-list__nowrap {
  white-space: nowrap;
}

/* The two profile icons, in the Contact column. Paint follows the link's
   colour, so hover and pressed states need no rule of their own. */
.footer-list-link--social {
  display: flex;
  gap: var(--space-4);
}

/* Padding grows the hit area past the visible icon; the negative margin on
   both axes cancels its effect on layout, so the icon's own position and the
   row's gap are unchanged. Vertically that leaves headroom to reach the
   touch-target floor. Horizontally it doesn't: the row's gap is --space-4,
   so a hit area wider than half that (--space-2) on each side would overlap
   its neighbour. The result is ~36-38px wide by ~46px tall — short of 44x44,
   but clear of the 24x24 WCAG 2.5.8 AA floor and the two targets meet
   edge-to-edge without overlapping. */
.footer-list-link--social a {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-3) var(--space-2);
  margin: calc(-1 * var(--space-3)) calc(-1 * var(--space-2));
}

.footer-list-link--social svg {
  display: block;
  fill: currentColor;
}

/* The identity row: legal name, year and the privacy link, under a rule. The
   year is computed by the build from the content's dates (see build-site.py),
   so it never depends on the clock. No address, by decision — the company
   publishes an email and a phone number only. */
.footer__legal {
  margin-top: var(--space-8);
  padding-top: var(--space-4);
  border-top: var(--border-1) var(--border-style) var(--color-cyan-50);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-6);
}

.footer__legal a {
  display: inline-block;
  /* 13px/1.55 is a 20px line box, short of the 24px tap-target floor. The
     negative margin cancels the padding's effect on layout, so the text
     still lines up with its neighbour in the row. */
  padding: var(--space-1) 0;
  margin: calc(-1 * var(--space-1)) 0;
  color: var(--color-white);
  text-decoration: underline;
  text-decoration-color: var(--color-cyan);
  text-underline-offset: 0.2em;
}

.footer__legal a:hover {
  color: var(--color-mint);
}

@media (max-width: 1024px) {
  /* Six tracks no longer fit; the logo takes a row of its own and the five
     columns wrap three-then-two. */
  .footer__nav {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .footer-logo {
    grid-column: 1 / -1;
    margin: 0;
  }
}

/* Three columns hold down to 426px. Two at 768 ran 680px, and 870px once the
   links became 44px rows; three, with Careers and Contact on the second row,
   hold it at 688px (measured 2026-10-20).
   The height threshold is one phone screen: the footer may not be taller than
   the 844px viewport at 390, and check-responsive fails it if it is. It was
   600px (footer-audit #9), set when the links were 16px tall. With 44px
   targets in two columns the rows alone need about 590px before the logo,
   the identity row or any gap, so 600 could only be met by dropping links;
   the owner chose the targets (2026-10-20). */
@media (max-width: 768px) {
  .footer {
    padding: var(--space-6) 0;
  }
}

/* Two columns on a phone. One column ran 838px at 390, the tallest footer on
   the site; two, with 44px rows, run 773px, inside the one-screen threshold
   above. Blog runs across both: its links are article titles, four
   lines each in a 160px column. The logo takes the header's phone size. */
@media (max-width: 425px) {
  .footer__nav {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    row-gap: var(--space-5);
  }

  .footer__item--wide {
    grid-column: 1 / -1;
  }

  .footer-logo .logo__img {
    display: block;
    width: 128px;
    height: auto;
    aspect-ratio: 172 / 66;
  }

  .footer__item-title {
    margin-bottom: 0;
  }
}

/* CONSENT
   The bar that asks before any visit is counted (src/_partials/consent.html,
   emitted only when site.json configures analytics). Fixed to the bottom, so
   it shifts nothing on the page; index.js gives the page scroll padding of
   its height while it is up, so a keyboard never parks focus behind it. It
   is the dark chrome, like the header and footer, so it takes their cyan
   focus ring. Under the menu drawer (z-index 4), which covers it while open.

   The two answers share one rule and nothing else touches either, so "Don't
   allow" can never become the quieter of the two: same size, same border,
   same weight, side by side (the EDPB's cookie-banner taskforce report,
   2023, lists every other arrangement as a way to push a visitor to yes).
   Close appears only when Privacy settings reopens the bar, and is not an
   answer, so it looks like a link. */
.consent {
  position: fixed;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 3;
  padding: var(--space-4) 0;
  background-color: var(--color-ink);
  border-top: var(--border-1) var(--border-style) var(--color-cyan);
  color: var(--color-white);
  font-size: var(--text-md);
  line-height: var(--leading-normal);
}

.consent[hidden] {
  display: none;
}

.consent__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4) var(--space-8);
}

.consent__copy {
  flex: 1 1 30em;
}

.consent__text,
.consent__state {
  margin: 0;
}

.consent__state {
  margin-top: var(--space-2);
  font-weight: var(--weight-semibold);
}

.consent__link {
  color: var(--color-white);
  white-space: nowrap;
  text-decoration: underline;
  text-decoration-color: var(--color-cyan);
  text-underline-offset: 0.2em;
}

.consent__link:hover {
  color: var(--color-mint);
}

/* Equal columns, so the two answers are the same width as well as the same
   style: an auto-sized grid gives every 1fr track the widest one's width. */
.consent__actions {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--space-3);
}

.consent__button,
.consent__close {
  min-height: var(--touch-target);
  padding: 0 var(--space-5);
  white-space: nowrap;
  font: inherit;
  font-weight: var(--weight-semibold);
  color: var(--color-white);
  background-color: transparent;
  cursor: pointer;
}

.consent__button {
  border: var(--border-1) var(--border-style) var(--color-white);
}

.consent__button:hover {
  background-color: var(--color-navy-soft);
}

.consent__close {
  border: 0;
  text-decoration: underline;
  text-decoration-color: var(--color-cyan);
  text-underline-offset: 0.2em;
}

.consent__close:hover {
  color: var(--color-mint);
}

.consent__button[hidden],
.consent__close[hidden] {
  display: none;
}

/* On a phone the two answers split the row between them, so neither is a
   wider target than the other. Close, when the bar is reopened, takes a row
   of its own under them: beside them it squeezed "Allow" to 81px against
   "Don't allow" at 110px on a 320 phone, and broke "Don't allow" onto two
   lines at 390. */
@media (max-width: 425px) {
  .consent {
    padding: var(--space-3) 0;
    font-size: var(--text-sm);
  }

  .consent__actions {
    width: 100%;
    grid-template-columns: 1fr 1fr;
    grid-auto-flow: row;
  }

  .consent__button,
  .consent__close {
    padding: 0 var(--space-3);
  }

  .consent__close {
    grid-column: 1 / -1;
  }
}

/* The footer's way back into the bar, set like the privacy link beside it. */
.footer__settings {
  padding: var(--space-1) 0;
  margin: calc(-1 * var(--space-1)) 0;
  font: inherit;
  color: var(--color-white);
  background: none;
  border: 0;
  text-decoration: underline;
  text-decoration-color: var(--color-cyan);
  text-underline-offset: 0.2em;
  cursor: pointer;
}

.footer__settings:hover {
  color: var(--color-mint);
}

/* TOUCH TARGETS
   The style profile asks 44px of every target on touch (--touch-target), as
   the menu drawer's rows already are. Measured at 390, these standalone links
   were 15-32px tall: the footer's lists, titles and legal link, the
   breadcrumb, the band's feed link, the article list's titles and read
   links, the home page's practice list and section links, the panels' layer
   links, the byline's name, the founder's profiles, the contact links, the
   statement's disclosure, and the article's contents and share lists
   (docs/visual-assessment/2026-10-20-mobile.md, S4).
   Below 768px each is at least --touch-target tall. Links inside running
   prose keep their size: WCAG 2.5.8's spacing exception covers them, and a
   44px line box would break the paragraph's leading. That is why the evidence
   link is listed only as a panel's title: in a sentence it opened a 44px line
   in the middle of the paragraph. Last in the file so it
   wins over each base rule without raising specificity. */
@media (max-width: 768px) {
  .home-hero__secondary,
  .home-proof__link,
  .information__all,
  .information__contact,
  .information-inner__title a,
  .services-list__item > strong > .services-evidence__link,
  .article-byline__name,
  .breadcrumb__link,
  .headline__feed,
  .information-inner__read,
  .person__links a,
  .contacts-item__email,
  .contacts-item__phone,
  .footer__item-title a,
  .footer__legal a,
  .footer__settings {
    display: inline-flex;
    align-items: center;
    min-height: var(--touch-target);
  }

  /* Stacked 44px links carry their own spacing; the row gap on top of it set
     them 64px apart on a phone. */
  .information__next {
    row-gap: 0;
  }

  /* The legal link padded itself to 24px and cancelled it with a negative
     margin; the row is tall enough now, and centres the copyright beside it. */
  .footer__legal {
    align-items: center;
  }

  .footer__legal a,
  .footer__settings {
    padding: 0;
    margin: 0;
  }

  .practice__item {
    padding-top: 0;
  }

  .practice__link,
  .article-toc__link,
  .article-share__link,
  .footer-list-link a {
    display: flex;
    align-items: center;
    min-height: var(--touch-target);
  }

  .footer-list-link + .footer-list-link {
    margin-top: 0;
  }

  /* A summary keeps display: list-item, or its disclosure marker goes. */
  .team-more summary {
    min-height: var(--touch-target);
    line-height: var(--touch-target);
  }

  /* Each icon a 44px square with the icon at its left edge, so the first
     still lines up with the text above it. Square boxes meet edge to edge,
     so the row needs no gap and no hit-area padding. */
  .footer-list-link--social {
    gap: 0;
  }

  .footer-list-link--social a {
    justify-content: flex-start;
    width: var(--touch-target);
    padding: 0;
    margin: 0;
  }
}
