/* Hallmark · genre: editorial · macrostructure: Stat-Led (landing),
 * Long Document (index + document), Workbench (tool)
 * theme: studied-DNA "Signal, light" · design-system: design.md · designed-as-app
 * DNA-source: url · studied: yes
 *   maggieappleton.com (temperature: warm light paper, serif body)
 *   c82.net           (strategy: chrome recedes, the work carries the colour)
 * paper oklch(98.0% 0.006 85) · accent oklch(51.5% 0.195 276)
 * fonts: Inter (structure) + Source Serif 4 (body) + system mono (data only)
 * axes: light paper / display-sans / cool accent (indigo 276)
 */

/* ============================================================
   James H. Pratt: site stylesheet
   v2, light. Origin: Personal Brand System tokens v1.0 (2026-05-24),
   redirected by two studied references. Direction: "Signal"

   design.md at the repo root is the source of truth for this system.
   Read it before changing colour, type, spacing, motion or CTA voice.
   ============================================================ */

/* ---------- 1. Fonts (self-hosted, no third-party CDN) ---------- */

@font-face {
  font-family: 'Inter';
  src: url('/assets/fonts/inter-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Inter';
  src: url('/assets/fonts/inter-500.woff2') format('woff2');
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Inter';
  src: url('/assets/fonts/inter-600.woff2') format('woff2');
  font-weight: 600; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Inter';
  src: url('/assets/fonts/inter-700.woff2') format('woff2');
  font-weight: 700; font-style: normal; font-display: swap;
}

/* Source Serif 4 is variable on the wght axis, so one file per style covers
   the whole range. Body sits at 400, strong at 600, both from the roman file.
   OFL 1.1, latin subset, sourced from Google Fonts. */
@font-face {
  font-family: 'Source Serif 4';
  src: url('/assets/fonts/source-serif-4-var.woff2') format('woff2');
  font-weight: 200 900; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Source Serif 4';
  src: url('/assets/fonts/source-serif-4-var-italic.woff2') format('woff2');
  font-weight: 200 900; font-style: italic; font-display: swap;
}

/* ---------- 2. Tokens ---------- */

:root {
  /* Sans carries structure, serif carries reading. Inverted from v1, and the
     main reason this no longer reads like a monitoring dashboard.
     Source Serif 4 is self-hosted; the stack behind it is the fallback
     if the woff2 fails to load. */
  --font-display: 'Inter', 'SF Pro Display', system-ui, sans-serif;
  --font-ui:      'Inter', 'SF Pro Text', system-ui, sans-serif;
  --font-body:    'Source Serif 4', 'Iowan Old Style', 'Palatino Linotype',
                  Palatino, Georgia, serif;
  --font-mono:    ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, monospace;

  /* Warm off-white, never #fff: pure white reads flat and synthetic. Every
     neutral below sits at hue 75-85, the warm side. That warmth is the point
     of v2 and is the thing to preserve if this palette is regenerated.
     Token names are v1's; design.md carries the name mapping. */
  --bg:            #faf8f4;   /* paper */
  --bg-subtle:     #f3f0ea;   /* raised, 1.07:1 */
  --bg-muted:      #e1dace;   /* sunken, 1.31:1 */
  --bg-overlay:    #ffffff;   /* overlays lift OFF the paper on light */

  --border:        #e0dad0;   /* decorative hairline, never bounds a control */
  --border-muted:  #ece7dd;
  --border-strong: #948977;   /* 3.24:1, bounds interactive controls per 1.4.11 */

  --text:          #26231f;   /* 14.75:1, headings */
  --text-2:        #46413a;   /*  9.53:1, body copy */
  --text-3:        #6d665c;   /*  5.34:1, muted labels and separators */
  --text-inv:      #faf8f4;

  --accent:        #4f52d4;   /* 5.67:1. The value v1's print block already used,
                                 so the brand hue survives the direction change. */
  --accent-solid:  #4f52d4;   /* filled controls: white on this is 6.02:1 */
  --accent-text:   #4341c4;   /* 7.09:1, indigo that is text */
  --accent-hover:  #3a37b0;   /* 8.41:1, hover goes darker on light */
  --accent-subtle: rgba(79, 82, 212, 0.07);
  --accent-border: rgba(79, 82, 212, 0.28);

  /* Status hues, re-derived for light paper. The v1 values (#22c55e, #38bdf8)
     measured 2.15:1 and 2.02:1 here, effectively invisible. */
  --ok:            #15803d;   /* 4.73:1, granted / active / in force */
  --ok-soft:       rgba(21, 128, 61, 0.14);
  --info:          #0369a1;   /* 5.59:1, reassigned to another owner */
  --on-accent:     #ffffff;   /* 6.02:1 on --accent-solid */

  --s1: .25rem;  --s2: .5rem;   --s3: .75rem;  --s4: 1rem;
  --s5: 1.25rem; --s6: 1.5rem;  --s8: 2rem;    --s10: 2.5rem;
  --s12: 3rem;   --s16: 4rem;   --s20: 5rem;   --s24: 6rem;
  --s32: 8rem;

  /* Type scale. Every size on the site resolves to one of these steps, so the
     whole page can be retuned from here instead of 39 scattered declarations.
     11px is the floor. Nothing renders smaller. */
  --text-2xs:   .6875rem;   /* 11px, the floor */
  --text-xs:    .75rem;     /* 12px */
  --text-sm:    .8125rem;   /* 13px */
  --text-md:    .875rem;    /* 14px */
  --text-base:  .9375rem;   /* 15px */
  --text-lg:    1rem;       /* 16px */
  --text-xl:    1.0625rem;  /* 17px */
  --text-2xl:   1.125rem;   /* 18px */
  --text-3xl:   1.25rem;    /* 20px */
  --text-4xl:   1.375rem;   /* 22px */
  --text-5xl:   1.5rem;     /* 24px */

  /* Fluid steps, named by role rather than size. */
  --text-lede:       clamp(1.0625rem, 1.6vw, 1.3125rem);
  --text-lede-hero:  clamp(1.0625rem, 1.5vw, 1.25rem);
  --text-quote:      clamp(1.5rem, 3.2vw, 2.25rem);
  --text-post-h2:    clamp(1.5rem, 3vw, 2rem);
  --text-stat:       clamp(1.75rem, 3.6vw, 2.75rem);
  --text-page-h1:    clamp(2.25rem, 5.6vw, 4rem);
  --text-display-s:  clamp(2.25rem, 6.5vw, 4.25rem);
  --text-display:    clamp(3rem, 11vw, 6rem);
  --text-display-sm: clamp(2.75rem, 13vw, 4rem);   /* hero, below 680px */

  --r-sm: 4px; --r-md: 8px; --r-lg: 12px; --r-xl: 16px; --r-full: 9999px;

  --content: 1080px;
  --narrow: 720px;

  --t-fast: 100ms cubic-bezier(.4, 0, .2, 1);
  --t-base: 180ms cubic-bezier(.4, 0, .2, 1);
  --t-slow: 320ms cubic-bezier(.4, 0, .2, 1);

  /* the signal rail width, referenced by layout and by the rail itself */
  --rail: 88px;
}

/* ---------- 3. Reset ---------- */

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

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

/* clip, not hidden: overflow-x:hidden on body promotes it to a scroll
   container, which is a known way to break position:sticky descendants
   (the masthead). clip suppresses the same overflow without that effect. */
html, body { overflow-x: clip; }

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--text-lg);
  font-weight: 400;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-feature-settings: 'cv05' 1, 'cv11' 1, 'ss01' 1;
}

img, svg { display: block; max-width: 100%; }
img { height: auto; }
ul, ol { list-style: none; }

a {
  color: inherit;
  text-decoration: none;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

/* Off-screen until focused. Without it a keyboard visitor tabs the wordmark,
   the menu toggle, five nav links and the booking button before reaching the
   content, on every page. */
.skip-link {
  position: absolute;
  top: var(--s4);
  left: var(--s4);
  z-index: 100;
  transform: translateY(-250%);
  background: var(--bg);
  color: var(--accent-text);
  font-family: var(--font-ui);
  font-size: var(--text-md);
  font-weight: 500;
  padding: var(--s3) var(--s5);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  transition: transform var(--t-fast);
}
.skip-link:focus { transform: translateY(0); }

/* --accent-solid, not --accent: white on --accent is 4.47:1 and fails AA.
   Every other filled control already uses --accent-solid; this was the gap. */
::selection { background: var(--accent-solid); color: var(--on-accent); }

/* ---------- 4. Type primitives ---------- */

.display {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: .92;
  letter-spacing: -0.03em;
}

.mono {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .12em;
  text-transform: uppercase;
  font-weight: 500;
}

.lede {
  font-size: var(--text-lede);
  line-height: 1.6;
  color: var(--text-2);
  max-width: 42ch;
}

/* Document-scale heading, for a prose block that needs a title above it.
   Named rather than repeated inline so the treatment has exactly one home. */
.title-lg {
  font-family: var(--font-ui);
  font-size: var(--text-5xl);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.25;
  margin-bottom: var(--s4);
}

/* Mono label introducing a data list. Composes with .mono, which already
   carries the face, size, tracking and case. Recurs on the landing, about and
   CV pages, so it is named rather than repeated inline five times. */
.dl-head {
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-bottom: var(--s5);
}
.dl + .dl-head { margin-top: var(--s10); }   /* stacked lists in one column */
.dl-head--break { margin-top: var(--s12); }  /* label opening a new block */

/* The homepage record, drawn. design.md's v2 thesis is that the record is an
   artifact rather than a table, and /patents/ and /research/ got that treatment
   while the landing page kept two definition lists. This is the third drawn
   record, so it takes a new positional encoding: a continuous cumulative line,
   not the year columns /patents/ owns or the lanes /research/ owns.

   No entrance animation on purpose. The stat rail is the page's one
   orchestrated entrance and design.md allows exactly one. */
.record-curve { margin: 0 0 var(--s10); }
.record-curve svg {
  display: block;
  width: 100%;
  height: clamp(72px, 9vw, 104px);
  overflow: visible;
}
.rc-line { fill: none; stroke: var(--accent); stroke-width: 1.5; stroke-linejoin: round; }
.rc-line--partial { stroke-dasharray: 3 3; }
.rc-base { stroke: var(--border-strong); stroke-width: 1; }
.rc-val {
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  color: var(--text-3);
  text-align: right;
  margin-bottom: var(--s2);
}
.rc-val strong {
  /* Proportional, not tabular. design.md reserves tabular-nums for columns of
     figures; this is a single stat callout, which stays proportional. */
  font-family: var(--font-mono);
  font-size: var(--text-3xl);
  font-weight: 600;
  color: var(--accent-text);
}
.rc-axis {
  display: flex;
  justify-content: space-between;
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  font-variant-numeric: tabular-nums;
  letter-spacing: .08em;
  color: var(--text-3);
  margin-top: var(--s2);
}
.record-curve .caption { margin-top: var(--s4); }

/* Quiet caption under a figure or data list. Unlike .note it carries no rule. */
.caption {
  margin-top: var(--s6);
  font-size: var(--text-base);
  line-height: 1.6;
  color: var(--text-3);
  max-width: 68ch;
}

/* Spelled-out engagement names under the letter tags on a case-study meta grid. */
.eng-names {
  margin-top: var(--s2);
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--text-3);
}

/* Copy rhythm for a section that runs paragraphs directly inside .shell rather
   than inside .prose. Bare <p> there matched no measure rule at all and rendered
   at 122ch with a 0px gap, which is what made the audit block read as one slab.
   Not .prose itself, because .prose a would restyle the CTA link and button. */
.flow > p { max-width: 68ch; margin-bottom: var(--s5); }
.flow > p.lede { max-width: 42ch; }
.flow > p:last-child { margin-bottom: 0; }

/* The published price is the practice's stated differentiator and it was set as
   another .lede: same serif, same size as the prose around it, wrapping to
   "Larger engagements / are scoped". It is a figure, so it reads as one, using
   the stat rail's vocabulary rather than a new treatment. */
.price {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--s2) var(--s4);
  font-family: var(--font-ui);
  font-size: var(--text-lg);
  color: var(--text-2);
}
.price strong {
  font-size: var(--text-stat);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--text);
}

.prose p { color: var(--text-2); margin-bottom: var(--s5); max-width: 68ch; }
.prose p:last-child { margin-bottom: 0; }
.prose strong { color: var(--text); font-weight: 600; }
.prose a {
  color: var(--text);
  box-shadow: inset 0 -1px 0 var(--accent-border);
  transition: box-shadow var(--t-fast), color var(--t-fast);
}
.prose a:hover { color: var(--accent-hover); box-shadow: inset 0 -1px 0 var(--accent); }

/* ---------- 5. Layout shell ---------- */

.shell {
  max-width: var(--content);
  margin-inline: auto;
  padding-inline: var(--s6);
}

.section {
  padding-block: clamp(var(--s10), 4.5vw, 4.5rem);
  border-top: 1px solid var(--border-muted);
  position: relative;
}
.section:first-of-type { border-top: 0; }

/* Rhythm modifiers. The page is not N identical chapters stacked: the
   reference sections stay compact, and the page opens out toward the close. */
.section--tight { padding-block: clamp(var(--s8), 3vw, var(--s12)); }
.section--open {
  border-top: 0;
  padding-block: clamp(var(--s16), 7vw, var(--s24)) clamp(var(--s10), 4.5vw, 4.5rem);
}

/* Section header: a label on a hairline. The label IS the section heading, so
   it is an <h2>, not a span. No ordinal numbers: these sections are a set, not
   a sequence, and numbering a set is decoration pretending to be structure. */
.sec-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2) var(--s4);
  padding-bottom: var(--s4);
  margin-bottom: var(--s8);
  border-bottom: 1px solid var(--border);
}
/* v2: a real heading, not a 12px mono-cap label. This is the single change
   that stops the page reading as a dashboard. */
.sec-head .label {
  font-family: var(--font-ui);
  font-size: var(--text-3xl);
  letter-spacing: -0.01em;
  color: var(--text);
  font-weight: 600;
  line-height: 1.2;
}
.sec-head .count {
  margin-left: auto;
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  letter-spacing: .02em;
  color: var(--text-3);
}

/* ---------- 6. The signal rail ---------- */

.rail {
  position: fixed;
  top: 0;
  left: 0;
  width: var(--rail);
  height: 100vh;
  z-index: 20;
  pointer-events: none;
  display: grid;
  place-items: center;
}
.rail::after {
  /* hairline the waveform sits on */
  content: '';
  position: absolute;
  inset-block: 0;
  left: calc(var(--rail) / 2);
  width: 1px;
  background: linear-gradient(
    to bottom,
    transparent,
    var(--border) 12%,
    var(--border) 88%,
    transparent
  );
}
.rail svg { position: relative; height: 100%; width: 100%; overflow: visible; }

/* The hero column, filled with evidence rather than texture. Figure 2 of
   US 12,124,506, one of the 358, drawn where the landing page otherwise leaves
   roughly 480px of empty paper beside the name.

   Rectilinear on purpose. The first attempt here was a second wandering line
   opposite the rail, and two organic squiggles read as noise however quiet
   each one is. A patent block diagram is boxes and rules, which is the
   vocabulary the rest of the site already speaks.

   Shipped as an alpha mask, not a picture, so the ink takes --border-strong
   and follows the palette instead of baking in black. Hidden below 1100px,
   where the 42ch lede closes the gap it lives in. */
.hero-patent {
  position: absolute;
  top: 0;
  right: var(--s6);
  width: min(38%, 400px);
  margin: 0;
  z-index: 0;
  display: none;
}
.hero-patent-fig {
  aspect-ratio: 900 / 1104;
  background-color: var(--border-strong);
  opacity: .45;
  -webkit-mask-image: url('/assets/img/us12124506-fig2.png');
          mask-image: url('/assets/img/us12124506-fig2.png');
  -webkit-mask-size: contain;      mask-size: contain;
  -webkit-mask-repeat: no-repeat;  mask-repeat: no-repeat;
  -webkit-mask-position: top center; mask-position: top center;
}
.hero-patent figcaption {
  margin-top: var(--s3);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  font-variant-numeric: tabular-nums;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-3);
  text-align: center;
}
.hero-patent figcaption span { display: block; margin-top: 2px; letter-spacing: .04em; }
.hero .shell { position: relative; }
.hero .shell > *:not(.hero-patent) { position: relative; z-index: 1; }

/* Gated on mask support as well as width. Without a mask this div would paint
   as a solid --border-strong rectangle, which is far worse than an empty
   column, so it stays hidden rather than degrading. */
@supports ((mask-image: url("#m")) or (-webkit-mask-image: url("#m"))) {
  @media (min-width: 1100px) { .hero-patent { display: block; } }
}
/* v2: retuned for light paper. --border-strong is 3.24:1 here, so the base
   trace needs more opacity than it did against the dark ground to read. */
.rail .wave-base { stroke: var(--border-strong); stroke-width: 1; fill: none; opacity: .7; }
.rail .wave-live { stroke: var(--accent); stroke-width: 1.25; fill: none; opacity: .9; }
.rail .playhead { fill: var(--accent); }
.rail .playhead-ring {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1;
  opacity: .35;
}

/* mobile: rail becomes a top scroll line */
.rail-top {
  display: none;
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 2px;
  background: var(--accent);
  transform-origin: 0 50%;
  transform: scaleX(0);
  z-index: 40;
}

/* ---------- 7. Masthead ---------- */

/* A solid masthead, not a frosted floating bar. The hairline is always there
   rather than fading in on scroll: an appearing border is part of the stock
   SaaS-header signature, and the masthead does not react to scroll at all. */
.masthead {
  position: sticky;
  top: 0;
  z-index: 30;
  background: var(--bg);
  border-bottom: 1px solid var(--border-muted);
}

.masthead .shell {
  display: flex;
  align-items: center;
  gap: var(--s6);
  height: 72px;
}

.wordmark {
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  letter-spacing: .16em;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
}
.wordmark .dot {
  display: inline-block;
  width: 5px; height: 5px;
  border-radius: var(--r-full);
  background: var(--accent);
  margin-right: .6em;
  vertical-align: middle;
  margin-top: -2px;
}

/* The nav speaks the site's own label voice (mono caps, as used by the section
   heads, the tags, the footer and the wordmark) rather than the 14px medium
   sans of a stock product bar. No sliding underline: the current page is named
   in the accent, which is how indigo is used everywhere else here. */
.nav { margin-left: auto; display: flex; gap: var(--s5); align-items: center; }
.nav a {
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  letter-spacing: .14em;
  text-transform: uppercase;
  font-weight: 500;
  color: var(--text-3);
  padding-block: var(--s3);
  transition: color var(--t-fast);
}
.nav a:hover,
.nav a:focus-visible { color: var(--text); }
.nav a[aria-current='page'] { color: var(--accent-text); }

.nav-toggle {
  display: none;
  margin-left: auto;
  background: none;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  letter-spacing: .12em;
  text-transform: uppercase;
  padding: var(--s2) var(--s3);
  cursor: pointer;
}

/* ---------- 8. Hero ---------- */

.hero {
  padding-top: clamp(var(--s10), 6vw, 5.5rem);
  padding-bottom: clamp(var(--s8), 4vw, 3.5rem);
}

.eyebrow {
  display: flex;
  align-items: center;
  gap: var(--s3);
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--text-2);
  margin-bottom: var(--s6);
}
.eyebrow .sep { color: var(--text-3); }
.status-dot {
  width: 6px; height: 6px;
  border-radius: var(--r-full);
  background: var(--ok);
  box-shadow: 0 0 0 3px var(--ok-soft);
  flex: none;
}

.hero h1 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-display);
  line-height: .88;
  letter-spacing: -0.035em;
  margin-bottom: var(--s6);
}
.hero h1 .quiet { color: var(--text-3); }

.hero .lede { max-width: 54ch; font-size: var(--text-lede-hero); }
.hero .lede + .lede { margin-top: var(--s5); }

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  margin-top: var(--s8);
}

/* ---------- 9. Buttons ---------- */

.btn {
  font-family: var(--font-ui);
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-size: var(--text-md);
  font-weight: 500;
  padding: var(--s3) var(--s5);
  border-radius: var(--r-md);
  border: 1px solid transparent;
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast), transform var(--t-fast);
  cursor: pointer;
}
.btn .arrow { transition: transform var(--t-base); }
.btn:hover .arrow,
.btn:focus-visible .arrow { transform: translateX(3px); }

.btn-primary { background: var(--accent-solid); color: var(--on-accent); }
.btn-primary:hover { background: var(--accent-hover); }

.btn-ghost { border-color: var(--border-strong); color: var(--text); }
.btn-ghost:hover { border-color: var(--accent-border); background: var(--accent-subtle); }

/* The one filled control in the site chrome. design.md's primary CTA voice at
   chrome scale: same fill, radius and arrow, one step down in padding and type
   so it sits inside the 72px bar without crowding the wordmark at 390px. */
.masthead-cta {
  font-size: var(--text-sm);
  /* Tighter than a page CTA horizontally, which is what buys room for the
     wordmark and menu toggle at 390px. Vertical padding stays full so the
     site's primary conversion control clears a 44px touch target. */
  padding: var(--s3) var(--s4);
  white-space: nowrap;
}

/* ---------- 10. Stat rail ---------- */

.stats {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.stat {
  padding: var(--s5) var(--s5) var(--s5) 0;
  border-right: 1px solid var(--border-muted);
}
.stat:last-child { border-right: 0; }
.stat .n {
  font-family: var(--font-display);
  font-size: var(--text-stat);
  line-height: 1;
  letter-spacing: -0.02em;
  margin-bottom: var(--s2);
}
.stat .k {
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  letter-spacing: .02em;
  color: var(--text-3);
  line-height: 1.5;
}

/* ---------- 11. Practice list (the five engagements) ---------- */

.rows { border-top: 1px solid var(--border-muted); }

/* When a row set is the last thing in its section, drop the closing rule on the
   final row. Otherwise that rule and the next section's border-top sit roughly
   58px apart with nothing between them, and the empty band reads as a blank
   row. Only applies where .rows is the section's last child, so row sets with
   copy after them (the homepage offer and ladder) keep their closing rule. */
.section > .rows:last-child .row:last-child { border-bottom: 0; }

/* Rows are read, not clicked: every .row on this site is a <div> with no href.
   They therefore carry no hover treatment. Decoration that only appears on
   hover is invisible to touch and keyboard, and it implies an affordance that
   is not here. The accent stripe and hover wash live on .work-list .item and
   .card, which are real links. */
.row {
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr) minmax(0, 1.4fr) 132px;
  gap: var(--s6);
  align-items: start;
  padding: var(--s5) 0;
  border-bottom: 1px solid var(--border-muted);
  position: relative;
}

.row .code {
  font-family: var(--font-mono);
  font-size: var(--text-md);
  color: var(--accent-text);
  font-weight: 600;
  padding-top: 3px;
}

/* Rows that carry no gutter code. The A-E letters were internal Portfolio Index
   taxonomy with no key on the page, so they moved to data-code and the column
   they occupied is reclaimed by the title and description. */
.rows--plain .row { grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) 132px; }

/* Two-column variant. A row set whose third column would repeat one word on
   every row is a glossary wearing a ledger's clothes, and it read as the price
   ladder further down the page. The PART factors use this. */
.rows--pair .row { grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr); }

/* Every in-page anchor target clears the sticky masthead. Without this an anchor
   jump lands with its heading tucked under the 72px bar. */
.section[id] { scroll-margin-top: 88px; }
.row .title {
  font-family: var(--font-ui);
  font-size: var(--text-xl);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.35;
}
.row .desc { color: var(--text-2); font-size: var(--text-base); line-height: 1.65; }
.row .meta {
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  text-align: right;
  letter-spacing: .02em;
  color: var(--text-3);
  white-space: nowrap;
  padding-top: 5px;
}

/* ---------- 12. Work cards ---------- */

/* Not three equal tiles. One lead, two supporting: the uneven grid and the
   natural (unlocked) heights are what keep this off the feature-tile shape. */
.work-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
  gap: 1px;
  background: var(--border-muted);
  border: 1px solid var(--border-muted);
}
.card {
  background: var(--bg);
  padding: var(--s6);
  display: flex;
  flex-direction: column;
  gap: var(--s4);
  transition: background var(--t-base);
  position: relative;
}
/* Spans two rows for hierarchy, but its content fills about one, so the bottom
   half read as an empty container rather than deliberate space. Centring turns
   the surplus into padding on both sides of the block. */
.card--lead {
  grid-row: span 2;
  padding: var(--s8) var(--s6);
  justify-content: center;
}
.card:hover, .card:focus-visible { background: var(--bg-subtle); }
.card .tags { display: flex; gap: var(--s2); flex-wrap: wrap; }
.tag {
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--accent-text);
  border: 1px solid var(--accent-border);
  border-radius: var(--r-full);
  padding: 3px 9px;
  line-height: 1.45;
}
.tag.tag-quiet { color: var(--text-3); border-color: var(--border-strong); }
.card h3 {
  font-family: var(--font-ui);
  font-size: var(--text-2xl);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.015em;
}
.card--lead h3 { font-size: var(--text-5xl); line-height: 1.2; letter-spacing: -0.02em; }
.card p { color: var(--text-2); font-size: var(--text-base); line-height: 1.65; }
.card--lead p { font-size: var(--text-lg); }

/* The affordance rides the title, where the eye already is. Three identical
   "Case study ->" footers under three identical cards was the tell. */
.card h3 .arrow {
  display: inline-block;
  margin-left: .3em;
  color: var(--text-3);
  transition: transform var(--t-base), color var(--t-base);
}
.card:hover h3 .arrow,
.card:focus-visible h3 .arrow { transform: translateX(3px); color: var(--accent); }

/* Retained for cards that are not links and carry a plain closing line. */
.card .foot {
  margin-top: auto;
  padding-top: var(--s4);
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-3);
}

@media (max-width: 860px) {
  .work-grid { grid-template-columns: 1fr; }
  .card--lead { grid-row: auto; padding: var(--s6); }
  .card--lead h3 { font-size: var(--text-3xl); }
  .card--lead p { font-size: var(--text-base); }
}

/* ---------- 13. Record (patents / research) ---------- */

.split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: var(--s10);
}

.dl { border-top: 1px solid var(--border-muted); }
.dl div {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s5);
  padding: var(--s4) 0;
  border-bottom: 1px solid var(--border-muted);
}
.dl dt { color: var(--text-2); font-size: var(--text-lg); }
.dl dd {
  font-family: var(--font-mono);
  font-size: var(--text-base);
  color: var(--text);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.link-out {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-size: var(--text-base);
  font-weight: 500;
  color: var(--accent-text);
  margin-top: var(--s6);
}
.link-out .arrow { transition: transform var(--t-base); }
.link-out:hover .arrow,
.link-out:focus-visible .arrow { transform: translateX(3px); }

/* The ask at the record's confidence peak. A full-width hairline separates it
   from both columns: without one it sits directly under "Search the portfolio"
   and reads as a third link in the patents column rather than the section's
   conclusion. Rules carry structure here, so this is the system's own idiom. */
.record-foot {
  margin-top: var(--s10);
  padding-top: var(--s6);
  border-top: 1px solid var(--border);
}
.record-foot .btn { margin-top: 0; }

/* ---------- 14. Now ---------- */

.now {
  display: grid;
  grid-template-columns: minmax(0, .85fr) minmax(0, 1.15fr);
  gap: var(--s12);
  align-items: start;
}
.now-quote {
  font-family: var(--font-body);
  font-size: var(--text-quote);
  line-height: 1.15;
  letter-spacing: -0.02em;
}
.now-quote .accent { color: var(--accent-text); }

/* ---------- 15. Contact ---------- */

.contact h2 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-display-s);
  line-height: .95;
  letter-spacing: -0.03em;
  margin-bottom: var(--s6);
}
.contact-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3) var(--s10);
  margin-top: var(--s10);
  padding-top: var(--s6);
  border-top: 1px solid var(--border-muted);
}
.contact-links a { color: var(--text); font-size: var(--text-lg); transition: color var(--t-fast); }
.contact-links a:hover,
.contact-links a:focus-visible { color: var(--accent-hover); }
.contact-links .k {
  display: block;
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-bottom: 2px;
}

/* ---------- 16. Footer ---------- */

.site-foot {
  border-top: 1px solid var(--border-muted);
  padding-block: var(--s10);
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  letter-spacing: .01em;
  color: var(--text-3);
}
.site-foot .shell { display: flex; flex-wrap: wrap; gap: var(--s4) var(--s8); align-items: center; }
.site-foot a { transition: color var(--t-fast); }
.site-foot a:hover,
.site-foot a:focus-visible { color: var(--text); }
.site-foot .right { margin-left: auto; }

/* ---------- 17. Reveal (progressive enhancement) ---------- */

.js .reveal {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 520ms cubic-bezier(.16, 1, .3, 1), transform 520ms cubic-bezier(.16, 1, .3, 1);
  will-change: opacity, transform;
}
.js .reveal.in { opacity: 1; transform: none; }

/* ---------- 18. Responsive ---------- */

@media (min-width: 1000px) {
  body { padding-left: var(--rail); }
  .masthead .shell, .shell { max-width: var(--content); }
}

@media (max-width: 999px) {
  .rail { display: none; }
  .rail-top { display: block; }
}

@media (max-width: 860px) {
  .row {
    grid-template-columns: 32px minmax(0, 1fr);
    gap: var(--s3) var(--s4);
    row-gap: var(--s2);
  }
  .row .desc { grid-column: 2; }
  .row .meta { grid-column: 2; padding-top: var(--s1); }
  .now { grid-template-columns: 1fr; gap: var(--s8); }
  .stats { grid-template-columns: repeat(2, 1fr); }
  .stat { border-bottom: 1px solid var(--border-muted); padding-right: var(--s4); }
  .stat:nth-child(2n) { border-right: 0; }
  .stat:nth-last-child(-n+2) { border-bottom: 0; }
}

/* The seven-item nav needs 906px of masthead: 134 wordmark, 584 links, 116 CTA
   and the gaps. Below that the CTA is clipped and then the links are, silently,
   because body carries overflow-x: clip. So the toggle takes over at 940, not at
   the 680 the five-item nav could afford. Measured, not guessed: at 834 the CTA
   read as the word "Book" cut by the screen edge, and at 768 it was gone. */
@media (max-width: 940px) {
  .nav { display: none; }
  .nav.is-open {
    display: flex;
    /* top: 100% tracks the masthead's height, which now changes on scroll */
    position: absolute;
    top: 100%; left: 0; right: 0;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    background: var(--bg-subtle);
    border-bottom: 1px solid var(--border);
    padding: var(--s2) var(--s6) var(--s5);
  }
  .nav.is-open a { padding-block: var(--s4); font-size: var(--text-md); width: 100%; }
  .nav-toggle { display: block; }
}

@media (max-width: 680px) {
  .shell { padding-inline: var(--s5); }
  .hero h1 { font-size: var(--text-display-sm); }
  .eyebrow { flex-wrap: wrap; gap: var(--s2); }
}

/* Codeless rows collapse to a single column, same as the coded ones do. */
@media (max-width: 860px) {
  .rows--plain .row { grid-template-columns: minmax(0, 1fr); }
  .rows--plain .row .desc,
  .rows--plain .row .meta { grid-column: 1; }
}

/* The masthead now carries a booking control alongside the wordmark and the
   menu toggle. At 390px those three do not fit on the standard --s6 gap, so the
   chrome tightens and the CTA drops its arrow rather than wrapping the bar. */
@media (max-width: 680px) {
  .masthead .shell { gap: var(--s3); }
  .masthead-cta .arrow { display: none; }
}

/* ---------- 19. Motion and print ---------- */

/* Reduced motion collapses spatial motion and keeps colour. A blanket
   transition kill also removes hover and focus colour feedback, which is
   information rather than decoration, so the property list is narrowed
   instead of the duration being zeroed across the board. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-property: opacity, color, background-color, border-color,
                         box-shadow, outline-color, fill, stroke !important;
    transition-duration: 150ms !important;
  }
  .js .reveal { opacity: 1; transform: none; }
}

@media print {
  /* Print palette. Declared once here and consumed by both print blocks (this
     one and the CV block in section 23). The CV PDF is rendered from /cv/
     through this stylesheet, so these must stay tokens: a raw hex here is a
     value that silently will not follow a brand change into the PDF. */
  :root {
    --p-paper:       #ffffff;
    --p-ink:         #111117;
    --p-body:        #3f3f46;
    --p-mute:        #52525b;
    --p-quiet:       #71717a;
    --p-faint:       #a1a1aa;
    --p-accent:      #4f52d4;
    --p-rule:        #ededea;
    --p-rule-mid:    #e2e2de;
    --p-rule-strong: #c9c9c4;
  }

  body { background: var(--p-paper); color: var(--p-ink); padding-left: 0; }
  .rail, .rail-top, .masthead, .hero-actions, .site-foot, .skip-link { display: none; }
  a { color: var(--p-ink); }
}

/* ============================================================
   20. Interior pages
   ============================================================ */

.page-head {
  padding-top: clamp(var(--s10), 6vw, 5rem);
  padding-bottom: clamp(var(--s8), 4vw, var(--s12));
  border-bottom: 1px solid var(--border);
}
.page-head .kicker {
  display: flex;
  align-items: center;
  gap: var(--s3);
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-bottom: var(--s6);
}
.page-head .kicker .sep { color: var(--text-3); }
.page-head h1 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-page-h1);
  line-height: .98;
  letter-spacing: -0.03em;
  max-width: 20ch;
  margin-bottom: var(--s6);
}
.page-head .lede { max-width: 62ch; }

/* Meta grid on a case study */
.meta-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1px;
  background: var(--border-muted);
  border: 1px solid var(--border-muted);
  margin-top: var(--s10);
}
.meta-grid > div { background: var(--bg); padding: var(--s5); }
.meta-grid dt {
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-bottom: var(--s2);
}
.meta-grid dd { font-size: var(--text-base); color: var(--text); line-height: 1.55; }
.meta-grid dd .tag { margin-right: 4px; }

/* Case study body */
.case-body { padding-block: clamp(var(--s10), 5vw, 4.5rem); }
.case-body h2 {
  font-family: var(--font-ui);
  font-size: var(--text-3xl);
  letter-spacing: -0.01em;
  color: var(--text);
  font-weight: 600;
  line-height: 1.25;
  padding-bottom: var(--s3);
  margin-bottom: var(--s6);
  border-bottom: 1px solid var(--border);
}
.case-body h2:not(:first-child) { margin-top: clamp(var(--s10), 4vw, var(--s16)); }
.case-body p { color: var(--text-2); max-width: 68ch; margin-bottom: var(--s5); font-size: var(--text-xl); }
.case-body p:last-child { margin-bottom: 0; }
.case-body strong { color: var(--text); font-weight: 600; }

.case-body ul { max-width: 68ch; }
.case-body ul li {
  color: var(--text-2);
  font-size: var(--text-xl);
  padding-left: var(--s6);
  position: relative;
  margin-bottom: var(--s4);
}
.case-body ul li::before {
  content: '';
  position: absolute;
  left: 0;
  top: .78em;
  width: 12px;
  height: 1px;
  background: var(--accent);
}
.case-body ul li:last-child { margin-bottom: 0; }

.note {
  border-left: 2px solid var(--border-strong);
  padding: var(--s2) 0 var(--s2) var(--s5);
  color: var(--text-3);
  font-size: var(--text-base);
  max-width: 68ch;
  margin-top: var(--s8);
}
/* A note the reader must not skip. The PART compliance line is what keeps the
   instrument on the unregulated side of EEOC, LL144 and the EU AI Act, and it
   is also the reassurance a cautious buyer is looking for, so it reads at the
   body tier rather than the muted one. Case-study asides stay quiet. */
.note--strong { color: var(--text-2); }

/* Prev / next */
.case-nav {
  display: flex;
  gap: var(--s4);
  justify-content: space-between;
  border-top: 1px solid var(--border);
  padding-top: var(--s6);
  padding-bottom: clamp(var(--s10), 5vw, 4.5rem);
}
.case-nav a { display: block; max-width: 45%; }
.case-nav .k {
  display: block;
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  letter-spacing: .13em;
  color: var(--text-3);
  margin-bottom: var(--s2);
}
.case-nav .t { font-size: var(--text-base); font-weight: 500; color: var(--text-2); transition: color var(--t-fast); }
.case-nav a:hover .t { color: var(--accent-hover); }
.case-nav .next { text-align: right; margin-left: auto; }

/* ============================================================
   21. Work index filter
   ============================================================ */

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  align-items: center;
  padding-block: var(--s6);
  border-bottom: 1px solid var(--border-muted);
}
.filters .flabel {
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-right: var(--s3);
}
.filters button {
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-2);
  background: none;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-full);
  padding: 6px 14px;
  cursor: pointer;
  transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast);
}
.filters button:hover { color: var(--text); border-color: var(--accent-border); }
.filters button[aria-pressed='true'] {
  color: var(--on-accent);
  background: var(--accent-solid);
  border-color: var(--accent-solid);
}
.filters .fcount {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: .1em;
  color: var(--text-3);
}

.work-list { border-top: 0; }
.work-list .item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
  gap: var(--s8);
  padding: var(--s8) 0;
  border-bottom: 1px solid var(--border-muted);
  position: relative;
  transition: background var(--t-base);
}
.work-list .item::before {
  content: '';
  position: absolute;
  left: calc(var(--s4) * -1);
  top: var(--s8);
  bottom: var(--s8);
  width: 2px;
  background: var(--accent);
  transform: scaleY(0);
  transform-origin: 50% 0;
  transition: transform var(--t-base);
}
/* Focus gets exactly what hover gets: these are links, and the affordance must
   not be pointer-only. Solid surface rather than a gradient, because
   transition: background cannot interpolate a gradient and would snap. */
.work-list .item:hover::before,
.work-list .item:focus-visible::before { transform: scaleY(1); }
.work-list .item:hover,
.work-list .item:focus-visible { background: var(--bg-subtle); }
.work-list .item h3 {
  font-family: var(--font-ui);
  font-size: var(--text-4xl);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.25;
  margin-bottom: var(--s3);
}
/* Flex, not block. As inline boxes these pills split into two fragments when a
   label wraps, and the --r-full radius gets sliced open mid-word. That only
   started showing when the A-to-E letters became full engagement names. Same
   rule .card .tags already carries. */
.work-list .item .tags {
  display: flex;
  gap: var(--s2);
  flex-wrap: wrap;
  margin-bottom: var(--s4);
}
.work-list .item p { color: var(--text-2); font-size: var(--text-lg); line-height: 1.65; }
.work-list .item .more {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  margin-top: var(--s4);
  font-size: var(--text-base);
  font-weight: 500;
  color: var(--accent-text);
}
.work-list .item:hover .more .arrow,
.work-list .item:focus-visible .more .arrow { transform: translateX(3px); }
.work-list .item .more .arrow { transition: transform var(--t-base); }
.work-list .item[hidden] { display: none; }

@media (max-width: 860px) {
  .work-list .item { grid-template-columns: 1fr; gap: var(--s4); }
  .case-nav a { max-width: 48%; }
}

/* Publication rows */
.row-pub { grid-template-columns: 40px minmax(0, 1fr) 150px; align-items: start; }
.row-pub .pub p { color: var(--text-2); font-size: var(--text-lg); line-height: 1.6; margin: 0; }
.row-pub .pub .pub-meta {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-3);
  margin-top: var(--s3);
  line-height: 1.6;
  letter-spacing: 0;
}
.row-pub .pub .pub-meta a { color: var(--accent-text); }
.row-pub .pub .pub-meta a:hover { color: var(--accent-hover); }
.row-pub .pub em { font-style: italic; }
.row-pub .pub strong { color: var(--text); font-weight: 600; }
@media (max-width: 860px) {
  .row-pub { grid-template-columns: 32px minmax(0, 1fr); }
  .row-pub .meta { grid-column: 2; text-align: left; }
}

/* ============================================================
   22. Patent explorer
   ============================================================ */

.pat-tools { padding-block: var(--s8) var(--s6); }

/* --- the portfolio field ---
   Every grant is one mark. 358 of them, stacked by year, so column height is
   still the count but the unit of the chart is the patent rather than the bar.
   This is the page's centrepiece: the record drawn, not tabulated. */
.field {
  display: flex;
  align-items: flex-end;
  gap: 10px;
  max-width: 760px;
  border-bottom: 1px solid var(--border);   /* solid hairline baseline */
  padding-bottom: 2px;
  margin-bottom: var(--s3);
}
.field-col {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column-reverse;   /* marks stack up from the baseline */
  gap: 2px;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  position: relative;
}
.unit {
  display: block;
  height: 6px;
  border-radius: 1px;
  background: var(--accent);
  transition: opacity var(--t-fast);
}
.unit.u1 { background: var(--border-strong); }                       /* expired */
.unit.u2 { background: none; box-shadow: inset 0 0 0 1px var(--accent); } /* too recent */
.unit.off { opacity: .18; }        /* filtered out, still present and countable */

.field-col:hover .unit,
.field-col:focus-visible .unit { opacity: 1; }
.field-col.dim .unit { opacity: .22; }

.field-key {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s5);
  align-items: center;
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  color: var(--text-3);
  margin-bottom: var(--s4);
}
.field-key span { display: inline-flex; align-items: center; gap: 6px; }
.field-key i {
  width: 14px; height: 6px;
  border-radius: 1px;
  background: var(--accent);
  font-style: normal;
}
.field-key i.k1 { background: var(--border-strong); }
.field-key i.k2 { background: none; box-shadow: inset 0 0 0 1px var(--accent); }

.hist-tip {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  background: var(--bg-overlay);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  padding: 5px 9px;
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  color: var(--text);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--t-fast);
  z-index: 5;
}
.field-col:hover .hist-tip,
.field-col:focus-visible .hist-tip { opacity: 1; }
/* keep the tooltip inside the viewport at the ends of the strip */
.field-col:first-child .hist-tip,
.field-col:nth-child(2) .hist-tip { left: 0; transform: none; }
.field-col:last-child .hist-tip,
.field-col:nth-last-child(2) .hist-tip { left: auto; right: 0; transform: none; }

.hist-axis {
  display: flex;
  max-width: 760px;
  justify-content: space-between;
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: .06em;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.hist-caption {
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  letter-spacing: .1em;
  color: var(--text-3);
  margin-bottom: var(--s4);
}

/* --- filter row (one row, above everything it scopes) --- */
.pat-filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  align-items: center;
  padding-block: var(--s6);
  border-top: 1px solid var(--border-muted);
  border-bottom: 1px solid var(--border);
  margin-top: var(--s8);
}
.pat-search {
  flex: 1 1 260px;
  min-width: 200px;
  background: var(--bg-subtle);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  padding: 10px 14px;
  color: var(--text);
  font-family: var(--font-ui);
  font-size: var(--text-base);
}
.pat-search::placeholder { color: var(--text-3); }
.pat-search:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent; }

.pat-count {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .08em;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* --- table view --- */
.pat-table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.pat-table { width: 100%; min-width: 560px; border-collapse: collapse; }
.pat-table caption {
  text-align: left;
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  letter-spacing: .12em;
  color: var(--text-3);
  padding-block: var(--s5) var(--s3);
}
.pat-table th {
  text-align: left;
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-3);
  font-weight: 500;
  padding: var(--s3) var(--s4) var(--s3) 0;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.pat-table td {
  padding: var(--s4) var(--s4) var(--s4) 0;
  border-bottom: 1px solid var(--border-muted);
  vertical-align: top;
  font-size: var(--text-base);
  color: var(--text-2);
  line-height: 1.5;
}
.pat-table tr:hover td { background: var(--bg-subtle); }
.pat-table .c-num {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--accent-text);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.pat-table .c-title { color: var(--text); width: 55%; }
.pat-table .c-year {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--text-3);
  white-space: nowrap;
}
.pat-table .c-status { white-space: nowrap; font-size: var(--text-md); }
.pat-table .c-people { font-size: var(--text-sm); color: var(--text-3); }
.dot-status {
  display: inline-block;
  width: 6px; height: 6px;
  border-radius: var(--r-full);
  margin-right: 7px;
  vertical-align: middle;
}
.s0 { background: var(--ok); }
.s1 { background: var(--text-3); }
.s2 { background: var(--info); }

.pat-empty { padding: var(--s12) 0; color: var(--text-3); text-align: center; }
.pat-more {
  display: block;
  width: 100%;
  margin-top: var(--s6);
  padding: var(--s4);
  background: none;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: var(--text-md);
  font-weight: 500;
  cursor: pointer;
  transition: border-color var(--t-fast), background var(--t-fast);
}
.pat-more:hover { border-color: var(--accent-border); background: var(--accent-subtle); }

.people-cloud { display: flex; flex-wrap: wrap; gap: var(--s2); }
.people-cloud button {
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  color: var(--text-2);
  background: none;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-full);
  padding: 5px 12px;
  cursor: pointer;
  transition: color var(--t-fast), border-color var(--t-fast);
}
.people-cloud button:hover { color: var(--text); border-color: var(--accent-border); }
.people-cloud button .n {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  color: var(--text-3);
  margin-left: 6px;
}

@media (max-width: 780px) {
  .pat-table .c-people, .pat-table th.h-people { display: none; }
  .pat-table .c-title { width: auto; min-width: 260px; }
  .pat-table { min-width: 520px; }
  .field { gap: 5px; }
  .unit { height: 5px; }
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ============================================================
   23. CV
   ============================================================ */

.cv-entry {
  display: grid;
  grid-template-columns: 190px minmax(0, 1fr);
  gap: var(--s8);
  padding: var(--s6) 0;
  border-bottom: 1px solid var(--border-muted);
}
.cv-entry:first-of-type { border-top: 1px solid var(--border-muted); }
/* The city is content, not a separator, so it may not use --text-4 (4.7:1,
   below this site's 7.2:1 floor for small mono). Both steps move up one:
   the date leads at 8.1:1, the city sits under it at 7.2:1. */
.cv-when {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--text-2);
  line-height: 1.65;
  padding-top: 3px;
  font-variant-numeric: tabular-nums;
}
.cv-when span { color: var(--text-3); }
.cv-what h3 {
  font-size: var(--text-2xl);
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: 1.3;
  margin-bottom: var(--s1);
}
.cv-what .org { color: var(--accent-text); font-size: var(--text-base); margin-bottom: var(--s4); }
.cv-what .sub {
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-3);
  margin: var(--s5) 0 var(--s3);
}
.cv-what ul { max-width: 74ch; }
.cv-what ul li {
  color: var(--text-2);
  font-size: var(--text-base);
  line-height: 1.65;
  padding-left: var(--s5);
  position: relative;
  margin-bottom: var(--s3);
}
.cv-what ul li::before {
  content: '';
  position: absolute;
  left: 0; top: .75em;
  width: 10px; height: 1px;
  background: var(--border-strong);
}
.cv-what ul li:last-child { margin-bottom: 0; }
.cv-what a { color: var(--text); box-shadow: inset 0 -1px 0 var(--accent-border); }
.cv-what a:hover { color: var(--accent-hover); }

.cv-pubs { counter-reset: pub; border-top: 1px solid var(--border-muted); }
.cv-pubs li {
  counter-increment: pub;
  position: relative;
  padding: var(--s5) 0 var(--s5) var(--s10);
  border-bottom: 1px solid var(--border-muted);
  color: var(--text-2);
  font-size: var(--text-base);
  line-height: 1.6;
  max-width: 84ch;
}
.cv-pubs li::before {
  content: counter(pub, decimal-leading-zero);
  position: absolute;
  left: 0; top: var(--s5);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--accent-text);
}
.cv-pubs em { font-style: italic; }

.cv-tags { color: var(--text-2); font-size: var(--text-base); line-height: 1.8; margin-top: var(--s3); }
.cv .dl dt { color: var(--text); font-weight: 500; }

@media (max-width: 780px) {
  .cv-entry { grid-template-columns: 1fr; gap: var(--s3); }
  .cv-when { padding-top: 0; }
}

/* Print: the CV page doubles as the PDF source */
@media print {
  @page { size: letter; margin: 14mm 13mm; }
  body { background: var(--p-paper); color: var(--p-ink); font-size: 9pt; line-height: 1.45; padding-left: 0; }
  .rail, .rail-top, .masthead, .site-foot, .hero-actions, .filters, .pat-tools,
  .skip-link { display: none !important; }
  a { color: var(--p-ink); box-shadow: none !important; }
  .page-head { padding-top: 0; padding-bottom: 8pt; border-bottom: 1px solid var(--p-rule-strong); }
  .page-head h1 { font-size: 22pt; color: var(--p-ink); margin-bottom: 6pt; }
  .page-head .lede { font-size: 9.5pt; max-width: none; }
  .page-head .kicker { margin-bottom: 6pt; font-size: 7pt; }
  .page-head .kicker, .page-head .lede { color: var(--p-body); }
  .section { padding-block: 9pt; border-top: 1px solid var(--p-rule-mid); break-inside: auto; }
  .sec-head { border-bottom: 1px solid var(--p-rule-strong); margin-bottom: 7pt; padding-bottom: 4pt; }
  .sec-head .label { color: var(--p-ink); }
  .sec-head .count { color: var(--p-mute); }
  .cv-entry { break-inside: auto; border-bottom: 1px solid var(--p-rule); padding: 6pt 0;
    grid-template-columns: 108px minmax(0, 1fr); gap: 12pt; }
  .cv-what h3 { font-size: 10.5pt; margin-bottom: 0; break-after: avoid; }
  .cv-what .org { break-after: avoid; }
  .cv-what ul li { break-inside: avoid; }
  .sec-head { break-after: avoid; }
  .cv-what .org { font-size: 9pt; margin-bottom: 4pt; }
  .cv-what .sub { margin: 6pt 0 3pt; font-size: 6.5pt; }
  .cv-what ul li { font-size: 8.75pt; line-height: 1.45; margin-bottom: 2.5pt; padding-left: 11pt; max-width: none; }
  .cv-when { font-size: 8pt; }
  .cv-pubs li { font-size: 8.5pt; padding: 5pt 0 5pt 26pt; line-height: 1.42; max-width: none; }
  .cv-tags { font-size: 8.5pt; line-height: 1.55; }
  .prose p { font-size: 9pt; max-width: none; }
  .row { padding: 5pt 0; }
  .row .desc { font-size: 8.5pt; }
  .split { gap: 18pt; }
  .dl div { padding: 3.5pt 0; }
  .dl dt, .dl dd { font-size: 8.5pt; }
  .cv-entry:first-of-type { border-top: 1px solid var(--p-rule); }
  .cv-when, .cv-what .sub { color: var(--p-quiet); }
  .cv-when span { color: var(--p-faint); }
  .cv-what h3 { color: var(--p-ink); }
  .cv-what .org { color: var(--p-accent); }
  .cv-what ul li, .cv-pubs li, .prose p, .cv-tags { color: var(--p-body); }
  .cv-what ul li::before { background: none; border-top: 1px solid var(--p-faint); height: 0; width: 7px; top: 6.5pt; }
  .case-body ul li::before { background: none; border-top: 1px solid var(--p-accent); height: 0; }
  .dot-status { border: 1px solid var(--p-quiet); background: none !important; }
  .cv-pubs li { border-bottom: 1px solid var(--p-rule); break-inside: avoid; }
  .cv-pubs li::before { color: var(--p-accent); }
  .row { border-bottom: 1px solid var(--p-rule); break-inside: avoid; padding: 8pt 0; }
  .row .code { color: var(--p-accent); }
  .row .title { color: var(--p-ink); }
  .row .desc, .row .meta { color: var(--p-body); }
  .dl div { border-bottom: 1px solid var(--p-rule); padding: 5pt 0; }
  .dl dt { color: var(--p-ink); }
  .dl dd { color: var(--p-body); }
  .contact-links a { color: var(--p-ink); }
  .contact-links .k { color: var(--p-quiet); }
}

/* ============================================================
   24. Post body (markdown from Jekyll)
   ============================================================ */

.post-body { padding-block: clamp(var(--s10), 5vw, 4rem); max-width: 68ch; }
.post-body p,
.post-body li { color: var(--text-2); font-size: var(--text-xl); line-height: 1.75; }
.post-body p { margin-bottom: var(--s6); }
.post-body h2 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-post-h2);
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--text);
  margin: clamp(var(--s10), 4vw, var(--s16)) 0 var(--s5);
}
.post-body h3 {
  font-size: var(--text-2xl);
  font-weight: 600;
  color: var(--text);
  margin: var(--s8) 0 var(--s4);
}
.post-body strong { color: var(--text); font-weight: 600; }
.post-body em { font-style: italic; }
.post-body a {
  color: var(--text);
  box-shadow: inset 0 -1px 0 var(--accent-border);
  transition: box-shadow var(--t-fast), color var(--t-fast);
}
.post-body a:hover { color: var(--accent-hover); box-shadow: inset 0 -1px 0 var(--accent); }
.post-body ul, .post-body ol { margin-bottom: var(--s6); }
.post-body ul li {
  padding-left: var(--s6);
  position: relative;
  margin-bottom: var(--s3);
}
.post-body ul li::before {
  content: '';
  position: absolute;
  left: 0; top: .85em;
  width: 12px; height: 1px;
  background: var(--accent);
}
.post-body ol { counter-reset: n; }
.post-body ol li {
  counter-increment: n;
  padding-left: var(--s8);
  position: relative;
  margin-bottom: var(--s3);
}
.post-body ol li::before {
  content: counter(n, decimal-leading-zero);
  position: absolute;
  left: 0; top: .18em;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--accent-text);
}
.post-body hr {
  border: 0;
  border-top: 1px solid var(--border);
  margin: clamp(var(--s10), 4vw, var(--s16)) 0;
}
.post-body blockquote {
  border-left: 2px solid var(--accent-border);
  padding-left: var(--s6);
  margin: var(--s8) 0;
  color: var(--text-2);
  font-style: italic;
}
.post-body code {
  font-family: var(--font-mono);
  font-size: .875em;
  background: var(--bg-muted);
  border-radius: var(--r-sm);
  padding: 2px 5px;
  color: var(--text);
}
.post-body pre {
  background: var(--bg-subtle);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--s5);
  overflow-x: auto;
  margin-bottom: var(--s6);
}
.post-body pre code { background: none; padding: 0; }
.post-body table { width: 100%; border-collapse: collapse; margin-bottom: var(--s6); }
.post-body th, .post-body td {
  text-align: left;
  padding: var(--s3) var(--s4) var(--s3) 0;
  border-bottom: 1px solid var(--border-muted);
  font-size: var(--text-base);
  color: var(--text-2);
}
.post-body th { color: var(--text-3); font-family: var(--font-mono); font-size: var(--text-xs); letter-spacing: .1em; text-transform: uppercase; }
.post-body img { border-radius: var(--r-md); margin-bottom: var(--s6); }
.post-body figure { margin: var(--s8) 0; }
.post-body figure img {
  width: 100%;
  margin-bottom: 0;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}
.post-body figcaption {
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--text-3);
  margin-top: var(--s3);
}

/* ============================================================
   25. Publication lanes
   ============================================================ */

/* --- the research record drawn ---
   Nine publications is a number you can already read as a list, so this is
   deliberately not a unit chart trying to make nine look like a lot. What
   the numbered list cannot show is the shape: three research lines that ran
   in sequence rather than in parallel, and the years between them. One lane
   per line, one mark per publication, placed in the year it appeared. The
   empty columns carry the argument the lede makes in prose.

   The marks stay a single accent on purpose. /patents/ uses fill, muted and
   outline to mean grant status, and reusing those three treatments here for
   a different variable would make the two pages contradict each other. The
   lane label carries the line identity instead, which is stronger than
   colour anyway. */
.lines { margin: var(--s5) 0 var(--s8); max-width: 760px; }
.line-chart { display: grid; gap: var(--s4); margin-top: var(--s5); }
.line-lane {
  display: grid;
  grid-template-columns: 170px minmax(0, 1fr) 2ch;
  gap: var(--s4);
  align-items: end;
}
.lane-name {
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-3);
  padding-bottom: 4px;
}
.lane-track {
  display: grid;
  grid-template-columns: repeat(15, 1fr);
  grid-template-rows: 6px 6px;
  gap: 2px;
  align-content: end;
  border-bottom: 1px solid var(--border-muted);
  padding-bottom: 4px;
}
.lane-track i {
  grid-row: 2;            /* marks sit on the baseline */
  height: 6px;
  border-radius: 1px;
  background: var(--accent);
}
.lane-track i.stack { grid-row: 1; }   /* a second publication in the same year */
.lane-count {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  text-align: right;
  padding-bottom: 4px;
}
.lines-axis {
  display: grid;
  grid-template-columns: 170px minmax(0, 1fr) 2ch;
  gap: var(--s4);
  margin-top: var(--s2);
}
.lines-axis .yrs {
  grid-column: 2;
  display: grid;
  grid-template-columns: repeat(15, 1fr);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: .06em;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.lines-axis .yrs span { white-space: nowrap; }
.lines-axis .yrs span:first-child { justify-self: start; }
.lines-axis .yrs span:last-child  { justify-self: end; }

/* Narrow: the label and its count share a line, the track runs full width
   beneath them. Left to stack on its own the count reads as a stray numeral
   floating under the timeline. */
@media (max-width: 700px) {
  .line-lane {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--s2) var(--s4);
    align-items: baseline;
  }
  .lane-name  { grid-column: 1; grid-row: 1; padding-bottom: 0; }
  .lane-count { grid-column: 2; grid-row: 1; padding-bottom: 0; }
  .lane-track { grid-column: 1 / -1; grid-row: 2; }
  .lines-axis { grid-template-columns: minmax(0, 1fr); }
  .lines-axis .yrs { grid-column: 1; }
}

/* ============================================================
   26. Portrait
   ============================================================ */

/* The photograph is a near-black frame: bookshelf behind, black shirt in
   front. Every edge meets the paper at 16-18:1, which made it the loudest
   element on the page, above the headings at 14.75:1. The 1px --border it
   carried over from v1 cannot read against a black edge, so it was doing
   nothing but describing an edge that was already absolute.

   This system has no shadows, so the photo is mounted the way a print is
   matted: paper, then the sunken warm tone, then the image. The graduation
   does the mediating a hairline could not. Radii are concentric, 8px inner
   plus an 8px mount giving the 16px outer. */
.portrait {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: var(--r-md);
  background: var(--bg-subtle);
}
.portrait-figure picture {
  display: block;
  padding: var(--s2);
  background: var(--bg-muted);
  border-radius: var(--r-xl);
}
.portrait-figure { margin-bottom: var(--s8); }
.portrait-figure figcaption {
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-top: var(--s3);
}

/* contact block with a portrait beside it */
.contact-split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 260px;
  gap: var(--s12);
  align-items: start;
}
@media (max-width: 860px) {
  .contact-split { grid-template-columns: 1fr; gap: var(--s8); }
  .contact-split .portrait-figure { max-width: 220px; }
}
