/* ══════════════════════════════════════════════════════════════════════════
   Eugene Farber — Personal site
   Design: Brand | Vibe: Clinical Precision (warm-paper variant) | Layout:
   Ledger + Alternating Split | Color: Restrained, single blue accent

   Token architecture follows a two-layer model: primitives (raw values) feed
   semantic schemes (background/text/accent/border as a set). A section opts
   into a scheme by class, never by hardcoding a colour.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Layer 1: primitives ──────────────────────────────────────────────── */
:root {
  /* Warm paper — neutrals tinted toward hue 80, never pure grey */
  --paper-lightest: oklch(98.5% 0.005 80);
  --paper-lighter: oklch(96.5% 0.008 79);
  --paper-light:   oklch(94.5% 0.012 78);
  --paper:         oklch(90%   0.014 78);
  --paper-dark:    oklch(78%   0.016 78);

  /* Blue slate — structural darks, tinted, never #000 */
  --slate-darkest: oklch(15% 0.016 250);
  --slate-darker:  oklch(18% 0.018 250);
  --slate-dark:    oklch(21% 0.020 250);
  --slate:         oklch(34% 0.020 250);
  --slate-mid:     oklch(45% 0.018 250);
  --slate-light:   oklch(62% 0.016 250);
  --slate-lighter: oklch(74% 0.014 250);

  /* Blue — the single accent, and the only hue that does not come from the
     paper/slate system above. It is the logo blue: #087cf5, the fill of the
     wordmark's "Tech" and of the P in LogoMark.png, sampled at 23,600px of the
     mark against 59,234px of its #032048 navy. Held as a literal rather than
     converted to oklch because the whole point of the value is that it matches
     the artwork, and an oklch round-trip returns a colour that is close rather
     than the same. The step below it is a literal for a reason of its own, and
     it is the only other one. */
  --logo-blue: #087cf5;
  --logo-blue-dark: #0b62c4;

  /* The steps are not rungs of a ladder: each is the one job it has to do. The
     accent is ink for two kinds of thing and the two have different floors. As
     a rule, marker, caret or icon it is a graphic — 3:1 — and #087cf5 clears
     that at 3.87:1 on --paper-lightest. As the small uppercase label it is
     text — 4.5:1 — which #087cf5 misses at 3.87:1 there and at 3.43:1 on the
     sand, so the label ink is the same hue one step down: #0b62c4 at 5.66:1 on
     --paper-lightest and 5.03:1 on the sand, the darker of the two steps the
     wordmark's own comment names for it.

     The dark schemes want the opposite move, a blue light enough to read on
     slate. --logo-blue-light is 7.67:1 on --slate-dark and 8.50:1 on
     --slate-darkest, and it fills the primary button there.
     --logo-blue-lightest and --logo-blue-darkest are the form status chip's
     wash and its ink, at 11.9:1 — a pair, not a rung. */
  --logo-blue-light: oklch(74% 0.135 256);
  --logo-blue-lightest: oklch(95% 0.024 256);
  --logo-blue-darkest: oklch(30% 0.103 256);

  /* Type scale — 1.25 ratio throughout. Flat scales read as uncommitted. */
  --step--2: 0.75rem;
  --step--1: 0.875rem;
  --step-0:  1rem;
  --step-1:  1.25rem;
  --step-2:  1.5625rem;
  --step-3:  1.953rem;
  --step-4:  2.441rem;
  --step-5:  3.052rem;
  --step-6:  3.815rem;
  --step-7:  4.768rem;

  /* Spacing — several values, used deliberately for rhythm */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 2.5rem;
  --space-8: 3.5rem;
  --space-9: 4.5rem;
  --space-10: 5.5rem;
  --space-11: 6.5rem;
  --space-12: 8rem;

  /* Motion — one curve, exponential out. No bounce, no elastic, no linear. */
  --ease: cubic-bezier(0.32, 0.72, 0, 1);
  --dur-fast: 180ms;
  --dur: 320ms;
  --dur-slow: 720ms;

  --radius-sm: 0.375rem;
  --radius: 0.625rem;
  --radius-lg: 1rem;
  --radius-pill: 999px;

  --rule: 1px;
  --shell: 74rem;
  --shell-wide: 84rem;
}

/* ── Layer 2: semantic schemes ────────────────────────────────────────── */
.scheme-paper {
  --bg: var(--paper-lightest);
  --bg-raised: oklch(99.2% 0.004 80);
  --text: oklch(19% 0.016 250);
  --text-soft: var(--slate-mid);
  --text-faint: var(--slate-light);
  --accent: var(--logo-blue);
  --accent-text: var(--logo-blue-dark);
  --border: oklch(19% 0.016 250 / 0.14);
  --border-strong: oklch(19% 0.016 250 / 0.28);
  --shadow: 0 1px 2px oklch(19% 0.016 250 / 0.05),
            0 12px 32px -12px oklch(19% 0.016 250 / 0.14);
}

.scheme-sand {
  --bg: var(--paper-light);
  --bg-raised: oklch(97.5% 0.007 79);
  --text: oklch(19% 0.016 250);
  --text-soft: var(--slate-mid);
  --text-faint: var(--slate-light);
  --accent: var(--logo-blue);
  --accent-text: var(--logo-blue-dark);
  --border: oklch(19% 0.016 250 / 0.16);
  --border-strong: oklch(19% 0.016 250 / 0.30);
  --shadow: 0 1px 2px oklch(19% 0.016 250 / 0.06),
            0 14px 36px -14px oklch(19% 0.016 250 / 0.18);
}

.scheme-slate {
  --bg: var(--slate-dark);
  --bg-raised: var(--slate-darker);
  --text: oklch(96% 0.008 80);
  --text-soft: var(--slate-lighter);
  --text-faint: var(--slate-light);
  --accent: var(--logo-blue-light);
  --accent-text: var(--logo-blue-light);
  --border: oklch(96% 0.008 80 / 0.16);
  --border-strong: oklch(96% 0.008 80 / 0.30);
  --shadow: none;
}

.scheme-deep {
  --bg: var(--slate-darkest);
  --bg-raised: oklch(19% 0.018 250);
  --text: oklch(96% 0.008 80);
  --text-soft: var(--slate-lighter);
  --text-faint: var(--slate-light);
  --accent: var(--logo-blue-light);
  --accent-text: var(--logo-blue-light);
  --border: oklch(96% 0.008 80 / 0.14);
  --border-strong: oklch(96% 0.008 80 / 0.26);
  --shadow: none;
}

/* ── Reset ────────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--paper-lightest);
  color: oklch(19% 0.016 250);
  font-family: "Public Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: var(--step-0);
  line-height: 1.65;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4 {
  font-family: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-weight: 600;
  line-height: 1.12;
  letter-spacing: -0.021em;
  margin: 0;
  text-wrap: balance;
}

p { margin: 0; text-wrap: pretty; }
ul, ol, dl, dd { margin: 0; padding: 0; }
li { list-style: none; }

a { color: var(--accent-text, inherit); text-underline-offset: 0.18em; }
a:hover { text-decoration-thickness: 2px; }

/* A phone number is one token, not three. The booking page's fallback line is
   in the narrow column and broke it at the hyphen — "(216)" ending one line,
   "236-3987" starting the next — at which point it stops reading as a number
   at all. `nowrap` on the anchor rather than the digits, so the thing that must
   not split is the whole clickable target. Matched on the scheme, since every
   `tel:` link on the site wants this and none of them wants its own rule. */
a[href^="tel:"] { white-space: nowrap; }

img, svg { max-width: 100%; height: auto; display: block; }

:focus-visible {
  outline: 2px solid var(--logo-blue);
  outline-offset: 3px;
  border-radius: 2px;
}

/* ── Utilities ────────────────────────────────────────────────────────── */
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

.skip-link {
  position: absolute; left: var(--space-4); top: -100%;
  z-index: 100; padding: var(--space-3) var(--space-5);
  background: var(--slate-darkest); color: oklch(96% 0.008 80);
  border-radius: var(--radius); font-weight: 500; text-decoration: none;
  transition: top var(--dur-fast) var(--ease);
}
.skip-link:focus { top: var(--space-4); }

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: clamp(1.25rem, 5vw, 3rem);
}
.shell--wide { max-width: var(--shell-wide); }

.section {
  background: var(--bg);
  color: var(--text);
  padding-block: var(--section-pad, var(--space-10));
}
.section + .section { border-top: var(--rule) solid var(--border); }

/* Anchor targets must clear the 4.25rem sticky header, or a nav click drops
   the heading underneath it.

   These are the four section ids the homepage actually has. The service
   guides carry their own heading ids and set `scroll-margin-top` in
   `.service-content`.

   `#northeast-ohio` was on this list for one revision and is not: the section was
   removed rather than moved, so nothing carries that id. It came off for the same
   reason `#difference` and `#knowledge` are not here — a selector that matches
   nothing reads as "there is a section called Northeast Ohio" to anyone using the
   sheet to find out what the page is made of.

   `#difference` and `#knowledge` were on this list and are not: the Difference
   was removed from the hero rather than moved, so nothing carries that id, and
   the Knowledge hub is a separate page whose own `#top` is never linked to from
   here.

   `#services` and `#experience` were on this list and are not: the
   eleven-service ledger was removed rather than moved, so nothing carries
   `#services`, and the experience record now lives on about.html. */
:where(#top, #engagements, #service-guides, #contact) {
  scroll-margin-top: 5.75rem;
}

.eyebrow {
  font-size: var(--step--2);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent-text);
  margin-bottom: var(--space-5);
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.eyebrow::before {
  content: "";
  width: 1.75rem; height: 1px;
  background: currentColor; opacity: 0.5;
  flex: none;
}

/* The hero byline sets a mixed-case name over an uppercase role, so it opts out
   of the small-caps treatment `.eyebrow` applies to every other use of the class
   ("Work with me", "Free working session"). Only the role keeps it — the name is
   the part that would otherwise be shouting. The line between the two used to be
   the Microsoft credential, set like the name rather than like the role; it came
   out when the role arrived, and the claim it made is still on the page in the
   hero's credential block under the lede.

   The two lines are wrapped together in `.eyebrow__lines`, and that wrapper is
   required rather than tidy: `.eyebrow` is a flex row, so spans left as its own
   children would sit side by side and take the 24px `gap` with them, and the
   `::before` rule that reads as the dash before the name would land beside the
   first line rather than before the block. One wrapper makes the rule and the
   text the only two items in the row, which is what the rule was written for.

   No `gap` between the lines. Each is its own grid row and carries `line-height`
   on its own, so they stack at the leading the rest of the eyebrow already uses;
   a gap here would be distance added on top of leading, which is how a two-line
   caption starts to read as two separate labels.

   The rule is re-aligned to the first line, and that is the whole reason for the
   two overrides below. `.eyebrow` centres its `::before` against the text block,
   which is right for a one-line eyebrow and wrong for this one: against two lines
   the rule lands 9px below the name's centre, sitting in the gap between them and
   reading as a dash between two labels rather than as the dash in front of
   "Eugene Farber". `flex-start` puts the rule at the top of the block and the
   margin re-centres it on the first line box, which measures 1.25rem. That figure
   is the line box and not a chosen number — it is `line-height` applied to
   `--step--2` — so if the eyebrow's leading ever changes, this is the value that
   follows it. */
.eyebrow--byline { text-transform: none; align-items: flex-start; }
.eyebrow--byline::before { margin-top: calc((1.25rem - 1px) / 2); }
.eyebrow__lines { display: grid; }
.eyebrow__role { text-transform: uppercase; }

.section__head { max-width: 46rem; margin-bottom: var(--space-9); }
.section__title { font-size: clamp(1.953rem, 1.4rem + 2.2vw, 3.052rem); }
.section__sub {
  margin-top: var(--space-5);
  font-size: var(--step-1);
  color: var(--text-soft);
  max-width: 40rem;
  line-height: 1.55;
}

/* ── Buttons ──────────────────────────────────────────────────────────── */
.btn {
  --btn-bg: transparent;
  --btn-fg: var(--text);
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-3) var(--space-3) var(--space-6);
  font-family: inherit;
  font-size: var(--step-0);
  font-weight: 500;
  line-height: 1;
  background: var(--btn-bg);
  color: var(--btn-fg);
  border: var(--rule) solid transparent;
  border-radius: var(--radius-pill);
  cursor: pointer;
  text-decoration: none;
  transition: background-color var(--dur) var(--ease),
              color var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              transform var(--dur-fast) var(--ease);
}
.btn:active { transform: scale(0.985); }
.btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
  transform: none;
}
.btn:disabled:hover .btn__icon { transform: none; }

.btn--primary {
  --btn-bg: var(--slate-darkest);
  --btn-fg: oklch(97% 0.008 80);
}
.scheme-slate .btn--primary,
.scheme-deep .btn--primary {
  --btn-bg: var(--logo-blue-light);
  --btn-fg: var(--slate-darkest);
}
.btn--primary:hover { --btn-bg: var(--slate-dark); }
.scheme-slate .btn--primary:hover,
.scheme-deep .btn--primary:hover { --btn-bg: oklch(80% 0.102 256); }

.btn--ghost {
  --btn-fg: var(--text);
  border-color: var(--border-strong);
  padding-inline: var(--space-6);
}
.btn--ghost:hover { border-color: var(--text); }

.btn--sm { font-size: var(--step--1); padding: var(--space-2) var(--space-2) var(--space-2) var(--space-5); }
/* `.btn--sm`'s padding is asymmetric on purpose — 1.5rem on the leading edge,
   0.5rem on the trailing one — because it is built to seat a `.btn__icon`
   against that short edge. A ghost button carries no icon, so the short side
   has nothing in it and the label sits visibly off centre in the pill. This
   gives the pairing symmetric padding. It wins on specificity rather than on
   source order, so it does not matter that `.btn--sm` is declared below. */
.btn--ghost.btn--sm { padding-inline: var(--space-4); }

/* Button-in-button: trailing icon nests in its own wrapper, flush with the
   button's inner padding — never sitting naked beside the label. Applied to
   primary CTAs only, so it stays a signal rather than wallpaper. */
.btn__icon {
  display: grid;
  place-items: center;
  width: 1.9em; height: 1.9em;
  flex: none;
  border-radius: 50%;
  background: color-mix(in oklab, currentColor 14%, transparent);
  transition: transform var(--dur) var(--ease);
}
.btn__icon svg { width: 0.95em; height: 0.95em; }
.btn:hover .btn__icon { transform: translateX(2px) translateY(-2px); }

/* ── Header ───────────────────────────────────────────────────────────── */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  /* 0.92, not 0.82: the header is sticky and passes over the dark schemes,
     where a lighter alpha composited to a cool grey band across an otherwise
     warm-paper page. The blur still supplies the depth cue. */
  background: oklch(98.5% 0.005 80 / 0.92);
  /* backdrop-blur lives ONLY here — a fixed element. Never on scrolling
     containers, where it forces continuous GPU repaint on mobile. */
  backdrop-filter: saturate(150%) blur(12px);
  border-bottom: var(--rule) solid oklch(19% 0.016 250 / 0.10);
}
@supports not (backdrop-filter: blur(12px)) {
  .site-header { background: var(--paper-lightest); }
}

.site-header.is-scrolled {
  box-shadow: 0 1px 0 oklch(19% 0.016 250 / 0.06),
              0 8px 24px -16px oklch(19% 0.016 250 / 0.35);
}

/* The row gap is 16px, down from 32px, and that is what pays for the logo.

   The row is at its tightest at 1241px — the 77.5rem breakpoint is where the
   nav and both buttons appear together, and one pixel above it is the fullest
   the bar ever gets. There the row wants 1118.1px of a 1130px content box:
   11.9px of slack. It was 1018.4px of 1025px and 6.6px of slack before the
   Services disclosure added a third nav item; that is the 104px this row has
   spent twice now, once on the monogram and once on the nav.

   The trade below is exactly break-even, which is why it was worth making. The
   monogram costs the brand 39.3px — 75.3px of mark plus an 8px gap, against
   the 32px square and 12px gap it replaces — and the spacing changes return
   40px: 48 from the three row gaps, 8 more from the nav's own, less the 16px
   the button pair's negative margin gives back. The row ends 0.7px shorter
   than it started.

   Reducing this gap reads as a bigger change than it is. `.header__nav` is
   `margin-left: auto`, so it absorbs every spare pixel, and the brand-to-nav
   distance is the gap *plus* that margin — shrink the gap and the margin grows
   by the same amount, leaving the brand and the nav exactly where they were.
   The gap only becomes real when the row is full, which is the case this
   change is for. The one visible effect is the nav sitting 16px from the first
   button instead of 32px, measured at 1440 and 1121 and accepted. */
.header__inner {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  min-height: 4.25rem;
}

/* The gap between the monogram and the lockup is 8px, down from 12px. The
   monogram is a wide mark rather than a compact square, so it needs less air
   beside the text than the square did, and the row needed the 4px. */
.header__brand {
  display: flex; align-items: center; gap: var(--space-2);
  text-decoration: none; color: inherit;
  font-family: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-weight: 600;
  letter-spacing: -0.015em;
  flex: none;
}
/* The ETP monogram. It was a 2rem square of `--slate-darkest` with "EF" set in
   it; the supplied logo replaces that, and it is not square — the monogram is
   678x252, an aspect of 2.69, so its width follows from the height rather than
   both being fixed.

   1.75rem (28px) is where it stops fitting. The brand's width is set by the
   descriptor beside it, not by this element, so the monogram is the thing the
   row can still afford to buy back: at 30px the row overran the shell's right
   padding at the old 1121px breakpoint, and at 28px it cleared with 7px to
   spare. The three gaps reduced alongside this are what pay for it — see
   `.header__inner`.

   The 7px is history now: the Services disclosure took the breakpoint up to
   77.5rem and the row back to 11.9px of slack there. 30px would cost 5.4px of
   that (the mark's width scales with its height), leaving 6.5px — arithmetic
   on the measured row, but not measured itself. Left at 28px regardless: the
   mark is keyed to the lockup's 31.8px, and growing it back spends headroom
   the next nav item will want.

   `flex: none` matters here more than it did on the square. A replaced element
   with an intrinsic ratio and `width: auto` is shrinkable, and in an overfull
   row the browser would happily squash it out of proportion — the one outcome
   this element exists to avoid. */
.header__mark {
  display: block;
  flex: none;
  height: 1.75rem;
  width: auto;
}
.header__name { font-size: var(--step-0); }

/* "Tech" wears the monogram's own blue, so the banner's wordmark and the mark
   beside it are drawing from one palette. The markup splits the word into
   `Expert<span>Tech</span>Pilot` with no whitespace between the parts, which is
   what keeps it one word: a space or a newline there and a screen reader
   announces "Expert Tech Pilot".

   Measured, this is the one thing on the page that misses WCAG AA. #087cf5
   against --paper-lightest is 3.87:1, and at --step-0 (16px) weight 600 the
   text is not large enough to qualify for the 3:1 large-text floor, so the bar
   is 4.5:1. The header's own 0.92 alpha means #fcfaf6 is the worst case, not
   the average — over a dark section the composited background only darkens and
   the ratio rises. #0a6ad4 is the nearest value on the same hue that clears the
   floor at 5.00:1, and #0b62c4 clears it at 5.66:1. The wordmark keeps the
   literal anyway, because it is the logo; every other place the page sets this
   blue as text takes --logo-blue-dark, which is #0b62c4. */
.header__name-accent { color: var(--logo-blue); }

/* The name and its descriptor are one lockup, not two items in a row, so they
   stack — and the 1px gap is what makes them read as a single mark rather than
   as two lines that happen to sit near each other.

   1.1 is a shape decision here, not a layout one, and this comment used to
   claim otherwise. It said that at the body's 1.5 the two lines would come to
   ~41px and the row would grow to carry them, moving every anchor the sticky
   header clears. Measured, it does not: the bar is `min-height: 4.25rem`
   (68px) and renders 69px at line-heights 1.1, 1.2, 1.5 and 2 alike, with
   `#engagements` landing at the same document position in all four. There was
   no height to defend.

   What 1.1 does is keep the lockup at 31.8px, near the monogram's 28px, so the
   mark and the two lines read as one lockup. At 1.5 the text is 43px and
   towers over the mark it sits beside. */
.header__lockup {
  display: grid;
  gap: 1px;
  line-height: 1.1;
}

/* The descriptor carries positive tracking where the name carries negative
   (`.header__brand` sets -0.015em, which is a wordmark's tightening). Uppercase
   at --step--2 keeps it a caption: the failure mode is two lines of the same
   weight reading as one sentence broken in the middle, and the case and size
   change is what prevents it. `--slate-mid` is the soft-text token, so this
   follows the scheme rather than naming a grey of its own. */
.header__tagline {
  font-size: var(--step--2);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--slate-mid);
}

.header__nav { margin-left: auto; }
/* 24px between nav items, down from 32px, for the same reason as the row gap
   above: the logo needs the width and this is the cheapest place left to find
   it. Two nav links are further apart than they need to be at 32px, and the
   visible cost here is a nav that reads slightly tighter than before. It is
   still 24px with a third item, and the third item is what took the breakpoint
   to 77.5rem — the gap was not reopened to pay for it. */
.header__list { display: flex; gap: var(--space-5); }
/* `.header__summary` is in this selector rather than given a rule of its own,
   so the Services trigger cannot drift from the two links beside it. It is a
   `<summary>` and not an `<a>` only because it opens a panel instead of
   navigating; it should be indistinguishable from its neighbours until it is
   opened, and sharing the rule is how that stays true. */
/* `display: flex` is here, not on `.header__summary`, and that placement is the
   whole fix for a 6px misalignment. As an inline box the `<a>` sits at the top
   of a stretched row and its text lands 6px higher than the `<summary>`'s,
   because the summary is a flex container that *centres* its content in the same
   row. Two different vertical models, two different text positions. Making both
   block-level flex containers gives them one model and one baseline. */
.header__list a,
.header__summary {
  display: flex;
  align-items: center;
  color: var(--slate-mid);
  text-decoration: none;
  font-size: var(--step--1);
  font-weight: 500;
  padding-block: var(--space-2);
  border-bottom: 2px solid transparent;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.header__list a:hover,
.header__summary:hover { color: var(--slate-darkest); border-bottom-color: var(--logo-blue); }

/* ── Services dropdown ────────────────────────────────────────────────────
   The one nav item that is a disclosure rather than a destination. See the
   markup comment for why it is a `<details>` and why it carries no
   `aria-expanded`.

   The marker has to be suppressed twice: `list-style` covers the standard
   `::marker`, and the `-webkit-` pseudo is the only thing that removes the
   triangle in Chrome and old Safari on a `summary` that is displayed as a flex
   container. `display: flex` alone drops it in current Chrome, but the file
   supports the two declarations rather than the one that happens to work. */
.header__menu { position: relative; }
/* `display: flex` and `align-items: center` now come from the shared rule above,
   so they are not restated here — that rule is the one place the nav's box model
   is defined, and a second copy is how the two would drift apart again. */
.header__summary {
  gap: var(--space-2);
  list-style: none;
  cursor: pointer;
  white-space: nowrap;
}
.header__summary::-webkit-details-marker { display: none; }
.header__caret {
  display: flex;
  flex: none;
  color: currentColor;
  transition: transform var(--dur) var(--ease);
}
.header__caret svg { width: 0.85em; height: 0.85em; display: block; }
.header__menu details[open] .header__caret { transform: rotate(180deg); }

/* `scheme-paper` is on this element in the markup, and the tokens below are
   the reason: --bg-raised, --border and --shadow are declared per scheme and
   resolve to nothing outside a `.scheme-*` ancestor. The header has no scheme
   class. `.chat` carries one for the same reason.

   `left: 0` aligns the panel to the trigger, and the widths check out at both
   ends of the range: the trigger sits at about 814px in a 1344px content box
   at 1440 and about 590px of 1120 at the 76rem breakpoint, so an 18rem panel
   clears the right edge either way. Measured, not assumed — if the nav grows
   again, this is the declaration that breaks first.

   The panel is absolutely positioned and therefore out of flow, so it overlays
   the page rather than pushing the bar taller. Nothing needs a z-index above
   the page's own content: `.site-header` is `position: sticky` with `z-index:
   50` and already establishes the stacking context the panel paints inside. */
.header__menu-panel {
  position: absolute;
  top: calc(100% + var(--space-3));
  left: 0;
  z-index: 1;
  min-width: 18rem;
  max-height: min(70vh, 32rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-2);
  background: var(--bg-raised);
  border: var(--rule) solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.header__menu-panel a {
  display: block;
  padding: var(--space-3);
  color: var(--text-soft);
  text-decoration: none;
  font-size: var(--step--1);
  line-height: 1.35;
  border-radius: var(--radius-sm);
  transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.header__menu-panel a:hover {
  color: var(--text);
  background: color-mix(in oklab, var(--accent) 10%, transparent);
}

/* Both disclosures open a left-aligned panel under the trigger. Case Studies
   sits far enough from the shell's right edge that the panel clears it at every
   width the desktop nav is shown: 288px wide, ending at 1018px at 1440, 1083 at
   1280 and 1044 at 1241. */

/* The fourth nav item (Case Studies) took the last of the row's slack. Between
   the nav breakpoint and 86rem the secondary "Start a conversation" button
   steps out, which is the cheapest way to keep every nav destination visible
   on a 13-inch laptop; both buttons return once the row has room. Below
   77.5rem the nav's own rule hides both buttons anyway. */
@media (max-width: 86rem) {
  .header__cta + .header__cta { display: none; }
}

.header__cta { flex: none; }

/* Two buttons in the bar now, and `.header__inner`'s gap applies between every
   child alike — the same gap that separates the brand from the nav. Two buttons
   that are one decision apart want less than that, so this pulls the pair back
   to the 12px the gap is elsewhere in this file.

   The subtraction is written against the row gap rather than as a flat -4px,
   so the pair stays 12px apart if `.header__inner`'s gap moves again. It moved
   once already, from 32px to 16px, when the logo needed the width.

   Written as an adjacent-sibling rule rather than by wrapping the two in a
   container, and that is the point: both stay children of the flex row, so the
   `.header__cta` hiding at the 70rem breakpoint goes on covering both without
   being told a second one exists. */
.header__cta + .header__cta { margin-left: calc(var(--space-3) - var(--space-4)); }

.header__toggle {
  display: none;
  margin-left: auto;
  background: none; border: 0; cursor: pointer;
  padding: var(--space-3); color: var(--slate-darkest);
}
.header__bars { display: grid; gap: 5px; width: 22px; }
.header__bars i {
  display: block; height: 2px; background: currentColor; border-radius: 2px;
  transition: transform var(--dur) var(--ease), opacity var(--dur) var(--ease);
}
.header__toggle[aria-expanded="true"] .header__bars i:first-child { transform: translateY(3.5px) rotate(45deg); }
.header__toggle[aria-expanded="true"] .header__bars i:last-child  { transform: translateY(-3.5px) rotate(-45deg); }

.mobile-nav {
  position: sticky; top: 4.25rem; z-index: 45;
  background: var(--paper-lightest);
  border-bottom: var(--rule) solid oklch(19% 0.016 250 / 0.12);
  padding: var(--space-4) clamp(1.25rem, 5vw, 3rem) var(--space-6);
}
.mobile-nav__list { display: grid; gap: var(--space-1); }
/* The Services row is a `<summary>`, so it joins the shared link rule rather
   than restating it — same reason as `.header__summary` above. */
.mobile-nav__list a,
.mobile-nav__summary {
  display: block;
  padding: var(--space-4) 0;
  color: var(--slate-dark);
  text-decoration: none;
  font-family: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: var(--step-1);
  font-weight: 500;
  border-bottom: var(--rule) solid oklch(19% 0.016 250 / 0.08);
}
.mobile-nav__list a:hover,
.mobile-nav__summary:hover { color: var(--logo-blue-dark); }

.mobile-nav__summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  list-style: none;
  cursor: pointer;
}
.mobile-nav__summary::-webkit-details-marker { display: none; }
.mobile-nav__caret {
  display: flex;
  flex: none;
  color: var(--logo-blue-dark);
  transition: transform var(--dur) var(--ease);
}
.mobile-nav__caret svg { width: 0.85em; height: 0.85em; display: block; }
.mobile-nav__group details[open] .mobile-nav__caret { transform: rotate(180deg); }

/* The five guide links keep the row rhythm of the items above them but drop
   to body size and take an indent, so the group reads as detail underneath
   "Services" rather than as five more sections. The last one carries no
   border of its own: the group's own rule closed the block already, and a
   second hairline directly under the first reads as a gap in the menu. */
.mobile-nav__sublist {
  display: grid;
  gap: var(--space-1);
  padding-left: var(--space-4);
}
.mobile-nav__sublist a {
  font-size: var(--step-0);
  padding: var(--space-3) 0;
}
.mobile-nav__sublist li:last-child a { border-bottom: 0; }

/* ── Hero ─────────────────────────────────────────────────────────────── */
.hero { padding-block: clamp(1rem, 2vw, 1.75rem) clamp(3rem, 6vw, 5.5rem); }

/* Two columns, and every child of the hero is now a grid item of this element
   rather than a descendant of a copy wrapper inside it. The wrapper is gone so
   that the lede and the credentials can span both tracks; a wrapper is
   a single grid item and nothing inside it can reach a column it does not occupy.

   5fr / 3fr — 62.5% and 37.5% of the 1008px left after the 80px gap, so 630 and
   378 at 1440. The headline takes the first track and the photograph the second,
   on one line; the intro above them, the tagline below them, and the lede and
   credentials below that span both. The boundary is stated as a share
   rather than a pixel width because that is how the ask was made.

   **The ratio is not what it was, and the headline is why.** The split and the
   type size are one decision, not two, because between them they decide how the
   headline breaks, and the headline is the only thing here that cannot reflow to
   suit either. The history of that, in order:

   It was 3:1 at 756px while the headline was 91 characters, and that headline
   needed 806px and a smaller ceiling to set to three even lines — 787 / 738 / 731
   for a 56px spread. The 47-character headline that replaced it took two lines at
   806px, 668 and 490, so the extra width stopped earning its keep; it stayed
   because the lede was measured against it and the second track was still empty.
   **That 47-character string is now `.hero__subhead`**: the heading restructure
   moved it below the h1 and gave the top slot to the service-and-region line, so
   neither its break nor its orphan is this column's problem any more.

   Then the photograph arrived in that track and the split was widened, first to
   3:2 and then to 5:3 on request, which is where it stands: 630px of text and 378
   of photograph at 1440.

   **The ratio costs nothing in height, and it does not decide the headline's
   break either — both were measured rather than reasoned.** The box beside the
   copy is as tall as the two rows it spans, so the hero measures 710 at every
   ratio tried: 5:3, 3:2, 2:1, 5:2, 7:3 and 4:1 all give the same 710, and the
   headline measures 2L/108 at all six of them. At 630px it sets to two lines, 547
   and 597, and it sets to the same two in a 605px track. No string this page has
   carried as the h1 sets to one line at any of these widths, and the one before
   it could not even hold two: "20 Years Building Microsoft Enterprise Software"
   set to 424 / 234 / 490 — "Microsoft" alone in the middle — because "20 Years
   Building Microsoft" wants 668. That string is the tagline now, out of this
   column, and the orphan left with it.

   What the ratio does cost is the caption painted into the photograph, and that
   is the trade to make knowingly rather than for the ratio's own sake. The
   caption's headline line is about 48px in a 1248-wide file, so it renders at the
   track over 1248: 15.5px at 3:2's 403px, 14.5px at the current 378, 12.9px at
   2:1's 336, and 7.8px at 4:1's 202, where it is unreadable. Every figure above
   was read off the render — the ratio set in the page, the box and the headline's
   breaks measured — not estimated from character counts; redo it the same way if
   either string changes.

   **The fold, which is what the restructure was judged on.** The hero is 710px
   at 1440 and the CTA bottom is at 693 — 257px clear of a 950 fold, and the whole
   section fits the frame in any viewport 780px or taller. Three changes brought
   it here, all three made on request and none of them a type decision:

     * the section's top padding, from `clamp(3.5rem, 7vw, 7rem)` — 100.8px at
       1440 — to `clamp(1rem, 2vw, 1.75rem)`, worth 73px. See `.hero` below.
     * the h1's ceiling, 3.6rem to 3.24rem, which cost a line as well as height:
       the headline is two lines now, so the row it sits in is 108px against 180.
       See `.hero__title`.
     * the photograph's track, in from 2/5 of the row to 3/8 of it, which does not
       touch the height at all — see the ratio paragraph above.

   The tagline still costs what it always did: hiding `.hero__subhead` drops the
   hero to 663 and the CTA to 646, so the row is 47px. That is the figure this
   paragraph was written around, and it is the one number here that has not moved.

   Nothing is over a bound any more, and the lever this paragraph used to hold in
   reserve went with the padding it acted on: `.hero` no longer declares
   `--section-pad`, so there is no outstanding debt against the fold to record.

   What the ratio does decide is the second track's width, and that is the whole
   of the trade. Measured at 1440, with the caption's rendered size taken as the
   file's ~48px headline line scaled by the track:

     3:2  track 403  caption 15.5px
     5:3  track 378  caption 14.5px   (current, and 3:2 until this revision)
     2:1  track 336  caption 12.9px
     5:2  track 288  caption 11.1px
     4:1  track 202  caption  7.8px

   The floor is the caption's, not the crop's: below about 300px of track its small
   line stops being readable, which rules 4:1 and 5:2 out on their own and leaves
   2:1 marginal. So the choice is between 3:2, which gives the biggest caption and
   the narrowest headline column at 605px, and the current 5:3, which takes 25px
   off the picture and hands it to the headline. Whoever changes it again should
   change it knowing which of the two they are spending, not for the ratio's own
   sake — and knowing that the hero's height will not move by a pixel either way.

   The lede is the other measurement tied to this rule and it is now the page's
   largest departure from its own 45–75 character rule: spanning, it is 1088px at
   1184 and up and sets 114 characters to the line. That is a consequence of
   spanning rather than of the ratio — no ratio moves it — and `column-count: 2`
   is the fix that fits, measured at 57 characters and 16px *shorter*. See
   `.hero__lede`.

   `column-gap` and `align-items` are `.contact__grid`'s, so the hero's columns
   stand the same distance apart as the other side-by-side pair on the page. The
   gap is the column gap only; `row-gap` is zero because the rows are spaced by
   their children's margins. `start` and not `stretch`, so the headline and the
   intro sit on the row's top line — the photograph is the exception and stretches
   deliberately, one row span down.

   The element has been a grid since the six service tiles, and a two-column grid
   twice before — once for those tiles, once for the Difference diagram — so this
   is the third time it has been given a track it did not have. The div is still
   here because it is the hero's only wrapper and every measurement note in this
   file names `.hero__grid`. */
.hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 3fr);
  /* Column gap only. The rows below the first two are spaced by their children's
     own `margin-top` — `--space-4` above the tagline, `--space-6` above the lede
     and the credentials, `--space-7` above the note — and a row gap would add to
     those rather than replace them, because margins between grid items do not
     collapse the way margins between blocks do. The last three are what spaced
     this hero when all of it was one wrapper's children; the tagline's margin
     arrived with the element. Setting this to zero is the whole reason the
     vertical rhythm survived the wrapper coming out. */
  column-gap: clamp(2.5rem, 6vw, 5rem);
  row-gap: 0;
  align-items: start;
}

/* Who goes where.

   The headline and the photograph take one track each, which is what puts them on
   one line; the intro above them, the tagline below them, and the lede and
   credentials below that span both, so they run the full width of the hero
   instead of stopping at the headline's column.

   **Every row is stated, and that is not decoration.** Auto-placement alone puts
   the photograph in rows 1-2 correctly and then pushes the intro and the headline
   out of the way to get there: an item with a definite row is placed before the
   auto-placed ones, so by the time the intro is looked at, row 1 is already half
   occupied and the intro — which spans both columns — cannot fit until row 3.
   Measured, that costs 180px of hero height and puts the eyebrow and the portrait
   underneath the picture. Stating the rows takes the auto-placement algorithm out
   of the decision, which is the point: the six rows here are a layout, not a
   consequence.

   **Re-measured after the tagline was added, and hero height is the wrong
   instrument for this.** Dropping `grid-row` from every item but the photograph
   leaves the hero at 710px either way, which reads as "the rows cost nothing
   now" and is not what happens: `.hero__image-wrap` collapses from 170-445 to
   170-170, because the wrapper holds no intrinsic height of its own and `1 / 3`
   is the only thing giving it two rows to stretch across. The photograph does not
   disappear — it is absolutely positioned and still paints — but the box that
   sizes it does. Anyone re-testing this should measure the wrapper, not the hero.

   `1 / -1` and not `1 / 3`. The two are the same grid today, but `-1` is the last
   line whatever the column count is, so the spanning items survive a ratio change
   or a third track being added without being restated. */
.hero__intro { grid-column: 1 / -1; grid-row: 1; }
.hero__title { grid-column: 1; grid-row: 2; }
.hero__subhead { grid-column: 1 / -1; grid-row: 3; }

/* The photograph's box, which is the thing the grid sizes — not the picture.

   It spans the intro's row and the headline's row, so its height is the portrait
   and the headline together and its bottom edge lands on the headline's last
   line. Sized to its own row instead, the photograph is taller than the headline
   beside it and the difference shows as blank space in the text column under the
   headline, which is what this is here to remove.

   The picture is taken out of flow inside it for one reason: a grid item's
   intrinsic height is a floor on the rows it covers, so an in-flow `<img>` sized
   to this box would hold the box open at its own natural 269px and the blank
   space would come back the moment the headline set to two lines instead of
   three. Positioned absolutely, it contributes no height, the rows are decided by
   the intro and the headline alone, and `align-self: stretch` hands the wrapper
   the total. The wrapper has no height of its own to fall back on — that is the
   same fact from the other side, and it is why the stacked layout below has to
   put the picture back into the flow. */
.hero__image-wrap {
  grid-column: 2;
  grid-row: 1 / 3;
  align-self: stretch;
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-lg);
}

.hero__lede { grid-column: 1 / -1; grid-row: 4; }
.hero__creds { grid-column: 1 / -1; grid-row: 5; }

/* The photograph itself, filling the box above and cropping to it.

   **Everything below describes the composed landscape asset, and the file in this
   folder is not it.** On disk `HeroImage2.jpg` is 822x1024 — a portrait with no
   caption anywhere in it — while the markup declares `width="1248" height="832"`
   and the live site serves a landscape JPEG under the same name. Below the stack
   breakpoint that difference is 467px of hero height; above it, it is the
   difference between the crop this comment describes and a crop of the other
   axis. The reasoning is kept as it stands because it is what the design is and
   what has to be met when the right file is back in place, and because the
   caption's floor is what fixes the second track's minimum width. See the
   HeroImage2.jpg entry in VERSION. It is not a description of what renders today.

   `object-fit: cover` and not `contain`. The wrapper's height is the left
   column's, which is not the file's ratio, so something has to give: `contain`
   letterboxes and puts the blank space back as bands above and below the picture,
   which is the same defect in another place. `cover` crops instead, and which way
   it crops is the two ratios' doing rather than a choice. The landscape asset is
   1.5 against a box that runs 1.85 at 1440 and 1.18 at 897: at the wide end the
   box is the wider of the two and 48px comes off the height, and at the narrow end
   the picture is the wider and the width is what goes — 82px of it at 897, which
   is what the next paragraph measures. The portrait file is 0.8, narrower than the
   box at every width in this layout, so against it the crop is vertical
   everywhere: 267px off the height at 1440 and 112 at 897, and `object-position`
   has no horizontal overflow left to act on.

   **`object-position: left` is doing real work against the landscape asset and
   nothing at all against the file that is here.** There the crop is a slice of
   width, and the default centre takes it off both sides — which eats the caption,
   because the caption is painted in near the left edge. The words start 38px into
   the file, 3% of its width, so any crop wider than about 6% of the rendered width
   takes letters with it. Measured, centre cuts "On-Site Microsoft Engineer" to
   "ite Microsoft Engineer" at 897px, where the track was 301 and the picture drew
   384 and 82px came off. Anchored left the whole 82px comes off the right instead,
   the caption keeps its 12px margin, and what is lost is the right edge of the
   photograph — a window and part of one person's shoulder, which the composition
   survives. Against the portrait file the crop is entirely vertical, so the rule
   is inert rather than wrong, and it is worth keeping for the day the landscape
   asset is restored.

   The 38px is read off the file rather than estimated: drawing the JPEG to a
   canvas and taking the bounding box of the pale blue lettering gives it, gating
   on blue-minus-red because the window behind it is bright but neutral. The crop
   at each width comes from shooting the hero at 1440 and at 897 for both
   object-positions and comparing the two. Neither is a script in this project —
   they were one-off measurements, and the numbers are recorded here so the next
   revision does not have to repeat them.

   The crop changes direction as the viewport narrows, because the text column
   above it does not: the box is 204px tall — the byline's row and the headline's —
   at 1440 and 240px at 897, while the track beside it goes from 378 to 283. The
   box's ratio therefore runs 1.85 at the wide end to 1.18 at the narrow end, and
   it is the distance between that and the file's own ratio that gets cut. 897 is
   one pixel above the stack breakpoint, which makes it the worst case in the
   two-column layout.

   The caption is the thing to watch if the geometry moves, once the asset that has
   one is back. It is painted into the file rather than set in type, so it scales
   with the rendered width and cannot be re-set from CSS: at 378px the file's 1248
   are at 30%, and the headline line of the caption — about 48px in the file —
   renders near 14.5px. Below about 300px of track the small line stops being
   readable, and that is the real floor on how far the second track can be squeezed
   — not the crop. */
.hero__image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: left center;
}

/* The role line and the portrait that reads as its byline. The wrapper exists
   only so the two share a row; the eyebrow keeps its own margin-bottom, which
   now measures from the bottom of whichever child is taller — at 6rem the face
   is much the taller of the two, so the role line is centred against it. */
.hero__intro {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

/* Square already, so `object-fit: cover` has nothing to crop and no
   `object-position` is needed — the file is cropped on the face. It has to be
   done in the file rather than here: covering a square with a portrait fits the
   width exactly, so object-position has no horizontal axis to move on, and this
   one's face sat 195px left of the disc's centre. `object-fit` stays as the
   guard for the day someone drops an uncropped portrait back in.

   The ring is the black trim the portrait already carries, cut as a border here
   instead of cropped in from the file. The file cannot supply it evenly: its
   badge sits 195px right of the face, so the black it holds is a crescent down
   one side, and a ring that is uniform has to be concentric with the badge —
   which is the framing that puts the face 15% off centre. Both cannot hold, and
   the face won. A border is uniform by construction, so this is the same trim
   drawn where the file could not.

   A literal, not a token, and the only border in this file that is. Every other
   rule here reads `--border` so it can follow the scheme; this one is not a
   surface, it is part of the photograph, and the photograph is rgb(0,0,0) — read
   off the source, which is also what makes it match the badge in the file rather
   than merely resemble it. The hero is `.scheme-paper` only, so there is no dark
   scheme for a fixed black to go missing on.

   Border-box, so the ring comes out of the 6rem rather than adding to it: the
   photo's own circle is 88px across, and the crop's black budget was computed
   against that figure. */
.hero__avatar {
  flex: none;
  width: 6rem;
  height: 6rem;
  border-radius: var(--radius-pill);
  object-fit: cover;
  border: 4px solid #000;
}

/* Capped below --step-7. The ceiling has moved four times: 4.2rem to 3.6rem
   when the headline was rewritten to a third of its length, 3.6rem to 3.2rem when
   the lede grew to three paragraphs, back to 3.6rem when the lede lost a line
   again, and 3.6rem to 3.24rem — 10% off, on request — in the revision that also
   added the lede's closing line. See `.hero__grid` above for why the size and the
   column are one decision.

   The round trip is worth recording, because it is the only reason the ceiling
   ever moved the second time: 3.2rem was never a type decision, it was arithmetic
   against the fold. The three-paragraph lede put the CTA bottom at 941 against a
   950 fold — nine pixels — and 3.6rem would have pushed it under. When the copy
   was revised and the lede came back to twelve lines, the same measurement gave
   922 at 3.6rem, twenty-eight clear, so the constraint that forced the shrink was
   gone and the shrink went with it.

   **The fourth move was not arithmetic.** 3.24rem is 10% off 3.6rem, asked for by
   eye rather than by a measurement, and it took a line out with it: the headline
   is two lines where it was three, so the hero lost 72px to it — 6 from the type
   and the rest from the row. Nothing in that follows from the fold, which had
   257px of room to spare; it is a reading decision, and it is recorded as one.

   No `max-width`. The measure is the column, so the headline sets to the width
   of the first track and breaks where its words run out. **That column is 630px**,
   from 605 until the photograph's track came in from 2/5 of the row to 3/8 of it,
   and the break it gives is two lines — 547 and 597, "Northeast Ohio Software" and
   "Consulting & Development". At the 3.6rem this rule carried before the last
   change the same string took three at 605px, 376 / 502 / 383, with no orphan
   among them either; the ceiling and the column moved together to take the third
   line out, and either one alone would have done it.

   The string this paragraph used to describe here, "20 Years Building Microsoft
   Enterprise Software", set to 424 / 234 / 490 — "Microsoft" alone in the middle —
   because "20 Years Building Microsoft" wants 668 and 605 will not hold it; at
   the 806px the column used to be, that same line set to two, 668 and 490. **That
   string is `.hero__subhead` now**, out of this column, so its orphan is no longer
   a property of this rule at any width. The trade and why it was taken are in
   `.hero__grid` above; the short version is that the headline's column and the
   picture's track come out of the same 1008, so widening one narrows the other —
   25px each way in the move that brought the ratio to 5:3 — and the picture's
   caption is painted in and cannot be re-set.

   The two lines are 50px apart, which is tighter than the 56px the 91-character
   string managed at 806px, the 126px the three-line break spread at 3.6rem, and
   the 178px the two-line version before it had. What it is not is a staircase: no
   line is a single word, at any width from 1440 down to 390, so the orphan this
   rule used to be written around is gone with the string that caused it.

   The clamp reaches the ceiling at about 1091px, so the headline is a constant
   51.84px across the desktop range and scales below it. Both are deliberate: a
   headline that keeps growing while the column beside it stops is the shape that
   produced the five-line break the first ceiling was lowered to fix.

   **What the ceiling does not fix, recorded because it was not obvious.** The
   comment here used to claim the cap kept the hero "about 1.4x the section
   titles", and measured against the rules as they now stand that was never true
   at any of the ceilings it has had: `.section__title` is 48.83px, so the hero
   is 1.061x at 3.24rem, was 1.180x at 3.6rem and 1.049x at 3.2rem. The larger
   problem is `.contact__title` at 61.04px, which is 9.2px *above* the hero
   headline — it was 3.4px above at 3.6rem, so 10% down widened this rather than
   closing it — and the h1 is therefore not the largest heading on the page, which
   is the one thing its ceiling was meant to guarantee. Left in place rather than
   fixed, and the alternatives have changed shape since this paragraph was
   written: the threshold to beat is 3.815rem, which is what the contact title is
   set at, and raising the hero to it no longer threatens the fold. It would move
   the CTA about 19px down the page, from 693 to 712, against 238px of clearance.
   Recorded rather than taken.

   The floor binds below about 500px, and the ceiling cannot reach there: at 500px
   the headline is 35.2px and sets to two, 371 and 406, and at 390px it is the same
   35.2px over three, 230 / 307 / 234. Neither has a single-word line.
   `text-wrap: balance` is still not the answer, and it was re-measured on the
   current string rather than carried over: byte-identical breaks against `none`
   at 1440, 1091, 897, 500, 390, 360 and 320. The paragraph here used to price the
   orphan's fix at about 460px of column against the 450px a 500px viewport gives
   — that was arithmetic about the previous string, and it does not carry over to
   this one.

   19ch (734px) stood here until it was removed on request, and what it bought is
   worth recording, because it is what the width costs. A `ch` measure scales with
   the font and therefore moves with the column, so 19ch held the same line breaks
   at every width from 1600 down to 500. Letting the column be the measure gives
   that up: the break is now a function of the viewport rather than a constant. */
.hero__title {
  font-size: clamp(2.2rem, 1.215rem + 2.97vw, 3.24rem);
  font-weight: 600;
  letter-spacing: -0.032em;
  line-height: 1.04;
}

/* The tagline under the h1 — the headline this page used to carry, demoted in
   the restructure so the h1 could name the service and the region instead of
   the person's tenure.

   **The ceiling was set by the fold, and the fold no longer binds.** At
   `--step-3` the tagline was 29.76px and its row cost 61px, which put the hero
   CTA at 967 against the 950 fold — 17px past it — and 1.5625rem is the size that
   answered it: 25px and a 31px row. The CTA is at 693 now, 257px clear, so
   nothing is holding the tagline down here but the reading: 25px under a 51.84px
   h1 is the relationship the rule is for. The row costs 47px of the hero and 47px
   of the CTA, measured by hiding it; `.hero__grid` carries the fold.

   `--space-4` and not `--space-5` is the same arithmetic from the other side:
   eight of the fourteen pixels recovered came out of the margin rather than the
   type. It also reads as what it is — a tagline sits closer to its own headline
   than to anything else on the page.

   It spans both tracks, and after the size drop that is no longer forced at the
   top of the range: at 25px the string measures 539px against the h1's 630px
   column, so at 1440, 1184 and 1091 it would now set to one line inside the
   column. It is kept spanned for the widths below those, where the column narrows
   faster than the type does — 529 at 1002 and 471 at 897 against the same 539 —
   because a tagline that is one line at the top of the range and two at the
   bottom is the raggedness this is here to avoid. Measured, it sets to one line
   at every width from 897 up, and to two at 500 and 390, where the floor and the
   viewport are talking rather than this rule.

   The softer colour is the other half of keeping it support rather than a second
   headline; at 25px against the h1's 51.84px the two are not competing on size. */
.hero__subhead {
  margin-top: var(--space-4);
  font-size: clamp(1.25rem, 1.05rem + 0.9vw, 1.5625rem);
  line-height: 1.25;
  letter-spacing: -0.015em;
  color: var(--text-soft);
}

/* A wrapper rather than a paragraph, because the lede runs to three of them and
   the type size has to be stated once. `p + p` below is the spacing, so a fourth
   paragraph needs no rule either.

   No `max-width`, and **the measure is now the page's worst departure from the
   site's own 45–75 character rule by a wide margin.** The lede spans both tracks,
   so at 1184px and up it is the full 1088px of the shell — wider than the 806px
   copy column it used to sit in, and wider than the 968px the one-column hero
   gave it, because a spanning row gets the shell and neither of those had all of
   it. Measured per paragraph, the three run to 2, 3 and 1 lines and the longest
   line is 114 characters. 1088 of 114 is an average character of 9.54px, against
   the 9.37 the 806px column gave, so the type is the same and the line is simply
   a third longer. `ch` is not that average: it is the advance of "0", 12.11px in
   this type, which is why a cap written in `ch` never comes out at the character
   count it names.

   The cap question is now a different one from the one this comment used to
   answer, and the difference is the fold. **A cap is affordable again** — the hero
   came down to 710px in this revision, so no cap in range takes the CTA under a
   950 fold any more. Re-measured at 1088px:

     62ch  759px  max 80   8L  h288   CTA 743  clear by 207
     60ch  734px  max 80   8L  h288   CTA 743  clear by 207
     56ch  685px  max 72  10L  h352   CTA 807  clear by 143
     52ch  636px  max 67  10L  h352   CTA 807  clear by 143

   So the only reason left not to cap is the reading, which is now the whole of the
   argument rather than half of it: every cap that brings the line into the 45–75
   range also pulls the lede in to 636–759px under a 630px headline column and a
   1088px shell, and the spanning row is the shape the hero was designed around.
   The cap that fits the range best is 56ch at 72 characters, and it is the one the
   old table preferred too.

   **`column-count: 2` is still the fix that fits best of all**, and it is the one
   worth reaching for if this is ever fixed: the same 1088px in two columns at a
   4rem gap sets 57 characters to the line, inside the range, and comes out 208px
   tall against the single column's 224 — so the hero is 16px shorter and the
   measure is inside the rule, which no cap manages. It is not applied here because
   it changes how the hero reads, not just how it measures, and that is a different
   kind of decision from a cap.

   What follows is the history of the single-column measurements, kept because the
   cap numbers above were read against it. An earlier version of this comment said
   three revisions had each measured longer than the last — 72, then 79, then 89 —
   and that this tracked average word length falling from 6.28 to 5.85. **That was
   wrong, and the copy after it is what showed it.** Three revisions were then
   measured in sequence, and their average word lengths run 5.93, 5.77 and 5.71
   against longest lines of 89, 87 and 86. They move together across those three —
   but by three characters over the whole sequence, which is the size of the effect
   rather than a rule.

   That was tested as a 2x2 — two revisions of the copy at both column widths, set
   in the page. **Both of its revisions are superseded,
   so its figures are history**, but the finding is what the 86 was read against:

     756px  5.77 avg  79   5.93 avg  82
     806px  5.77 avg  87   5.93 avg  89

   At a fixed column the shorter-worded copy measures two or three characters
   shorter, while the 50px from 756 to 806 is worth eight. The old 72/79/89 triple
   was read across three revisions and at least two column widths, so it was never
   a controlled comparison; the column was doing most of it. What the words do is
   move the number a few characters either way, by deciding which of them lands at
   the break — real, but second-order, and the reason the 114 above is a property
   of the width rather than of this copy.

   54ch stood here until it was removed on request; at 806px it cost fourteen
   characters, 72 against 86. The four 2ch-spaced candidates measured then, and
   superseded by the table above:

     54ch  654px  max 72  14L  h480
     56ch  678px  max 72  13L  h448
     58ch  702px  max 77  13L  h448
     60ch  727px  max 77  13L  h448

   56ch was then the widest cap landing inside the range — 72 characters, 128px
   short of the headline's right edge. 54ch tied it at 72 and sat three characters
   further in, so the reason to prefer 56ch was the 24px of column, not the range.
   58ch and 60ch were both 77, two over, and bought nothing 56ch did not.

   Per paragraph, at the widths that matter, the spanning row now measures 114 at
   1440 and 1184 where the shell is capped, 97 at 1024, and 88 at 900 — so it
   comes back toward the range only where the viewport narrows the shell under it,
   and it is furthest out exactly where the design is at its widest. Every figure
   above was counted rather than predicted: each cap set in the page, the
   characters per line read back off the render, and every paragraph reported
   rather than the first — the longest line is not always in the opening one. */
.hero__lede {
  margin-top: var(--space-6);
  font-size: clamp(1.0625rem, 1rem + 0.4vw, 1.25rem);
  line-height: 1.6;
  color: var(--slate-mid);
}
.hero__lede p + p { margin-top: var(--space-4); }

/* The Microsoft certification, the badge that certifies it, and the primary call
   to action, closing the hero under the lede. Folded into the lede the credential
   reads as one more clause in a sentence; given a line of its own, the button
   under it costs a full row of whitespace. "Microsoft Certified" is set once, as
   a label over the name — repeating the prefix in front of it doubles the length
   and buries the part that differs. The mark is decorative; the label under it
   already says what it means.

   One name, not two. The Azure Administrator Associate is still on the page, in
   the experience record's credential block; here it was the second of two lines
   in a row competing with the headline.

   All three share one line, and the badge's leaving is what let them. With it
   here the row was the mark, the two credential lines, the 85px shield and the
   297px button: 665px wanted against a 630px column, so the button wrapped to a
   line of its own. The badge has gone down to the footer, and without it the row
   comes to 564px — the button is back on the credentials' line, in the place the
   shield was holding.

   That 564 is the number to hold on to, because the row is now a spanning row and
   the space around it has changed twice without the row itself changing at all.
   It was 806px when the copy column was 806 and the split was 4:1; 242px of slack
   then, 540px now that the row spans and is the shell's full 1088. The auto margin
   below spends whatever there is either way, so the button's distance from the
   names is the row's width and not a value stated here. Where it sits on that line
   is the auto margin, not this rule — and at 540px of slack the button is far
   enough right to read as belonging to the photograph above it rather than to the
   credentials beside it, which is worth knowing before the span is relied on.

   Removing the second name left that 564 exactly where it was, which is not the
   result the change looks like it should have. The block is sized by its widest
   line, and measured at max-content inside this list — the only place a clone
   inherits the right font-size; hung off `<body>` it reads about 14% wide — the
   two names came to 213px and 197px. The longer one stayed, so the shorter one
   was already inside its width and freed nothing. The label is 170px, under
   both. What the removal bought is a line of height: the block was 59px against
   the button's 56 and is now 56. Those 3px were the margin between the CTA and the
   fold when this was written; the CTA is 257px inside it now, so they are three
   pixels of row height and nothing more.

   `center` and not `flex-start`. What sets this row's height is the credential
   block at 56px against the button's 56, so they now agree and the difference is
   nothing either way; the mark is 22px, and top-aligning it against a 56px
   button is what the rule is here to prevent: centred, the mark, the name and
   the button all sit on the row's middle. */
.hero__creds {
  margin-top: var(--space-6);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-4);
}

.hero__creds-mark {
  flex: none;
  width: 1.375rem;
  height: 1.375rem;
  /* Optical: the label's line box carries half-leading above the caps, so the
     mark nudged down sits level with the word rather than floating above it. */
  margin-top: 0.1rem;
  color: var(--accent);
}
.hero__creds-mark svg { width: 100%; height: 100%; }

.hero__creds-label {
  font-family: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: var(--step--2);
  font-weight: 600;
  letter-spacing: 0.14em;
  line-height: 1.2;
  text-transform: uppercase;
  color: var(--accent-text);
}

.hero__creds-list {
  margin-top: var(--space-1);
  font-size: var(--step--1);
  line-height: 1.45;
  color: var(--text);
}

/* The buttons are the last item of the credentials row, and `margin-left: auto`
   puts the pair on the right of it: the mark and the credential line hold the
   row's left edge, the buttons hold the right one, and the space between them
   stops being a row that ends early. That is the whole job of the auto margin
   here, and it has outlived the single button it was written for.

   It used to land on the note rule's right edge below it. That note is gone, and
   the sentence it carried — "Founder & Principal Software Architect at Expert
   Tech Pilot." — is now the byline at the top of the hero, so there is nothing
   under the buttons for them to line up with and nothing left below them at all.
   What the auto margin still does is hold the row's
   right edge, which is worth having on its own: without it the pair would trail
   the credentials rather than close the row.

   An earlier two-column hero put this row at 563px, and there the button shared
   the line only because the room was left over — the mark and the credential lines
   took 251px and the button 239, so the row needed 506 to hold them together and
   had 563. `margin-left: auto` did the same thing, but as a consequence rather
   than as a statement.

   `flex: none` and not `auto`, so the group keeps its natural width and the two
   buttons stay on one line together. The line break is decided before any
   shrinking happens, so `auto` would usually do the same; this says it outright
   and stops a font fallback a few pixels wider from squeezing one button onto
   the line above and wrapping its label.

   `flex-wrap` is the one thing here that can move, and it is inside the group
   rather than outside it. `.hero__creds` already wraps, so at narrow widths the
   whole group drops to a line of its own; at 34rem the two buttons then stack,
   which is handled at that breakpoint rather than here.

   Not measured: this pair is wider than the single button it replaced, and the
   row it sits in has not been re-measured in a browser since. `flex: none` means
   the group cannot shrink to fit, so if the row is ever seen wrapping at a width
   it should not, this element is the one to measure first. */
.hero__actions {
  flex: none;
  margin-left: auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-3);
}

.blueprint { width: 100%; color: var(--slate); }

/* SVG sub-parts share one ink so they re-theme with the scheme. The hero's own
   parts — source, connect, out — went with the hero diagram; the rest belong to
   the three case-study figures. */
.bp-flow path { stroke: currentColor; stroke-opacity: 0.42; stroke-width: 1.25; }
.bp-ticks path { stroke: currentColor; stroke-opacity: 0.24; stroke-width: 1; }
.bp-doc rect { fill: none; stroke: currentColor; stroke-opacity: 0.34; stroke-width: 1.25; }
.bp-doc rect:last-child { fill: currentColor; fill-opacity: 0.10; }
.bp-stack path { fill: none; stroke: currentColor; stroke-opacity: 0.40; stroke-width: 1.25; }
.bp-axis { stroke: currentColor; stroke-opacity: 0.28; stroke-width: 1; }
.bp-wave { stroke: var(--logo-blue); stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }
.bp-marker circle { fill: var(--logo-blue); }
.bp-plan path { stroke: currentColor; stroke-opacity: 0.32; stroke-width: 1.25; }
.bp-occupancy circle { fill: var(--logo-blue); opacity: 0.55; }
.bp-occupancy circle:nth-child(2n) { opacity: 0.28; }

/* ── Three ways to engage ─────────────────────────────────────────────────
   The commercial half of the page. The offers are the one place on the page
   that lifts onto a raised surface, because they are the only content a visitor
   is meant to pick from. */
.engagements { --section-pad: var(--space-11); }

.offers {
  margin-top: var(--space-6);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-5);
}
/* One card surface, two consumers. An engagement offer and a guide card are the
   same object visually, and sharing the rule is what keeps them the same object
   visually — restating the four tokens in a second block is how the two drift
   apart six months from now. */
.offer,
.guide {
  display: flex;
  flex-direction: column;
  padding: var(--space-6);
  background: var(--bg-raised);
  border: var(--rule) solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

/* The paid entry point, marked with the section's only accent rule. */
.offer--entry { border-top: 2px solid var(--accent); }
.offer__tag,
.guide__tag {
  font-size: var(--step--2);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent-text);
  margin-bottom: var(--space-4);
}

.offer__name,
.guide__title {
  font-size: var(--step-1);
  letter-spacing: -0.015em;
  text-wrap: balance;
}
.offer__lede,
.guide__lede {
  margin-top: var(--space-3);
  font-size: var(--step--1);
  line-height: 1.55;
  color: var(--text-soft);
}

/* ── Knowledge Center strip ───────────────────────────────────────────── */
/* Four cells — three guides and the way into the rest — so the grid never
   leaves an orphan. `.offers` is three-up and puts its odd card across the
   whole row, which is right for a pricing block and would read as a mistake
   under a two-column pair of guide cards. */
.guides {
  margin-top: var(--space-6);
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-5);
}
@media (max-width: 68rem) {
  .guides { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 40rem) {
  .guides { grid-template-columns: 1fr; }
}

.guide { position: relative; }

/* The whole card is the link target, but the link is the heading rather than a
   wrapper: an <a> around a heading puts unreadable length in the accessible
   name, and a card with no link at all cannot be reached by keyboard. Same
   arrangement as the hub's `.kcard` in knowledge.css. */
.guide__title a {
  color: var(--text);
  text-decoration: none;
}
.guide__title a:hover { color: var(--accent-text); }
/* Widens the click target to the card without moving the semantics. */
.guide__title a::after {
  content: "";
  position: absolute;
  inset: 0;
}

/* Pinned to the card foot so every card in a row ends on one line, following
   .offer__close and .kcard__more. */
.guide__more {
  margin-top: auto;
  padding-top: var(--space-5);
  font-size: var(--step--1);
  font-weight: 500;
  color: var(--accent-text);
}

/* The way into the full Knowledge Center, and the reason this section is on the
   homepage at all: a link in the body of the page rather than a nav item or a
   footer line. Accent rule so it reads as the section's action rather than as a
   fourth guide. */
.guide--more { border-top: 2px solid var(--accent); }

.offer__list {
  margin-top: var(--space-5);
  padding-top: var(--space-5);
  border-top: var(--rule) solid var(--border);
  display: grid;
  gap: var(--space-3);
}
.offer__list li {
  position: relative;
  padding-left: var(--space-5);
  font-size: var(--step--1);
  line-height: 1.55;
  color: var(--text-soft);
}
.offer__list li::before {
  content: "";
  position: absolute;
  left: 0; top: 0.6em;
  width: 0.35rem; height: 0.35rem;
  border-radius: 50%;
  background: var(--accent);
}

/* Pinned to the card's foot so the three lists end on one line. Uses
   --text-soft, not --text-faint: this is selling copy, and faint measures
   3.4:1 on the raised card surface — under AA for 14px text. */
.offer__close {
  margin-top: auto;
  padding-top: var(--space-5);
  font-size: var(--step--1);
  line-height: 1.5;
  color: var(--text-soft);
}

/* ── Service guide caret ─────────────────────────────────────────────────
   The disclosure caret in the five guides' FAQ lists. The class kept its
   service-scoped name after the old eleven-service ledger came off the
   homepage, and there is no `.service` wrapper any more.

   `margin-left: auto` pushes it to the far end of the row, and `flex: none`
   keeps it from shrinking against a long question. The rotation lives on
   `.service-faq details[open]` below. */
.service__caret {
  margin-left: auto;
  flex: none;
  color: var(--accent-text);
  transition: transform var(--dur) var(--ease);
}
.service__caret svg { width: 1rem; height: 1rem; display: block; }

/* No height animation. `::details-content` is Baseline as of September 2025,
   but the `block-size: 0 → auto` slide needs `interpolate-size: allow-keywords`
   and that is Chromium only — Firefox and Safari support the pseudo-element and
   not the interpolation, so they would snap regardless. The caret rotation is
   the whole motion, and reduced motion already flattens it via the universal
   `transition-duration` override at the foot of this file. */

/* ── Selected work ────────────────────────────────────────────────────── */
.work { --section-pad: var(--space-10); }

.case {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.8fr);
  gap: clamp(2rem, 6vw, 5rem);
  align-items: center;
  padding-block: var(--space-9);
  border-top: var(--rule) solid var(--border);
}
.case:last-child { padding-bottom: 0; }
.case--flip .case__body { order: 2; }
.case--flip .case__figure { order: 1; }

.case__title {
  font-size: clamp(1.5625rem, 1.2rem + 1.5vw, 2.441rem);
  letter-spacing: -0.026em;
  max-width: 22ch;
}
.case__lede {
  margin-top: var(--space-5);
  font-size: var(--step-1);
  line-height: 1.55;
  color: var(--text-soft);
  max-width: 46ch;
}

/* The outcome, lifted out of the ledger that used to close this section and
   folded back under the case that produced it. Same figure grammar as the
   ledger, sized down for a case body and sharing the lede's measure. */
.case__impact {
  margin-top: var(--space-6);
  max-width: 46ch;
}
.case__impact-figure {
  font-family: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: clamp(1.5625rem, 1.2rem + 1.3vw, 2.441rem);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--space-3);
}
.case__impact-unit {
  font-family: "Public Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--accent);
  line-height: 1.2;
}
.case__impact-pair { white-space: nowrap; }
.case__impact-arrow { color: var(--accent); font-weight: 400; }
.case__impact-note {
  margin-top: var(--space-3);
  color: var(--text-soft);
  font-size: var(--step-0);
  line-height: 1.7;
}

.case__points {
  margin-top: var(--space-6);
  display: grid;
  gap: var(--space-3);
  max-width: 52ch;
}
.case__points li {
  position: relative;
  padding-left: var(--space-5);
  font-size: var(--step-0);
  line-height: 1.6;
  color: var(--text-soft);
}
.case__points li::before {
  content: "";
  position: absolute;
  left: 0; top: 0.62em;
  width: 0.4rem; height: 0.4rem;
  border-radius: 50%;
  background: var(--accent);
}
.case__tech {
  margin-top: var(--space-6);
  padding-top: var(--space-4);
  border-top: var(--rule) solid var(--border);
  font-size: var(--step--1);
  color: var(--text-faint);
  letter-spacing: 0.005em;
}

.case__figure { margin: 0; }
.blueprint--case { max-width: 25rem; margin-inline: auto; color: var(--slate); }

/* ── Experience ───────────────────────────────────────────────────────── */
.experience { --section-pad: var(--space-12); }

.timeline { display: grid; }
.timeline__item {
  display: grid;
  grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
  gap: clamp(1rem, 4vw, 3.5rem);
  padding-block: var(--space-6);
  border-top: var(--rule) solid var(--border);
}
.timeline__item:last-child { border-bottom: var(--rule) solid var(--border); }

.timeline__period {
  font-family: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: var(--step--1);
  font-weight: 500;
  color: var(--accent-text);
  letter-spacing: 0.005em;
  padding-top: 0.35rem;
}
.timeline__role {
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: -0.014em;
}
.timeline__org {
  margin-top: var(--space-1);
  font-size: var(--step--1);
  font-weight: 500;
  color: var(--slate-mid);
}
.timeline__desc {
  margin-top: var(--space-3);
  font-size: var(--step-0);
  line-height: 1.65;
  color: var(--text-soft);
  max-width: 62ch;
}

/* Certifications and education, closing the experience section rather than
   holding a section of their own. The timeline's last row already ends on a
   hairline, so there is no rule here — a second one would draw a double line
   across the seam. One size down throughout: this is a footnote to the record
   above it, not another section of the page.

   The badge and its text share a line, in a fixed gutter. Stacked, as they were
   in the old credentials section, four entries read as eight lines and the block
   stops looking like a footer. The gutter is sized to "AZ-305" — the widest of
   the four — so the text of every entry starts on the same axis. */
.credentials-footer {
  margin-top: var(--space-9);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
  gap: var(--space-7) clamp(2rem, 5vw, 4rem);
}

.credentials-footer__label {
  font-family: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: var(--step--2);
  font-weight: 600;
  letter-spacing: 0.14em;
  line-height: 1.2;
  text-transform: uppercase;
  color: var(--accent-text);
}

.credentials-footer__list {
  margin-top: var(--space-4);
  display: grid;
  gap: var(--space-3);
}
.credentials-footer__list li {
  display: grid;
  grid-template-columns: 4.25rem minmax(0, 1fr);
  gap: var(--space-3);
  align-items: baseline;
}
.credentials-footer__badge {
  font-family: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: var(--step--2);
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--accent-text);
}
.credentials-footer__text {
  font-size: var(--step--1);
  line-height: 1.5;
  color: var(--text-soft);
}

/* ── Contact ──────────────────────────────────────────────────────────── */
.contact { --section-pad: var(--space-11); }

.contact__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(2.5rem, 6vw, 5rem);
  align-items: start;
}
/* The 16ch cap looks like the thing that decides where this headline breaks and
   is not. At 1440 it resolves to 561px against a 504px column, so the column
   wins; it binds only around 896px, where it lands at 483px. `text-wrap:
   balance` was measured on the copy it now holds and changed no line break, so
   neither the cap nor balancing is the lever — the wording is. A headline that
   breaks badly here has to be rewritten, not re-measured. See the note on the
   contact section in index.html. The figures above came from setting candidate
   headlines in this element at 1440 and 896 and reading the breaks, which is the
   only way to tell which of the three levers is actually binding. */
.contact__title {
  font-size: clamp(2.441rem, 1.6rem + 3vw, 3.815rem);
  letter-spacing: -0.032em;
  max-width: 16ch;
}
.contact__lede {
  margin-top: var(--space-6);
  font-size: var(--step-1);
  line-height: 1.6;
  color: var(--text-soft);
  max-width: 44ch;
}

.contact__direct {
  margin-top: var(--space-9);
  padding-top: var(--space-6);
  border-top: var(--rule) solid var(--border-strong);
  display: grid;
  gap: var(--space-5);
}
.contact__line { display: grid; grid-template-columns: 7rem minmax(0, 1fr); gap: var(--space-4); }
.contact__line dt {
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
  padding-top: 0.2rem;
}
.contact__line dd { font-size: var(--step-0); color: var(--text); }
.contact__line a { color: var(--accent-text); font-weight: 500; }

/* ── Form ─────────────────────────────────────────────────────────────── */
.form {
  background: var(--bg-raised);
  border: var(--rule) solid var(--border);
  border-radius: var(--radius-lg);
  padding: clamp(1.5rem, 3vw, 2.25rem);
  box-shadow: var(--shadow);
  display: grid;
  gap: var(--space-5);
}

.form__row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-5); }

.field { display: grid; gap: var(--space-2); }
.field label {
  font-size: var(--step--1);
  font-weight: 500;
  color: var(--text);
  letter-spacing: 0.005em;
}
.field__opt { color: var(--text-faint); font-weight: 400; }
.field__hint {
  font-size: var(--step--1);
  line-height: 1.5;
  color: var(--text-faint);
}
.field[data-state="ok"] .field__hint { color: var(--accent-text); }
.field[data-state="error"] .field__hint { color: oklch(52% 0.17 25); }

.field input,
.field select,
.field textarea {
  font-family: inherit;
  font-size: var(--step-0);
  color: var(--text);
  background: var(--bg);
  border: var(--rule) solid var(--border-strong);
  border-radius: var(--radius);
  padding: 0.7rem 0.85rem;
  width: 100%;
  transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.field textarea { resize: vertical; min-height: 7rem; line-height: 1.6; }
.field select { appearance: none; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5' fill='none' stroke='%23666' stroke-width='1.4' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 0.85rem center; background-size: 0.85rem;
  padding-right: 2.2rem;
}
.field input:hover, .field select:hover, .field textarea:hover { border-color: var(--slate-light); }
.field input:focus, .field select:focus, .field textarea:focus {
  border-color: var(--logo-blue);
  outline: 2px solid var(--logo-blue);
  outline-offset: 0;
}
.field input:user-invalid, .field textarea:user-invalid { border-color: oklch(52% 0.17 25); }

.form__status {
  font-size: var(--step--1);
  line-height: 1.6;
  color: var(--logo-blue-darkest);
  background: var(--logo-blue-lightest);
  border-radius: var(--radius);
  padding: 0;
  min-height: 0;
}
.form__status:not(:empty) { padding: var(--space-3) var(--space-4); }
.form__status a { font-weight: 600; }
.form__status ul { display: grid; gap: var(--space-1); }

/* Heading shared by both enquiry forms. It names the three engagement types
   before the fields, so a visitor answers in the terms the practice is sold
   in rather than describing a problem and leaving the routing to me. The
   `.form__head` and `.chat__title` selector pair is what keeps the two doors
   one vocabulary — the contact section's form and the chat panel carry the
   same head, sub, field labels and submit label, and this rule is where that
   is enforced. Only the textarea placeholder differs, because the panel's is
   two rows. */
.form__intro { margin-bottom: var(--space-5); }
.form__head, .chat__title {
  font-size: var(--step-2);
  letter-spacing: -0.02em;
  text-wrap: balance;
}
.form__sub {
  margin-top: var(--space-3);
  font-size: var(--step--1);
  line-height: 1.6;
  color: var(--text-soft);
  max-width: 42ch;
}

/* ── Footer ───────────────────────────────────────────────────────────── */
.site-footer {
  background: var(--slate-darkest);
  color: oklch(96% 0.008 80);
  padding-block: var(--space-8);
}
.footer__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4) var(--space-6);
}
.footer__name {
  font-family: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-weight: 600;
  letter-spacing: -0.015em;
}
.footer__links { display: flex; flex-wrap: wrap; gap: var(--space-5); margin-left: auto; }
.footer__links a {
  color: oklch(96% 0.008 80 / 0.78);
  font-size: var(--step--1);
  text-decoration: none;
  transition: color var(--dur) var(--ease);
}
.footer__links a:hover { color: var(--logo-blue-light); }
/* The footer's two small-print lines — one of identity, one of territory — in
   the same register and each on a row of its own. `flex-basis: 100%` is what
   buys the row: the footer is a single wrapping flex line, and without it these
   would try to sit beside the name and the links. */
.footer__meta,
.footer__areas {
  font-size: var(--step--1);
  color: oklch(96% 0.008 80 / 0.55);
  flex-basis: 100%;
}

/* The identity line above has never needed a cap; this one does. Nine city names
   run to 135 characters at 1440 — nearly double the 75 ceiling — and would cross
   the shell in one unbroken line. The cap is the widest that holds the longest
   rendered line inside the range, measured per rendered line rather than
   predicted from `ch`, which is the advance of "0" and not of an average
   character (1ch is 8.477px at this size, so 58ch resolves to 492px):

     50ch  424px  3 lines  max 62  [54,62,17]   <- 17-char orphan
     54ch  458px  2 lines  max 69  [69,65]
     58ch  492px  2 lines  max 71  [71,63]      <- this
     62ch  526px  2 lines  max 71  [71,63]
     66ch  559px  2 lines  max 82  [82,52]      <- over the ceiling

   58ch over 62ch because they render identically at 1440 and 58ch is the tighter
   box for the same result. Below 640px the container is narrower than the cap and
   the cap stops binding, which is the point at which the line is already inside
   the range on its own. */
.footer__areas { max-width: 58ch; }

/* The Azure certification badge, moved down from the hero. It is what makes the
   two `Microsoft Certified` lines in the experience record above true, and at the
   head of the footer it reads as a seal rather than as one more thing standing
   between the headline and the button.

   It is also what the hero's credentials row was short of room for: the badge and
   the button together wanted 665px of that row and it had 630, which is why the
   button sat on a line of its own. Out of the row, the button joins it.

   Sized by height with `width: auto`, which holds the file's own ratio; a width
   here would fight the ratio and letterbox it. 5.5rem draws the shield 85px
   across, which is the size the hero settled on: the file used to be a 900x670
   frame with the shield occupying 570x589 of it, so a 3rem height spent a third
   of itself on the frame's empty margin and drew the shield 61px across, and
   trimmed to the artwork 3rem draws it 46px — at which the banner lettering
   inside stops being readable.

   5.5rem is tall for a bar built around one line of text, and the footer grows to
   hold it. That is the trade the seal makes; a smaller one is a smudge.

   No `border-radius`. The file carries real transparency rather than a painted
   board, so the shield's own outline is the shape that meets the background — and
   the banner's tips reach the file's left and right edges, where a radius would
   clip them. The footer is `--slate-darkest`, which the badge's own white and
   blue read against rather than sitting on a light plate. */
.footer__badge {
  flex: none;
  height: 5.5rem;
  width: auto;
}

/* ── Chat launcher ──────────────────────────────────────────────────────
   A floating shortcut to the enquiry the contact section already takes.
   The wrapper carries .scheme-paper in the markup, which is not decorative:
   a position:fixed element sits outside every section, so without a scheme
   class it resolves none of the layer-2 tokens and paints with nothing. */
.chat {
  position: fixed;
  right: var(--space-5);
  bottom: calc(var(--space-5) + env(safe-area-inset-bottom, 0px));
  z-index: 40;            /* under .mobile-nav (45) and .site-header (50) */
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-3);
  /* Deliberately NO pointer-events: none on this wrapper. It buys almost
     nothing — the box shrinks to its two children, so the only dead area is
     the 12px gap between them — and WebKit hit-tests the touch against the
     wrapper first, which makes the launcher untappable on iOS. */
}

.chat__launch {
  box-shadow: var(--shadow);
  gap: var(--space-2);
  padding: 0.7rem var(--space-5);
}
.chat__launch svg { width: 1.05em; height: 1.05em; flex: none; }

.chat__panel {
  width: min(23rem, calc(100vw - 2 * var(--space-5)));
  max-height: min(46rem, calc(100vh - 9rem));
  /* dvh where supported: mobile browser chrome makes 100vh taller than the
     area actually on screen, which would push the submit button off it. */
  max-height: min(46rem, calc(100dvh - 9rem));
  overflow-y: auto;
  overscroll-behavior: contain;   /* panel scroll must not chain to the page */
  padding: var(--space-6);
  background: var(--bg-raised);
  border: var(--rule) solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}
/* Progressive enhancement: animates the way out of display:none where the
   browser supports it, and simply appears where it does not. */
@starting-style {
  .chat__panel { opacity: 0; transform: translateY(0.5rem); }
}

.chat__head { display: flex; align-items: flex-start; gap: var(--space-4); }
.chat__title { flex: 1; }

/* The panel takes focus on touch devices so the soft keyboard does not open
   the moment the launcher is tapped. It is tabindex="-1", so no keyboard user
   ever reaches it by Tab and the ring would only ever be an artefact. */
.chat__panel:focus { outline: none; }

.chat__close {
  flex: none;
  display: grid;
  place-items: center;
  width: 2rem; height: 2rem;
  margin: -0.35rem -0.35rem 0 0;
  color: var(--text-soft);
  background: none;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.chat__close:hover { color: var(--text); background: var(--border); }
.chat__close svg { width: 1rem; height: 1rem; }

/* Tighter than the contact form: the panel has a fixed maximum height, and a
   form that opens already needing a scroll reads as broken rather than full. */
.chat__form { margin-top: var(--space-5); display: grid; gap: var(--space-4); }
.chat__form textarea { min-height: 5rem; }

/* The launcher ships 41px tall, just under the 44px minimum tap target.
   Keyed to pointer type rather than viewport width: an iPad is a touch device
   at 768px and would otherwise keep the small target. */
@media (pointer: coarse) {
  .chat__launch { padding-block: 0.85rem; }
}

/* ── Booking page ─────────────────────────────────────────────────────────
   book.html. One grid, two tracks: the copy at 0.9fr and the calendar at
   1.1fr, because the page's whole job is the thing in the right column and
   the copy is what a visitor reads while deciding whether to use it.

   The 1.1fr is not a fudge to make the panel look important. Calendly's
   month grid sets a seven-column table with day numbers and its own padding;
   under about 460px it starts truncating the weekday labels, and the left
   track has no such floor — it is prose, and prose reflows. The ratio is
   what keeps the wider track on the element that cannot shrink. */
.booking { --section-pad: clamp(3rem, 6vw, 5.5rem); }

.booking__grid {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  gap: clamp(2.5rem, 5vw, 4.5rem);
  align-items: start;
}

/* Sized below the homepage h1's 3.6rem ceiling on purpose. This page's
   headline is a label for a form, not a claim about twenty years of work,
   and at equal weight the two pages would read as equally important when
   only one of them is the front door.

   The figure here said 3.815rem until the heading restructure, and that was
   never the homepage h1's ceiling: 3.815rem is --step-6 and the max of
   `.contact__title`. `.hero__title` caps at 3.6rem. This page clears both, so
   nothing about the rule changes — only a comment that had the wrong number
   in it. */
.booking__title {
  font-size: clamp(2.1rem, 1.5rem + 2.4vw, 3.4rem);
  letter-spacing: -0.024em;
}

.booking__lede {
  margin-top: var(--space-5);
  font-size: var(--step-1);
  line-height: 1.55;
  color: var(--text-soft);
  max-width: 44ch;
}

.booking__block { margin-top: var(--space-8); }

.booking__heading {
  font-size: var(--step-2);
  letter-spacing: -0.02em;
}

/* A rule rather than a bullet or a tick. A tick is a claim that the item is
   true of this engagement; these are examples offered so a visitor can
   recognise their own situation, and the blue rule marks them as a list
   without asserting anything about them. */
.booking__list {
  margin-top: var(--space-4);
  display: grid;
  gap: var(--space-3);
}
.booking__list li {
  position: relative;
  padding-left: var(--space-5);
  font-size: var(--step-0);
  line-height: 1.55;
  color: var(--text-soft);
}
.booking__list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.78em;
  width: 0.6rem;
  height: 1px;
  background: var(--accent);
}

.booking__fallback {
  margin-top: var(--space-7);
  font-size: var(--step--1);
  color: var(--text-soft);
}

.booking__embed-sub {
  margin-top: var(--space-2);
  font-size: var(--step--1);
  color: var(--text-soft);
}

/* `--bg-raised` and `--shadow` lift the panel off the paper without a second
   scheme, so the calendar reads as an object dropped onto the page rather
   than as another band of it. The iframe is inset by the panel's own padding
   so Calendly's edge is not flush to the border, and it takes a background
   of its own — the frame is empty until the request lands, and an unset
   iframe paints white, which is the one place on this page pure white would
   appear. */
.booking__embed {
  background: var(--bg-raised);
  border: var(--rule) solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  padding: var(--space-3);
}

.booking__frame {
  display: block;
  width: 100%;
  height: 46rem;
  margin-top: var(--space-4);
  border: 0;
  border-radius: calc(var(--radius-lg) - var(--space-3));
  background: var(--paper-lighter);
}

.booking__embed-note {
  margin-top: var(--space-4);
  font-size: var(--step--1);
  color: var(--text-faint);
}

/* Stacked at the same 56rem the rest of the file stacks its two-column grids
   at. The frame grows rather than shrinks: full width on a phone is narrower
   than the left track was on desktop, and Calendly's month grid needs the
   height back once it loses the width. */
@media (max-width: 56rem) {
  .booking__grid { grid-template-columns: 1fr; }
  .booking__frame { height: 44rem; }
}

/* ── Scroll reveal ────────────────────────────────────────────────────── */
.reveal {
  opacity: 0;
  transform: translateY(1.25rem);
  transition: opacity var(--dur-slow) var(--ease), transform var(--dur-slow) var(--ease);
}
.reveal.is-visible { opacity: 1; transform: none; }

/* ── Responsive ───────────────────────────────────────────────────────── */
/* The offers are the one grid that changes at its own width rather than the
   page's: below ~1088px three columns drop under 320px each and the bullet
   copy breaks to two and three lines apiece. The odd card out spans the row
   instead of sitting beside an empty cell. */
@media (max-width: 68rem) {
  .offers { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .offer:last-child { grid-column: 1 / -1; }
}

/* Below this the paired columns fall under ~330px and every bullet starts
   wrapping to two lines, while the spanning card stays twice their width —
   which reads as a broken layout rather than a deliberate pair. */
@media (max-width: 48rem) {
  .offers { grid-template-columns: 1fr; }
  .offer:last-child { grid-column: auto; }
}

/* The header row is brand + nav + two buttons on one line, and none of them is
   allowed to wrap. Below the row's natural width the flex row does not reflow,
   it pushes the last button past the shell's right edge and clips it — with a
   horizontal scrollbar on the page to reach it.

   **77.5rem (1240px), raised from 70rem by the Services disclosure.** The row
   was measured finished at 70rem at 7px of slack, and a third nav item is not
   7px: the trigger is "Services" plus an 8px gap plus a ~12px caret, and the
   nav's own 24px gap lands with it, so the row grew 104px to 1118.1px. Set
   against a 1025px content box that is a 93px overflow, which is what the old
   breakpoint would have shipped — the last button pushed off the shell and a
   scrollbar under the page.

   **How the number was taken, because the obvious instrument is blind.** The
   row's free space is not `documentElement.scrollWidth`: an overfull row eats
   the header's 48px right padding and parks the last button on the screen edge
   while that value still equals the viewport, because the page never scrolls.
   It is the content box minus the children, the gaps and the explicit margins
   — with `.header__nav`'s `margin-left: auto` pinned to zero first, since an
   auto margin absorbs every spare pixel and makes the row read as exactly full
   at every width. On that measure, at 1241px: content box 1130, row 1118.1,
   **11.9px of slack.** The 7px this row used to carry is now 11.9px, so the
   raise bought back the old margin rather than merely spending it.

   **The band between 1229 and 1241 is why this is not simply "where it stops
   overflowing".** Below a 1214px content box the nav does not overflow, it
   *compresses*: the two `<a>` links have no `nowrap`, so they wrap to two
   lines and the bar grows taller with them. That is a quieter failure than a
   clipped button and it starts before the noisy one, so the breakpoint is set
   clear of the compression band rather than at the overflow edge.

   **Chrome matches `@media` against `window.innerWidth`, scrollbar included,
   and that is the width stated here.** Verified rather than assumed: with the
   old 70rem rule the nav is visible at a 1121px window whose layout viewport
   is 1106px, which is inside the query — so the query is not being evaluated
   against the layout width. The 15px difference matters at this margin, and
   the 11.9px figure above is quoted against the 1241px window an author would
   actually type into a resize handle.

   History, kept because the numbers are not interchangeable: 64rem was enough
   for five nav items and not six — adding "Knowledge" widened the row ~105px
   and opened a 1024–1098px band with the CTA cut off. 70rem covered that row
   at 1002px, which a later restructure made history: four nav items went
   (~300px) and a second button arrived (~220px), and the breakpoint was left
   where it was on that reasoning rather than on a measurement.

   The page does scroll sideways between 545px and 609px, and it is not this
   row. It is `.hero__actions`: its two buttons do not fit on one line until the
   34rem breakpoint stacks them, which is 65px too late (+62px of overflow at
   545px, tapering to 0 at 609px). Pre-existing, independent of the header, and
   noted here only so the next reader does not take this block for a clean bill.

   The row's budget is still 7px-shaped in this sense: there is no third place
   to find 100px. The gaps are already at 16px, the nav's at 24px and the
   brand's at 8px, all cut to pay for the monogram. The next thing added here
   should be measured against 11.9px, or it should displace something. */
@media (max-width: 77.5rem) {
  .header__nav { display: none; }
  .header__cta { display: none; }
  .header__toggle { display: block; }
}

@media (max-width: 56rem) {
  /* The two-column grids in this file stack here — the case studies, where the
     body and the figure change places. The hero is back with them now that it has
     a headline column and a photograph column again; it sat this rule out while it
     was one column at every width.

     **The discontinuity here is much larger than it was, and the photograph is
     all of it.** Measured across the boundary, last two-column pixel to first
     stacked one:

       897px  cols 471 + 283   hero  781   headline 3L/144   image 283x240
       896px  cols 806         hero 1737   headline 2L/96    image 806x1005

     The gap between them is 956px, and 467 of it is the picture: the file in this
     folder is an 822x1024 portrait rather than the 1248x832 landscape the markup
     declares, so in the stacked layout its box is the column's full 806px wide and
     1005px tall — a 1.5 file there would come to 538. See the HeroImage2.jpg entry
     in VERSION: that is a defect, not a design. The rest of the gap is the headline
     losing a line and the copy above it moving.

     The hero gains 956px by losing a column, and 765 of it is the picture: the
     wrapper's height is the headline column's in the two-column layout, so it
     enters the stacked one at 240px and leaves at 1005. The rest is the lede and the headline reflowing — the
     headline drops to two lines because it has the shell's full width, and the
     lede gains nothing back because it spanned already and was at 807px either
     side of the boundary.

     The CTA pays for it: 797 above the line, 1773 below it, so on a 950 fold the
     button is 976px further down than it was. That is a mobile number and mobile
     folds are shorter than 950, so it is not the constraint the desktop fold is —
     but the picture being 1005px tall on a phone is worth a look on its own, and
     capping it is not the free change it sounds like. A cap means `object-fit:
     cover` cropping the picture again rather than fitting it, and against the
     landscape asset — the one with a caption painted into it — the crop at that
     ratio is the height, which is the direction the caption cannot survive;
     keeping it would take `object-position: left top`. Against the portrait file
     that is here there is no caption to lose, and the reason not to cap it is the
     simpler one above: the box is the wrong shape before anything is done to it.
     Left alone rather than guessed at.

     The old note here was about an empty 151px column at this breakpoint reading
     as a wide right margin. There is no empty column any more — the track holds
     the photograph — so that concern is gone with it. */
  .contact__grid,
  .hero__grid,
  .case,
  .case--flip { grid-template-columns: 1fr; }

  /* The hero is stated as a layout in the two-column rules above, so all six
     items have to be given their rows back here. `grid-column: auto` alone is not
     enough: a leftover `grid-row: 2` in a one-column grid still asks the grid to
     place that item in row 2 and leaves row 1 empty, so the hero would come out
     with a hole in it wherever the declaration was missed.

     The photograph is the one that is not a reset. Its wrapper has no height of
     its own — it holds an absolutely positioned child and nothing else — and in
     the two-column layout it gets its height from the row span it was given.
     Stacked, that span is gone, so the picture goes back into the flow and takes
     its natural height: the wrapper becomes an ordinary box again and the image
     fills it the usual way. Left as it was it would collapse to zero and the
     photograph would disappear. */
  .hero__intro,
  .hero__title,
  .hero__subhead,
  .hero__image-wrap,
  .hero__lede,
  .hero__creds { grid-column: auto; grid-row: auto; }

  /* Mobile: the portrait file would otherwise stand 1024/822 of the shell's
     width tall — about 436px at a 390px viewport, which is taller than the
     headline it sits under. Capping the wrapper to the asset's authored 3:2
     ratio (the 1248x832 the markup declares) and letting the picture cover-crop
     into it reproduces the landscape format it has beside the headline on
     desktop. `height: 100%` on the now-static image gives `object-fit: cover` a
     box to crop into; `height: auto` made the picture overflow the capped
     wrapper and get clipped instead of cropped. The margin is the breathing
     room between the photograph and the "20 Years Building..." line above it in
     the stacked order. */
  .hero__image-wrap {
    align-self: auto;
    aspect-ratio: 3 / 2;
    margin-top: var(--space-4);
  }
  .hero__image { position: static; inset: auto; width: 100%; height: 100%; }

  .case--flip .case__body { order: 1; }
  .case--flip .case__figure { order: 2; }
  .case__figure { max-width: 20rem; margin-inline: 0; }

  .spec__row,
  .timeline__item { grid-template-columns: 1fr; gap: var(--space-4); }

  .contact__line { grid-template-columns: 1fr; gap: var(--space-1); }
  .footer__links { margin-left: 0; }
}

@media (max-width: 34rem) {
  .form__row { grid-template-columns: 1fr; }

  /* The brand's descriptor goes, and the arithmetic is the reason rather than a
     taste for brevity. With it the brand is 368.9px wide and `flex: none`, so it
     cannot shrink; with the 16px row gap and the 46px toggle the header's minimum
     content is 430.9px. Measured by forcing the descriptor back on below this
     breakpoint, the shell's content box falls under that at about 478px and the
     page gains a horizontal scrollbar from there down — 71px of overflow at 380px,
     51 at 400, 32 at 420.

     Dropping the descriptor takes the header's minimum to 259.6px, which clears
     even 320px with 60px to spare. Almost all of the brand's width is the
     descriptor: it measures 285.6px against `ExpertTechPilot`'s 114.2px. And the
     breakpoint fires at 544px, where the header still has about 60px of slack
     before the descriptor even becomes tight — early rather than late, which is
     the direction to be wrong in.

     Hidden rather than reflowed to two lines, and the reason is that it does not
     reflow at all: the brand is `flex: none` and the lockup is a grid, so the
     track sizes to the text's 285.6px max-content and the row overflows instead of
     wrapping. Measured down to 320px the descriptor holds one line and pushes the
     page wide. Wrapping it would mean giving the lockup a max-width and letting
     the brand shrink, and the break would then land wherever the width happened to
     fall — a subtitle that splits mid-phrase on some phones and not others is
     worse than no subtitle. The name is not lost — it is in the <title>, the H1
     and the footer. */
  .header__tagline { display: none; }

  /* Each button takes the whole line here. `1 0 100%` and not `1`, because the
     basis is what decides the line break: at a zero basis a button would be
     small enough to join the credentials' line and then grow into it, leaving
     the credential name in whatever width was left over.

     The group takes the full width as well, so the two stacked buttons share one
     right edge rather than each finding its own — `justify-content: flex-end` on
     the group would otherwise leave the shorter of the two floating. */
  .hero__actions { flex: 1 0 100%; }
  .hero__actions > .btn { flex: 1 0 100%; }
  .chat { right: var(--space-4); bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px)); }
  /* Near-full-screen and tighter: at 390px the desktop spacing left the form
     ~50px too tall, so it opened already scrolled and the submit button sat
     below the fold. Gutters match at --space-4 on both sides. */
  .chat__panel {
    width: min(23rem, calc(100vw - 2 * var(--space-4)));
    max-height: calc(100vh - 5rem);
    max-height: calc(100dvh - 5rem);
    padding: var(--space-5);
  }
  .chat__form { gap: var(--space-3); }
  .chat__form textarea { min-height: 4.5rem; }

}

/* ── Reduced motion ───────────────────────────────────────────────────── */
@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;
  }

  /* State clarity is preserved with opacity and instant changes — never by
     removing feedback altogether. */
  .reveal { opacity: 1; transform: none; }
  .btn:hover .btn__icon { transform: none; }
  .btn:active { transform: none; }
}

/* ── Service guides ─────────────────────────────────────────────────────
   Five long-form service pages share one reading layout: a breadcrumb, a
   two-column opening with the H1 and an "at a glance" record, a sticky
   contents rail, the article, related services, and a closing CTA.

   The layout is built from the same tokens, rules, and type scale as the
   rest of the site. No new palette, no new radius, no new shadow — a service
   page should read as another page of this site, not a landing-page template
   dropped into it. */

/* ── Homepage service index ─────────────────────────────────────────────
   Sits between the engagements and the contact section. A ruled list, not a
   card grid, so it extends the ledger language of the offers above it rather
   than competing with them. */
.service-index { --section-pad: var(--space-10); }
.service-index__list { border-top: var(--rule) solid var(--border-strong); }
.service-index__item { border-bottom: var(--rule) solid var(--border); }
.service-index__item > a {
  display: grid;
  grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr) auto;
  gap: var(--space-5) var(--space-6);
  align-items: center;
  padding-block: var(--space-5);
  color: inherit;
  text-decoration: none;
}
.service-index__name {
  font-family: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: 1.2;
  color: var(--text);
  transition: color var(--dur) var(--ease);
}
.service-index__desc {
  font-size: var(--step--1);
  line-height: 1.55;
  color: var(--text-soft);
}
.service-index__arrow {
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border: var(--rule) solid var(--border-strong);
  border-radius: var(--radius-pill);
  color: var(--accent-text);
  transition: background-color var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              transform var(--dur) var(--ease);
}
.service-index__arrow svg { width: 0.9rem; height: 0.9rem; display: block; }
.service-index__item > a:hover .service-index__name { color: var(--accent-text); }
.service-index__item > a:hover .service-index__arrow {
  background: color-mix(in oklab, var(--accent) 12%, transparent);
  border-color: var(--accent);
  transform: translate(2px, -2px);
}
.header__menu-panel a[aria-current="page"] {
  color: var(--text);
  font-weight: 600;
}

/* ── Breadcrumb ────────────────────────────────────────────────────────── */
.breadcrumb { margin-bottom: var(--space-7); }
.breadcrumb ol {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--step--1);
  color: var(--text-faint);
}
.breadcrumb li { display: flex; align-items: center; gap: var(--space-2); }
.breadcrumb li + li::before {
  content: "/";
  color: var(--text-faint);
  opacity: 0.6;
}
.breadcrumb a {
  color: var(--text-soft);
  text-decoration: none;
  transition: color var(--dur) var(--ease);
}
.breadcrumb a:hover { color: var(--accent-text); }
.breadcrumb [aria-current="page"] { color: var(--text); }

/* ── Service hero ──────────────────────────────────────────────────────── */
.service-hero { --section-pad: clamp(2rem, 3vw + 1rem, 3.75rem); }
.service-hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(15rem, 0.6fr);
  gap: clamp(2.25rem, 6vw, 5rem);
  align-items: end;
}
.service-hero__title {
  max-width: 18ch;
  font-size: clamp(2.35rem, 1.65rem + 2.4vw, 3.8rem);
  line-height: 1.03;
  letter-spacing: -0.034em;
  text-wrap: balance;
}
.service-hero__lede {
  max-width: 48ch;
  margin-top: var(--space-6);
  font-size: var(--step-1);
  line-height: 1.6;
  color: var(--text-soft);
}
.service-hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-7);
}
.service-hero__note {
  max-width: 48ch;
  margin-top: var(--space-5);
  font-size: var(--step--1);
  line-height: 1.55;
  color: var(--text-faint);
}
.service-hero__aside {
  padding-top: var(--space-5);
  border-top: 2px solid var(--accent);
}
.service-aside__title {
  margin-bottom: var(--space-4);
  font-size: var(--step--2);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent-text);
}
.service-facts { display: grid; gap: var(--space-4); margin: 0; }
.service-facts > div {
  padding-top: var(--space-4);
  border-top: var(--rule) solid var(--border);
}
.service-facts > div:first-child { padding-top: 0; border-top: 0; }
.service-facts dt {
  font-size: var(--step--2);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.service-facts dd {
  margin: var(--space-1) 0 0;
  font-size: var(--step--1);
  line-height: 1.55;
  color: var(--text-soft);
}

/* ── Article layout ────────────────────────────────────────────────────── */
.service-detail { --section-pad: clamp(3rem, 5vw + 1rem, 5.5rem); }
.service-layout {
  display: grid;
  grid-template-columns: 13rem minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: start;
}
.service-toc {
  position: sticky;
  top: 6.5rem;
  padding-top: var(--space-4);
  border-top: var(--rule) solid var(--border-strong);
}
.service-toc__label {
  font-size: var(--step--2);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.service-toc ul {
  display: grid;
  gap: var(--space-1);
  margin: var(--space-4) 0 0;
  padding: 0;
  list-style: none;
}
.service-toc ul a {
  display: block;
  padding: 0.2rem 0 0.2rem var(--space-3);
  border-left: 2px solid transparent;
  color: var(--text-soft);
  text-decoration: none;
  font-size: var(--step--1);
  line-height: 1.4;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.service-toc ul a:hover,
.service-toc ul a:focus-visible {
  border-left-color: var(--accent);
  color: var(--accent-text);
}
.service-toc__contact {
  margin-top: var(--space-6);
  padding-top: var(--space-4);
  border-top: var(--rule) solid var(--border);
  font-size: var(--step--1);
  line-height: 1.55;
  color: var(--text-soft);
}
.service-toc__contact a { color: var(--accent-text); }

.service-content { max-width: 68ch; }
.service-content > .service-intro {
  margin: 0;
  max-width: 64ch;
  font-size: var(--step-1);
  line-height: 1.65;
  color: var(--text);
}
.service-intro + h2 { margin-top: var(--space-7); }
.service-content > h2 {
  margin-top: var(--space-9);
  padding-top: var(--space-6);
  border-top: var(--rule) solid var(--border);
  font-size: clamp(1.6rem, 1.25rem + 1.3vw, 2.2rem);
  line-height: 1.18;
  letter-spacing: -0.02em;
  text-wrap: balance;
}
.service-content > h2:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.service-content h3 {
  margin-top: var(--space-7);
  font-size: var(--step-1);
  line-height: 1.3;
  letter-spacing: -0.012em;
}
.service-content p {
  margin-top: var(--space-4);
  line-height: 1.7;
  color: var(--text-soft);
}
.service-content h2 + p,
.service-content h3 + p { margin-top: var(--space-3); }
.service-content strong { color: var(--text); font-weight: 600; }
.service-content ul,
.service-content ol {
  display: grid;
  gap: var(--space-3);
  margin: var(--space-4) 0 0;
  padding: 0;
  list-style: none;
}
.service-content li {
  position: relative;
  padding-left: var(--space-5);
  line-height: 1.65;
  color: var(--text-soft);
}
.service-content ul > li::before {
  content: "";
  position: absolute;
  top: 0.7em;
  left: 0.15rem;
  width: 0.3rem;
  height: 0.3rem;
  border-radius: 50%;
  background: var(--accent);
}
.service-content ol { counter-reset: service-step; gap: 0; }
.service-content ol > li {
  counter-increment: service-step;
  padding: var(--space-4) 0 var(--space-4) var(--space-7);
  border-top: var(--rule) solid var(--border);
}
.service-content ol > li:last-child { border-bottom: var(--rule) solid var(--border); }
.service-content ol > li::before {
  content: counter(service-step, decimal-leading-zero);
  position: absolute;
  top: var(--space-4);
  left: 0;
  font-family: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--accent-text);
}
.service-content a { font-weight: 500; }
.service-content h2,
.service-content h3 { scroll-margin-top: 6.5rem; }

/* Evidence note: the one place on a service page where the limits of a claim
   are spelled out in the open. Deliberately quiet — a margin note, not a
   warning banner. */
.service-note {
  margin-top: var(--space-6);
  padding: var(--space-5);
  background: var(--bg-raised);
  border-left: 2px solid var(--accent);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.service-note p {
  margin: 0;
  font-size: var(--step--1);
  line-height: 1.6;
}
.service-note p + p { margin-top: var(--space-3); }
.service-note__label {
  display: block;
  margin-bottom: var(--space-2);
  font-size: var(--step--2);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent-text);
}

/* ── On-page questions ─────────────────────────────────────────────────── */
.service-faq {
  margin-top: var(--space-5);
  border-top: var(--rule) solid var(--border);
}
.service-faq details { border-bottom: var(--rule) solid var(--border); }
.service-faq summary {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  padding-block: var(--space-4);
  list-style: none;
  cursor: pointer;
}
.service-faq summary::-webkit-details-marker { display: none; }
.service-faq summary h3 {
  flex: 1;
  margin: 0;
  font-size: var(--step-0);
  line-height: 1.45;
  color: var(--text);
}
.service-faq details[open] .service__caret { transform: rotate(180deg); }
.service-faq__body {
  max-width: 64ch;
  padding-bottom: var(--space-5);
}
.service-faq__body p {
  margin: 0;
  font-size: var(--step--1);
  line-height: 1.65;
  color: var(--text-soft);
}
.service-faq__body p + p { margin-top: var(--space-3); }

/* ── Related services ──────────────────────────────────────────────────── */
.service-related { --section-pad: var(--space-10); }
.service-related__list { border-top: var(--rule) solid var(--border-strong); }
.service-related__item { border-bottom: var(--rule) solid var(--border); }
.service-related__item > a {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr) auto;
  gap: var(--space-5);
  align-items: center;
  padding-block: var(--space-5);
  color: inherit;
  text-decoration: none;
}
.service-related__name {
  font-family: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: 1.2;
  color: var(--text);
  transition: color var(--dur) var(--ease);
}
.service-related__desc {
  font-size: var(--step--1);
  line-height: 1.55;
  color: var(--text-soft);
}
.service-related__arrow {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  color: var(--accent-text);
  transition: transform var(--dur) var(--ease);
}
.service-related__arrow svg { width: 1rem; height: 1rem; display: block; }
.service-related__item > a:hover .service-related__name { color: var(--accent-text); }
.service-related__item > a:hover .service-related__arrow { transform: translate(2px, -2px); }

/* ── Closing CTA ───────────────────────────────────────────────────────── */
.service-cta { --section-pad: clamp(3rem, 4vw + 1rem, 4.5rem); }
.service-cta__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: end;
}
.service-cta__title {
  max-width: 20ch;
  font-size: clamp(1.85rem, 1.35rem + 2vw, 2.9rem);
  line-height: 1.08;
  letter-spacing: -0.028em;
  text-wrap: balance;
}
.service-cta__lede {
  max-width: 50ch;
  margin-top: var(--space-4);
  line-height: 1.6;
  color: var(--text-soft);
}
.service-cta__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  justify-content: flex-end;
}
.service-cta__direct {
  margin-top: var(--space-4);
  font-size: var(--step--1);
  line-height: 1.6;
  color: var(--text-faint);
  text-align: right;
}
.service-cta__direct a { color: var(--text-soft); }

/* ── Case study ──────────────────────────────────────────────────────────
   One long-form case study reuses the service reading layout above. The few
   additions are the parts a case study needs and a service page does not: a
   result trio, a ruled data table, a short process sequence, a note, and the
   research citation. They are built from the same tokens and type scale, so
   the page reads as another document on this site rather than a template. */

.case-metrics {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-4);
  margin-top: var(--space-6);
  padding-top: var(--space-5);
  border-top: 2px solid var(--accent);
}
.case-metric__figure {
  margin-top: 0;
  font-family: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: clamp(1.75rem, 1.25rem + 1.7vw, 2.75rem);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.case-metric__unit {
  display: block;
  margin-top: var(--space-2);
  font-size: var(--step--2);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-text);
}
.case-metric__label {
  margin-top: var(--space-3);
  font-size: var(--step--1);
  line-height: 1.5;
  color: var(--text-soft);
}
/* The article's generic paragraph rule is more specific than a bare class,
   so the metric and paper blocks scope their own overrides. */
.case-metrics .case-metric__figure { color: var(--text); }
.case-metrics .case-metric__unit { color: var(--accent-text); }
.case-metrics .case-metric__label { color: var(--text-soft); }

.case-table-wrap {
  margin-top: var(--space-5);
  overflow-x: auto;
  border: var(--rule) solid var(--border);
  border-radius: var(--radius);
}
.case-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--step--1);
  line-height: 1.5;
}
.case-table th,
.case-table td {
  padding: var(--space-3) var(--space-4);
  text-align: left;
  vertical-align: top;
  border-bottom: var(--rule) solid var(--border);
}
.case-table thead th {
  background: var(--bg-raised);
  font-size: var(--step--2);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.case-table tbody th {
  color: var(--text);
  font-weight: 600;
}
.case-table tbody tr:last-child th,
.case-table tbody tr:last-child td { border-bottom: 0; }
.case-table__num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.case-table--wide { min-width: 30rem; }

/* The four steps are a real sequence, so the numbers carry order rather than
   decoration. The rules below outrank the generic article list styles and
   turn the ordered list into a row of connected steps. */
.service-content ol.case-flow {
  counter-reset: none;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
  margin-top: var(--space-5);
}
.service-content ol.case-flow > li {
  counter-increment: none;
  padding: var(--space-4);
  border: var(--rule) solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-raised);
  font-size: var(--step--1);
  line-height: 1.5;
}
.service-content ol.case-flow > li:last-child {
  border-bottom: var(--rule) solid var(--border);
}
.service-content ol.case-flow > li::before {
  content: attr(data-step);
  position: static;
  display: block;
  margin-bottom: var(--space-2);
  font-size: var(--step--2);
  font-weight: 600;
  letter-spacing: 0.1em;
  color: var(--accent-text);
}

.case-note {
  margin-top: var(--space-6);
  padding: var(--space-5);
  border-left: 3px solid var(--accent);
  border-radius: 0 var(--radius) var(--radius) 0;
  background: color-mix(in oklab, var(--accent) 6%, var(--bg-raised));
  font-size: var(--step--1);
  line-height: 1.65;
  color: var(--text-soft);
}
.case-note p { margin: 0; }
.case-note p + p { margin-top: var(--space-3); }
.case-note strong { color: var(--text); }

.case-paper {
  margin-top: var(--space-5);
  padding: var(--space-5);
  border: var(--rule) solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-raised);
}
.case-paper__label {
  margin-top: 0;
  font-size: var(--step--2);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-text);
}
.case-paper__title {
  margin-top: var(--space-2);
  font-family: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: var(--step-1);
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: -0.015em;
  color: var(--text);
}
.case-paper__meta {
  margin-top: var(--space-3);
  font-size: var(--step--1);
  line-height: 1.6;
  color: var(--text-soft);
}
.case-paper .case-paper__label { color: var(--accent-text); }
.case-paper .case-paper__title { color: var(--text); }

.case-figure { margin: var(--space-6) 0 0; }
.case-figure img {
  display: block;
  width: 100%;
  height: auto;
  border: var(--rule) solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-raised);
}
.case-figure figcaption {
  margin-top: var(--space-3);
  font-size: var(--step--2);
  line-height: 1.55;
  color: var(--text-faint);
}

.case-client {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
  padding-bottom: var(--space-5);
  border-bottom: var(--rule) solid var(--border);
}
.case-client__logo {
  display: inline-flex;
  align-items: center;
  min-height: 2rem;
  max-width: 100%;
}
/* Client logos arrive in different shapes — a square-ish monogram, a wide
   vendor wordmark, a name set in the client's own type. All three settle to
   2rem tall and take whatever width that needs, up to the column. A logo
   supplied only as white artwork for dark backgrounds (data-tone="dark")
   sits on a slate plate so it stays legible on paper. The plate belongs to
   the image, not to the mark: when the file is missing the typographic
   fallback stands alone on paper rather than dark-on-dark. */
.case-client__logo[data-tone="dark"][data-state="ready"] {
  background: var(--slate-dark);
  border-radius: 0.25rem;
  padding: 0.25rem 0.45rem;
}
.case-client__logo[data-tone="dark"][data-state="ready"] img { height: 1.5rem; }
.case-client__wordmark {
  font-family: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: 1.125rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.2;
  color: var(--text);
}
.case-client__logo img {
  display: none;
  width: auto;
  height: 2rem;
  max-width: 100%;
  object-fit: contain;
}
.case-client__logo[data-state="ready"] img { display: block; }
.case-client__logo[data-state="ready"] .case-client__wordmark { display: none; }
.case-client__label {
  font-size: var(--step--2);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  line-height: 1.4;
  color: var(--text-faint);
}

@media (max-width: 56rem) {
  .case-metrics { grid-template-columns: minmax(0, 1fr); }
  .case-metric__figure { font-size: var(--step-4); }
}

@media (max-width: 56rem) {
  .service-index__item > a {
    grid-template-columns: minmax(0, 1fr) auto;
  }
  .service-index__desc { grid-column: 1 / -1; grid-row: 2; }
  .service-hero__grid { grid-template-columns: minmax(0, 1fr); }
  .service-hero__aside { max-width: 34rem; }
  .service-layout { grid-template-columns: minmax(0, 1fr); }
  .service-toc { display: none; }
  .service-related__item > a {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--space-2) var(--space-5);
  }
  .service-related__desc { grid-column: 1 / -1; grid-row: 2; }
  .service-cta__inner { grid-template-columns: minmax(0, 1fr); }
  .service-cta__actions { justify-content: flex-start; }
  .service-cta__direct { text-align: left; }
}

@media (max-width: 34rem) {
  .breadcrumb { margin-bottom: var(--space-6); }
  .service-content ol.case-flow { grid-template-columns: minmax(0, 1fr); }
  .case-table--wide { min-width: 0; }
  .case-table th,
  .case-table td { padding: var(--space-2) 0.625rem; }
  .case-table__num { white-space: nowrap; }
  .service-hero__actions > .btn { flex: 1 1 100%; justify-content: space-between; }
  .service-cta__actions > .btn { flex: 1 1 100%; justify-content: space-between; }
  .service-index__name,
  .service-related__name { font-size: var(--step-0); }
}
