/* ============================================================
   CEAD / PKJ design system — simulation-forward, dark ground
   Tokens -> reset -> type -> layout -> components -> motion
   ============================================================ */

/* ---------- 1. Tokens ---------- */
:root {
  /* type */
  --font-display: "Newsreader", Georgia, "Times New Roman", serif;
  --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  /* ground — deep, slightly blue-black; reads as a viewport, not a page */
  --ink:        #06080c;
  --surface:    #0d1117;
  --surface-2:  #141b24;
  --surface-3:  #1c252f;
  --line:       #242e3a;
  --line-soft:  #18202a;

  /* text */
  --text:       #e9edf4;
  --text-dim:   #9aa6b8;
  --text-faint: #6b7688;

  /* accent — cool "field" primary, hot "damage" secondary, as in a
     scientific colormap running cold -> hot */
  --accent:     #45d8c4;
  --accent-2:   #ff7847;
  --accent-dim: #1d5f57;
  --accent-glow: rgba(69, 216, 196, 0.16);

  --max:        78rem;
  --max-prose:  40rem;
  --max-wide:   92rem;

  --s1: .375rem; --s2: .75rem;  --s3: 1.25rem;
  --s4: 2rem;    --s5: 3.25rem; --s6: 5rem;    --s7: 8rem;

  --radius: 3px;
  --radius-lg: 6px;

  --ease: cubic-bezier(.2, .7, .3, 1);

  /* media treatment — retuned per theme, since a dark ground needs the
     simulation frames pushed down and a light ground needs them lifted */
  --media-hero-filter: saturate(1.2) contrast(1.04) brightness(.5);
  --media-band-filter: saturate(1.2) contrast(1.04) brightness(.72);
  --media-card-filter: saturate(1.05) brightness(.9);
  --scrim-hero:
    linear-gradient(180deg, rgba(6,8,12,.82) 0%, rgba(6,8,12,.45) 30%, rgba(6,8,12,.9) 76%, var(--ink) 100%),
    linear-gradient(90deg, rgba(6,8,12,.9) 0%, rgba(6,8,12,.55) 55%, rgba(6,8,12,.25) 100%);
  --scrim-band:
    linear-gradient(90deg, var(--ink) 0%, rgba(6,8,12,.94) 38%, rgba(6,8,12,.62) 72%, rgba(6,8,12,.45) 100%),
    linear-gradient(180deg, var(--ink) 0%, transparent 18%, transparent 82%, var(--ink) 100%);
}

/* ---------- Light theme ----------
   Not an inversion. The simulation renders are mostly figures on white, so a
   light ground lets them sit naturally instead of being darkened into the
   page; the scrims turn into veils rather than shadows. */
:root[data-theme="light"] {
  --ink:        #ffffff;
  --surface:    #f7f6f3;
  --surface-2:  #efeee9;
  --surface-3:  #e6e4de;
  --line:       #d8d5cc;
  --line-soft:  #e8e6df;

  --text:       #14171c;
  --text-dim:   #4d5560;
  --text-faint: #767f8c;

  --accent:     #0f766e;
  --accent-2:   #c2410c;
  --accent-dim: #99f6e4;
  --accent-glow: rgba(15, 118, 110, .08);

  --media-hero-filter: saturate(1.06) contrast(1.02) brightness(1.02);
  --media-band-filter: saturate(1.04) contrast(1.01) brightness(1.03);
  --media-card-filter: saturate(1.02) brightness(1);
  --scrim-hero:
    linear-gradient(180deg, rgba(255,255,255,.72) 0%, rgba(255,255,255,.35) 30%, rgba(255,255,255,.88) 78%, var(--ink) 100%),
    linear-gradient(90deg, rgba(255,255,255,.94) 0%, rgba(255,255,255,.62) 55%, rgba(255,255,255,.28) 100%);
  --scrim-band:
    linear-gradient(90deg, var(--ink) 0%, rgba(255,255,255,.93) 38%, rgba(255,255,255,.6) 72%, rgba(255,255,255,.4) 100%),
    linear-gradient(180deg, var(--ink) 0%, transparent 20%, transparent 80%, var(--ink) 100%);
}

:root[data-theme="light"] html[data-site="lab"],
html[data-site="lab"][data-theme="light"] {
  --accent:   #b45309;
  --accent-2: #0f766e;
  --accent-glow: rgba(180, 83, 9, .08);
}

/* lab identity runs warm-primary: same system, inverted temperature */
html[data-site="lab"] {
  --accent:     #ff8a54;
  --accent-2:   #45d8c4;
  --accent-dim: #6b3520;
  --accent-glow: rgba(255, 138, 84, 0.16);
}

/* ---------- 2. Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  background: var(--ink);
}

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: 16.5px;
  line-height: 1.65;
  color: var(--text);
  background: var(--ink);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-feature-settings: "ss01", "cv05";
}

img, video, svg { max-width: 100%; display: block; }
::selection { background: var(--accent); color: var(--ink); }

/* ---------- 3. Typography ---------- */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.08;
  letter-spacing: -0.018em;
  margin: 0 0 var(--s3);
  text-wrap: balance;
}

h1 { font-size: clamp(2.3rem, 1.5rem + 3.4vw, 4.5rem); }
h2 { font-size: clamp(1.9rem, 1.4rem + 2vw, 3rem); }
h3 { font-size: clamp(1.2rem, 1.05rem + .6vw, 1.5rem); }
h4 { font-size: 1.05rem; font-family: var(--font-sans); font-weight: 600; letter-spacing: 0; }

p { margin: 0 0 var(--s3); }
p:last-child { margin-bottom: 0; }

a { color: var(--accent); text-decoration: none; transition: color .18s var(--ease); }
a:hover { color: var(--text); }

strong { font-weight: 600; color: var(--text); }

/* the lede under a hero headline */
.lede {
  font-size: clamp(1.1rem, 1rem + .6vw, 1.35rem);
  line-height: 1.55;
  color: var(--text-dim);
  max-width: 46ch;
  text-wrap: pretty;
}
/* A lede under a section heading, not a page title: the page-head size is set
   to carry a whole screen and reads as a second headline halfway down. Wider
   too, since it sits over a metrics row rather than alone in a column. */
.lede--section {
  font-size: clamp(1rem, .95rem + .3vw, 1.13rem);
  max-width: 68ch;
  margin-block: 0 var(--s4);
}

/* small mono label — the workhorse of the metadata layer */
.eyebrow {
  font-family: var(--font-mono);
  font-size: .68rem;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 var(--s3);
  display: flex;
  align-items: center;
  gap: var(--s2);
}
.eyebrow::after {
  content: "";
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, var(--line) 0%, transparent 100%);
}
.eyebrow--plain::after { display: none; }

/* ---------- 4. Layout ---------- */
.container { width: 100%; max-width: var(--max); margin-inline: auto; padding-inline: var(--s4); }
.container--wide { max-width: var(--max-wide); }
.container--prose { max-width: var(--max-prose); }

.section { padding-block: var(--s6); position: relative; }
.section--tight { padding-block: var(--s5); }
.section--alt { background: var(--surface); }
.section--edge { border-top: 1px solid var(--line-soft); }

.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s4);
  margin-bottom: var(--s5);
  flex-wrap: wrap;
}
.section-head h2 { margin: 0; }

.more {
  font-family: var(--font-mono);
  font-size: .78rem;
  letter-spacing: .04em;
  white-space: nowrap;
}
.more::after { content: " \2192"; transition: transform .2s var(--ease); display: inline-block; }
.more:hover::after { transform: translateX(3px); }

.grid { display: grid; gap: var(--s4); }
/* Fixed tracks, not auto-fit. With `repeat(auto-fit, minmax(17rem, 1fr))` the
   column count followed the item count: a thrust with one project got a single
   card stretched across the full container, one with five got five in a row,
   and no two thrust pages laid out the same way. A card is now the same size
   wherever it appears, and a short group leaves the remaining tracks empty
   rather than inflating to fill them.

   minmax(0, 1fr), not 1fr: the cards carry aspect-ratio media, which sets an
   intrinsic width a bare 1fr track will not shrink below. */
.grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 62rem) {
  .grid--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 40rem) {
  .grid--2, .grid--3 { grid-template-columns: minmax(0, 1fr); }
}

/* ---------- 5. Header ---------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 60;
  background: color-mix(in srgb, var(--ink) 72%, transparent);
  backdrop-filter: saturate(160%) blur(14px);
  -webkit-backdrop-filter: saturate(160%) blur(14px);
  border-bottom: 1px solid var(--line-soft);
}
.site-header > .container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
  min-height: 4.25rem;
}

.site-brand {
  font-family: var(--font-display);
  font-size: 1.15rem;
  color: var(--text);
  letter-spacing: -.01em;
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  white-space: nowrap;
}
.site-brand em {
  font-family: var(--font-mono);
  font-style: normal;
  font-size: .64rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.site-brand:hover { color: var(--accent); }

.site-nav { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
.site-nav a {
  font-size: .84rem;
  font-weight: 450;
  color: var(--text-dim);
  padding-block: .3rem;
  position: relative;
}
.site-nav a::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0;
  width: 0; height: 1px;
  background: var(--accent);
  transition: width .22s var(--ease);
}
.site-nav a:hover { color: var(--text); }
.site-nav a:hover::after { width: 100%; }
.site-nav a.is-cta {
  color: var(--ink);
  background: var(--accent);
  padding: .34rem .8rem;
  border-radius: var(--radius);
  font-weight: 600;
}
.site-nav a.is-cta::after { display: none; }
.site-nav a.is-cta:hover { background: var(--text); }

/* Hidden above the breakpoint; the rules that show it are in section 17. */
.nav-toggle {
  display: none;
  background: none; border: 1px solid var(--line-soft); border-radius: var(--radius);
  width: 2.35rem; height: 2.35rem; padding: 0; cursor: pointer;
  flex-direction: column; justify-content: center; align-items: center; gap: 4px;
  margin-left: auto;
}
.nav-toggle span {
  display: block; width: 15px; height: 1.5px; background: var(--text-dim);
  transition: transform .22s var(--ease), opacity .18s var(--ease);
}
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(5.5px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-5.5px) rotate(-45deg); }

/* ---------- 6. Simulation hero ---------- */
.hero {
  position: relative;
  min-height: min(72vh, 42rem);
  display: flex;
  /* Centred rather than bottom-anchored. When the copy is shorter than the
     hero's minimum height, flex-end dropped the whole surplus above the first
     line, so the text started well down the page with nothing in the band
     above it. Centring splits that surplus instead of stacking it on top. */
  align-items: center;
  overflow: hidden;
  border-bottom: 1px solid var(--line-soft);
}

.hero-media {
  position: absolute;
  inset: 0;
  z-index: 0;
}
.hero-media img,
.hero-media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* simulation renders are usually light-ground; push them into the dark
     system rather than letting them glare */
  filter: var(--media-hero-filter);
}

/* scrim: readable text over arbitrary simulation frames */
.hero-media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--scrim-hero);
}

.hero-inner {
  position: relative;
  z-index: 1;
  width: 100%;
  padding-block: var(--s3) var(--s4);
}
.hero h1 { margin-bottom: var(--s3); }
.hero .lede { margin-bottom: var(--s4); }

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

/* caption crediting what the loop actually shows */
.hero-credit {
  position: absolute;
  right: var(--s4);
  bottom: var(--s3);
  z-index: 2;
  font-family: var(--font-mono);
  font-size: .66rem;
  letter-spacing: .05em;
  color: var(--text-faint);
  text-align: right;
  max-width: 24ch;
}

/* ---------- 7. Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  font-family: var(--font-sans);
  font-size: .86rem;
  font-weight: 550;
  padding: .62rem 1.15rem;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  color: var(--text);
  background: transparent;
  transition: all .2s var(--ease);
  cursor: pointer;
}
.btn:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-glow); }
.btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--ink);
  font-weight: 650;
}
.btn--primary:hover { background: #fff; border-color: #fff; color: var(--ink); }

/* ---------- 8. Metrics ---------- */
.metrics {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s5);
  padding-block: var(--s3);
}
.metrics--bordered {
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  padding-block: var(--s4);
}
.metric { display: flex; flex-direction: column; gap: .1rem; }
.metric-value {
  font-family: var(--font-display);
  font-size: clamp(1.9rem, 1.4rem + 1.6vw, 2.9rem);
  line-height: 1;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.metric-value em { font-style: normal; color: var(--accent); }
.metric-label {
  font-family: var(--font-mono);
  font-size: .66rem;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* the restrained personal-site variant: one line, in context */
.metric-line {
  font-family: var(--font-mono);
  font-size: .78rem;
  letter-spacing: .04em;
  color: var(--text-faint);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  align-items: center;
}
.metric-line b { color: var(--text-dim); font-weight: 500; }
.metric-line span[aria-hidden] { color: var(--line); }

/* ---------- 9. Badges ---------- */
.badges { display: flex; flex-wrap: wrap; gap: .4rem; }
.badge {
  font-family: var(--font-mono);
  font-size: .62rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  padding: .2rem .5rem;
  border-radius: 2px;
  border: 1px solid var(--line);
  color: var(--text-faint);
  white-space: nowrap;
}
.badge--active { color: var(--accent); border-color: var(--accent-dim); background: var(--accent-glow); }
.badge--funded { color: var(--accent-2); border-color: color-mix(in srgb, var(--accent-2) 35%, transparent); }
.badge--review { color: #c8a2ff; border-color: rgba(200,162,255,.3); }

/* ---------- 10. Theme bands (full-bleed research thrusts) ---------- */
.band {
  position: relative;
  min-height: 30rem;
  display: flex;
  align-items: center;
  overflow: hidden;
  border-top: 1px solid var(--line-soft);
}
.band-media { position: absolute; inset: 0; z-index: 0; }
/* Much of this media is analysis output carrying its own axis labels and
   legends. Left sharp it competes with the band headline as *text*, so a deep
   directional scrim demotes it to texture. The renders are clean enough now
   that the scrim alone does it; the blur this used to carry is gone. */
.band-media img, .band-media video {
  width: 100%; height: 100%; object-fit: cover;
  filter: var(--media-band-filter);
}
.band-media::after {
  content: "";
  position: absolute; inset: 0;
  background: var(--scrim-band);
}
/* fallback when a thrust has no media yet: a rendered field, not a grey box */
.band-media--none {
  position: absolute; inset: 0; z-index: 0;
  background: linear-gradient(120deg, var(--surface-2) 0%, var(--surface) 60%, var(--ink) 100%);
}
.band-media--none::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(90deg, var(--ink) 0%, rgba(6,8,12,.7) 55%, transparent 100%);
}

.band-inner {
  /* it is a .container too, so a max-width here centred it and left the page's
     left half empty while the media sat off to the right. Keep the container's
     own width and hold the measure on the children instead. */
  position: relative; z-index: 1; padding-block: var(--s5);
}
.band-inner > * { max-width: 34rem; }
.band-index {
  font-family: var(--font-mono);
  font-size: .72rem;
  letter-spacing: .18em;
  color: var(--accent);
  display: block;
  margin-bottom: var(--s2);
}
.band h2 { margin-bottom: var(--s3); }
.band p { color: var(--text-dim); }

.band-links { display: flex; flex-wrap: wrap; gap: var(--s3); margin-top: var(--s3); font-family: var(--font-mono); font-size: .74rem; }

/* ---------- 11. Cards ---------- */
.card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-lg);
  overflow: hidden;
  transition: border-color .22s var(--ease), transform .22s var(--ease);
}
.card:hover { border-color: var(--line); transform: translateY(-2px); }

/* min-height:0 because a flex item's automatic minimum is its content, which
   let a video taller than the box push the box past its own aspect-ratio --
   one card in a row then stood taller than its neighbours. The wrapper rule is
   for the theme-paired case: sim-media emits a plain div per variant, and
   without a height the media inside had nothing to resolve height:100% against. */
.card-media {
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--surface-2);
  position: relative;
  min-height: 0;
}
.card-media > * { width: 100%; height: 100%; }
.card-media img, .card-media video {
  width: 100%; height: 100%; object-fit: cover;
  filter: var(--media-card-filter);
  transition: transform .5s var(--ease), filter .3s var(--ease);
}
.card:hover .card-media img, .card:hover .card-media video { transform: scale(1.035); filter: saturate(1.15) brightness(1); }

.card-body { padding: var(--s3); display: flex; flex-direction: column; gap: var(--s2); flex: 1; }
.card-title { font-family: var(--font-display); font-size: 1.22rem; line-height: 1.2; margin: 0; }
.card-title a { color: var(--text); }
.card-title a:hover { color: var(--accent); }
.card-desc { font-size: .88rem; color: var(--text-dim); margin: 0; }
.card-foot:empty { display: none; }
.card-foot {
  margin-top: auto;
  padding-top: var(--s2);
  border-top: 1px solid var(--line-soft);
  /* a footer with nothing in it drew a rule under empty space */
  display: flex; flex-wrap: wrap; gap: .6rem;
  font-family: var(--font-mono);
  font-size: .68rem;
  color: var(--text-faint);
  letter-spacing: .04em;
  display: flex;
  gap: var(--s2);
  flex-wrap: wrap;
}

/* stretch the whole card as one hit target */
.card-link::after { content: ""; position: absolute; inset: 0; }

/* ---------- 12. Publications ---------- */
.pub-list { display: flex; flex-direction: column; }
.pub {
  /* the year lives on the group heading now, so the row is a single column */
  padding-block: var(--s3);
  border-bottom: 1px solid var(--line-soft);
}
.pub:last-child { border-bottom: 0; }
.pub-year {
  font-family: var(--font-mono);
  font-size: .78rem;
  color: var(--text-faint);
  padding-top: .25rem;
  font-variant-numeric: tabular-nums;
}
.pub-title { font-family: var(--font-display); font-size: 1.1rem; line-height: 1.3; margin: 0 0 .25rem; }
.pub-title a { color: var(--text); }
.pub-title a:hover { color: var(--accent); }
.pub-meta { font-size: .82rem; color: var(--text-dim); margin: 0; }
.pub-venue { font-style: italic; }
.pub-links { display: flex; gap: var(--s2); margin-top: .5rem; font-family: var(--font-mono); font-size: .68rem; letter-spacing: .06em; text-transform: uppercase; }
.pub-links a { color: var(--text-faint); }
.pub-links a:hover { color: var(--accent); }

/* ---------- 13. Activity feed ---------- */
.feed { display: flex; flex-direction: column; }
.feed-item {
  display: grid;
  grid-template-columns: 7rem 1fr;
  gap: var(--s3);
  padding-block: var(--s3);
  border-bottom: 1px solid var(--line-soft);
  align-items: baseline;
}
.feed-date {
  font-family: var(--font-mono);
  font-size: .7rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.feed-title { font-size: .96rem; margin: 0; color: var(--text); }
.feed-title a { color: var(--text); }
.feed-title a:hover { color: var(--accent); }

/* ---------- 14. People ---------- */
.people-grid { display: grid; gap: var(--s4); grid-template-columns: repeat(auto-fill, minmax(min(100%, 12rem), 1fr)); }
.person { display: flex; flex-direction: column; }
.person-link { display: flex; flex-direction: column; gap: var(--s2); color: inherit; }
.person-link:hover .person-name { color: var(--accent); }
.person-photo {
  aspect-ratio: 1;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface-2);
  border: 1px solid var(--line-soft);
}
.person-photo img { width: 100%; height: 100%; object-fit: cover; filter: grayscale(.35) contrast(1.05); transition: filter .3s var(--ease); }
.person:hover .person-photo img { filter: grayscale(0) contrast(1); }
/* No photograph: a monogram on the surface plate, which reads as deliberate
   where a stock silhouette reads as missing. */
.person-mono {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%;
  font-family: var(--font-display); font-size: 3rem; font-weight: 400;
  color: var(--text-faint);
  background: linear-gradient(140deg, var(--surface-2) 0%, var(--surface) 100%);
}
.person-name { font-weight: 600; font-size: .95rem; margin: 0; }
.person-focus { font-size: .82rem; color: var(--text-dim); margin: .3rem 0 0; line-height: 1.45; }
.person-role { font-family: var(--font-mono); font-size: .68rem; letter-spacing: .05em; color: var(--text-faint); margin: 0; }

.people-group + .people-group { margin-top: var(--s5); }
.people-group > h3, .people-group > h2 {
  font-family: var(--font-mono);
  font-size: .7rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: var(--s3);
  padding-bottom: var(--s2);
  border-bottom: 1px solid var(--line-soft);
}

/* ---------- 15. Prose (single pages) ---------- */
.prose { max-width: var(--max-prose); font-size: 1.02rem; }
.prose h2 {
  /* base h2 is the page-section scale; inside running prose it has to sit
     below the lede, not compete with the page title */
  font-size: clamp(1.24rem, 1.1rem + .45vw, 1.55rem);
  margin-top: var(--s5); margin-bottom: var(--s2);
  letter-spacing: -.005em;
}
.prose h3 {
  font-size: clamp(1.05rem, 1rem + .25vw, 1.18rem);
  margin-top: var(--s4); margin-bottom: var(--s2);
}
.prose h2 + p, .prose h3 + p { margin-top: 0; }
.prose img, .prose video { border-radius: var(--radius-lg); margin-block: var(--s4); border: 1px solid var(--line-soft); }
.prose code {
  font-family: var(--font-mono);
  font-size: .86em;
  background: var(--surface-2);
  padding: .12em .4em;
  border-radius: 2px;
  color: var(--accent);
}
.prose pre {
  background: var(--surface);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-lg);
  padding: var(--s3);
  overflow-x: auto;
}
.prose pre code { background: none; padding: 0; color: var(--text); }
.prose blockquote {
  margin: var(--s4) 0;
  padding-left: var(--s3);
  border-left: 2px solid var(--accent);
  color: var(--text-dim);
  font-style: italic;
}
.prose table { width: 100%; border-collapse: collapse; font-size: .9rem; }
.prose th, .prose td { text-align: left; padding: .55rem .75rem; border-bottom: 1px solid var(--line-soft); }
.prose th { font-family: var(--font-mono); font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--text-faint); }

.page-head { padding-block: var(--s6) var(--s4); border-bottom: 1px solid var(--line-soft); }
.page-head p.lede { margin-top: var(--s3); }
/* A record page's title is a paper or grant title, not a section name: it needs
   a reading measure and a smaller size. Seven lines of 64px display type in a
   590px column was the old result. */
.page-head--record { padding-block: var(--s5) var(--s4); }
.page-head--record h1 {
  font-size: clamp(1.6rem, 1.25rem + 1.5vw, 2.5rem);
  max-width: 26ch;
  line-height: 1.15;
}
.page-head--record .record-meta,
.page-head--record .lede { max-width: 68ch; }
/* the body keeps a reading measure inside the same wide container, so the left
   edge does not jump between the head and the body */
.recordbody { max-width: 68ch; }
.recordbody .prose { max-width: none; }

/* ---------- 16. Footer ---------- */
.site-footer {
  border-top: 1px solid var(--line-soft);
  background: var(--surface);
  padding-block: var(--s5);
  font-size: .84rem;
  color: var(--text-faint);
}
.site-footer .container { display: flex; flex-wrap: wrap; gap: var(--s4); justify-content: space-between; align-items: flex-start; }
.footer-links { display: flex; flex-wrap: wrap; gap: var(--s3); font-family: var(--font-mono); font-size: .74rem; letter-spacing: .05em; }
.footer-links a { color: var(--text-dim); }
.footer-links a:hover { color: var(--accent); }

/* ---------- 17. Responsive ---------- */
@media (max-width: 900px) {
  .band-inner { max-width: none; }
  /* copy now sits over the media rather than beside it: scrim top-to-bottom */
  .band-media::after {
    background: linear-gradient(180deg, rgba(6,8,12,.5) 0%, rgba(6,8,12,.92) 55%, var(--ink) 100%);
  }
}

/* The nav collapses behind the toggle. Its own breakpoint, not the layout's:
   the personal site carries thirteen items and needs about 1360px to lay them
   out at full size, so between 900px and there the row wrapped and doubled the
   header's height. It is tightened from 1360px down and collapses at 1100px,
   which is the width below which it cannot fit on one line at any size.
   The theme control is pulled back out of the panel and parked next to the
   toggle so it stays one tap away. */
@media (max-width: 1100px) {
  .nav-toggle { display: flex; }
  .site-nav {
    position: absolute; left: 0; right: 0; top: 100%;
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--ink);
    backdrop-filter: saturate(160%) blur(14px);
    -webkit-backdrop-filter: saturate(160%) blur(14px);
    border-bottom: 1px solid var(--line-soft);
    padding: var(--s2) var(--s3) var(--s3);
    max-height: calc(100vh - 4rem); overflow-y: auto;
  }
  .site-nav:not(.is-open) { display: none; }
  .site-nav a {
    font-size: .95rem; padding-block: .7rem;
    border-bottom: 1px solid var(--line-soft);
  }
  .site-nav a::after { display: none; }
  .site-nav a.is-cta {
    margin-top: var(--s2); text-align: center; border-bottom: 0;
  }
  .site-header .container { position: relative; }
  .site-nav .theme-toggle { margin-top: var(--s2); align-self: flex-start; }
}

@media (max-width: 720px) {
  body { font-size: 16px; }
  .container { padding-inline: var(--s3); }
  .section { padding-block: var(--s5); }
  .site-header > .container { min-height: 3.5rem; }
  .site-brand em { display: none; }
  .hero { min-height: 78vh; }
  .metrics { gap: var(--s4); }
  .pub { grid-template-columns: 1fr; gap: .35rem; }
  .pub-year { padding-top: 0; }
  .feed-item { grid-template-columns: 1fr; gap: .3rem; }
  .hero-credit { display: none; }
}

/* ---------- 18. Motion + a11y ---------- */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

.skip-link {
  position: absolute;
  left: -9999px;
  z-index: 100;
  padding: .6rem 1rem;
  background: var(--accent);
  color: var(--ink);
  font-weight: 600;
}
.skip-link:focus { left: var(--s3); top: var(--s3); }

.reveal { opacity: 0; transform: translateY(14px); transition: opacity .7s var(--ease), transform .7s var(--ease); }
.reveal.is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    transition-duration: .01ms !important;
  }
  .reveal { opacity: 1; transform: none; }
  /* videos are decorative here; JS also pauses them under this preference */
}

/* ---------- 19. Theme toggle ---------- */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem; height: 2rem;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  transition: all .2s var(--ease);
  flex-shrink: 0;
}
.theme-toggle:hover { color: var(--accent); border-color: var(--accent); }
.theme-toggle svg { width: 15px; height: 15px; }
.theme-toggle .icon-sun { display: none; }
:root[data-theme="light"] .theme-toggle .icon-sun { display: block; }
:root[data-theme="light"] .theme-toggle .icon-moon { display: none; }

/* ---------- 20. Showcase rail ---------- */
.showcase {
  border-bottom: 1px solid var(--line-soft);
  background: var(--surface);
  padding-block: var(--s2);
}
/* leading the page: no top rule needed against the header, and the tiles can
   run a touch shorter so the hero still shows above the fold */
main > .showcase:first-child { padding-top: var(--s2); }
/* the caption is a fixed height, so a very short tile leaves the media as a
   sliver under it -- keep 16:9 and let the strip be tall enough to read */

/* The rail. `overflow-x: auto` on a real scroll container rather than a
   transform marquee, so a wheel, a swipe and a drag all still work and the
   scrollbar is a genuine affordance -- site.js drives scrollLeft. The earlier
   rail hid its scrollbar, never moved, and left two-thirds of its tiles
   unreachable; both halves of that are fixed here. */
.showcase-rail {
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
  /* Full-bleed: the tiles run to both window edges so the strip reads as
     continuing past them, but the first tile still starts on the page's text
     column rather than jammed against the glass. */
  padding-inline: max(var(--s3), calc((100vw - var(--max-wide)) / 2));
  padding-block: 2px var(--s2);
  /* Fade the two edges so a tile leaving the viewport dissolves instead of
     being guillotined -- which is what made the old parked rail look broken. */
  -webkit-mask-image: linear-gradient(to right, transparent, #000 5%, #000 95%, transparent);
          mask-image: linear-gradient(to right, transparent, #000 5%, #000 95%, transparent);
}
.showcase-rail:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
.showcase-rail::-webkit-scrollbar { height: 6px; }
.showcase-rail::-webkit-scrollbar-track { background: transparent; }
.showcase-rail::-webkit-scrollbar-thumb {
  background: var(--line); border-radius: 3px;
}
.showcase-rail:hover::-webkit-scrollbar-thumb { background: var(--text-dim); }

.showcase-track {
  /* One media height for the whole strip; each tile's width comes from its own
     render's aspect ratio. That is what makes it read as a filmstrip rather
     than a ragged row, and it is why a rail beats a grid for this content --
     these six clips run from 1:1 to 3.4:1, and any single cell shape would have
     to crop most of them. */
  --shot-h: 14rem;
  display: flex;
  gap: var(--s3);
  width: max-content;
  align-items: stretch;
}

/* Arrows. Rendered `hidden`; site.js unhides them only when the track actually
   overflows, so a narrow tile set never shows dead controls. */
.showcase-nav { display: flex; gap: .4rem; margin-left: auto; }
.railbtn {
  width: 34px; height: 34px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--text-dim);
  cursor: pointer;
  transition: color .2s var(--ease), border-color .2s var(--ease);
}
.railbtn:hover { color: var(--accent); border-color: var(--accent); }
.railbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.railbtn svg { width: 16px; height: 16px; }
/* A finger is not a mouse pointer: 34px is under the 44px target guideline,
   which matters here because the rail is most useful on the device where you
   would swipe it. */
@media (pointer: coarse) {
  .railbtn { width: 44px; height: 44px; }
  .railbtn svg { width: 19px; height: 19px; }
}

/* Tile widths. In a rail a tile need not be the same width as its
   neighbours, which is the whole reason to prefer a rail over a grid here: a
   3.4:1 plate or a three-panel comparison gets a tile shaped like the render
   instead of being cropped to fit a uniform cell. flex: 0 0 auto so nothing
   stretches or shrinks -- the track is width: max-content. */
.shot {
  flex: 0 0 auto;
  /* min() nothing, max() a floor: a 1:1 render would otherwise give a tile too
     narrow for its own caption to set in fewer than five lines. */
  width: max(15rem, calc(var(--shot-h) * var(--shot-ar, 1.778)));
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  /* The caption used to be an opaque plate over the bottom of the tile, which
     cost 40% of every render and made the strip read as a row of letterboxed
     strips. Media and caption are now stacked: the render keeps its whole
     frame, and the caption sits on the page ground where it is legible in
     either theme. */
  position: relative;
  margin: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--surface-2);
  scroll-snap-align: start;
}
/* A 2.8:1 comparison render letterboxed into a 16:9 tile is ~110px per panel
   and unreadable. Two columns, and its own aspect, gives it ~200px per panel
   at the same media height as every other tile. */
.shot-media { position: relative; height: var(--shot-h); overflow: hidden; }
.shot-media img, .shot-media video {
  /* One treatment across the row. A per-tile `contain` left the corrector
     triptych filling the top 55% of its frame with a grey band above the
     caption, while its neighbours bled to the edges. */
  width: 100%; height: 100%; object-fit: cover;
  filter: var(--media-card-filter);
  transition: filter .3s var(--ease);
}
/* No hover scale on rail tiles. The rail scrolls horizontally under a
   stationary cursor, so every tile would zoom in and out as it passed -- and a
   flat scale(1.05) here also replaced the per-tile scale(var(--shot-zoom)),
   so a 2x framed tile snapped back to 1.05x on hover. Lift the image instead. */
.shot:hover .shot-media img, .shot:hover .shot-media video {
  filter: saturate(1.18) brightness(1.06);
}
.shot figcaption {
  position: relative;
  z-index: 1;
  flex: 1;
  padding: var(--s2) var(--s3) var(--s3);
  display: flex;
  flex-direction: column;
  gap: .15rem;
  background: var(--ink);
  border-top: 1px solid var(--line-soft);
}
.shot-label {
  font-family: var(--font-mono);
  font-size: .68rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--accent);
}
.shot-sub { font-size: .8rem; color: var(--text-dim); line-height: 1.35; }

.shot-link { position: absolute; inset: 0; z-index: 2; }

/* Narrow viewports. A 22rem tile is already close to a phone's whole width, so
   the step-down is mostly about the wide tiles, which would otherwise be twice
   the viewport and unreadable. Below 48rem the rail keeps working as a rail --
   swiping a strip is the natural phone gesture -- but every tile fits. */
@media (max-width: 64rem) { .showcase-track { --shot-h: 12rem; } }
@media (max-width: 48rem) {
  .showcase-track { --shot-h: 10rem; }
  /* A 3.4:1 plate at any readable height is wider than a phone. Cap the tile at
     the screen and letterbox that one render rather than cropping two-thirds of
     a crack off it -- the strip scrolls, so the cap costs nothing else. */
  .shot { max-width: 86vw; }
  .shot .shot-media img, .shot .shot-media video { object-fit: contain; }
  /* the mask eats a real slice of a small screen; keep only a hint of it */
  .showcase-rail {
    -webkit-mask-image: linear-gradient(to right, #000 92%, transparent);
            mask-image: linear-gradient(to right, #000 92%, transparent);
  }
}

/* A visitor who asked for less motion gets a rail that sits still and is
   scrolled by hand. site.js never starts the drift; this makes the scrollbar
   unmistakable so the strip does not read as a truncated row. */
@media (prefers-reduced-motion: reduce) {
  .showcase-rail { scrollbar-color: var(--text-dim) transparent; }
  .showcase-rail::-webkit-scrollbar-thumb { background: var(--text-dim); }
}
.u-sr {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

/* the caption is out of the image now, so it needs no special light-theme plate */
:root[data-theme="light"] .shot figcaption {
  background: var(--ink);
  border-top: 1px solid var(--line-soft);
}
:root[data-theme="light"] .shot-sub { color: var(--text); }
:root[data-theme="light"] .hero-credit { color: var(--text-dim); }

/* ---------- 19b. The hero mark ----------
   One non-simulation image on each homepage: a portrait on the personal site,
   the lab's mark on the lab site. It shares the copy's row rather than floating
   over the reel, so the composition stays a single block against the render
   instead of two things competing. */
/* Only the lab puts its mark opposite the copy; on the personal site the mark
   lives inside .hero-copy and the row has a single child. */
.hero-row {
  display: flex;
  /* centred, not bottom-aligned. With the mark on the baseline the whole
     top-right quadrant of the hero was empty while the mark sat in the
     corner; centred against the copy it balances the headline instead. */
  align-items: center;
  gap: var(--s5);
  justify-content: space-between;
}
.hero-copy { min-width: 0; flex: 1 1 auto; }

.heromark {
  margin: 0 0 .4rem;
  flex: 0 0 auto;
  width: clamp(9rem, 17vw, 15rem);
}
.heromark img { display: block; width: 100%; height: auto; }

/* The portrait sits at the head of the copy rather than opposite it, and stays
   small: a byline mark, not a headshot panel. At the larger size it covered the
   part of the frame where the render is actually moving, which is the one thing
   the hero is there to show. */
.heromark--portrait {
  width: 5.25rem;
  margin: 0 0 1.1rem;
}

/* Portrait and role line as one byline. The eyebrow's rule is suppressed on
   this variant so the pair reads as a unit, and both sit on the copy's left
   edge with the circle centered against the line of type. */
.hero-id {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin-bottom: var(--s4);
}
.hero-id .heromark--portrait { margin: 0; }
.hero-id .eyebrow { margin: 0; }
.heromark--portrait img {
  aspect-ratio: 1;
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid color-mix(in srgb, var(--accent) 60%, transparent);
  box-shadow: 0 6px 18px rgb(0 0 0 / .25);
}

/* The lab mark is the real logo, rendered from the source SVG with its cream
   ground dropped, so it sits on the render rather than on a plate. No frame,
   no shadow: it is a logo, and the moment it needs a card behind it, it reads
   as a sticker. It ships in two ink variants -- see partials/hero-mark.html --
   and the theme picks between them. */
.heromark--logo { width: clamp(11rem, 21vw, 19rem); margin: 0; }
.heromark--logo .media-theme { width: 100%; }
.heromark .media-theme--dark { display: block; }

.heromark figcaption {
  margin-top: .45rem;
  font-family: var(--font-mono);
  font-size: .62rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-faint);
  text-align: right;
}

/* Below 60rem the reel and the headline are the whole job; a decorative element
   there would push the claim under the fold. */
@media (max-width: 60rem) {
  .hero-row { display: block; }
  .heromark { display: none; }
}

/* ---------- 20b. Mechanics + methods ----------
   Two labelled rows of tags at the top of a thrust or project. The label sits
   in a fixed column so the two rows align and can be scanned as a pair, and
   collapses above the tags on a phone rather than squeezing them. */
.expertise {
  display: flex;
  flex-direction: column;
  gap: .55rem;
  /* The block had no bottom margin, so the prose began a few pixels under the
     last row of method tags -- and when the methods wrapped to two lines the
     text looked wedged against them. */
  margin: var(--s3) 0 var(--s4);
  padding-top: var(--s3);
  border-top: 1px solid var(--line-soft);
}
.expertise-row { display: flex; gap: var(--s2); align-items: baseline; }
.expertise-label {
  flex: 0 0 5.5rem;
  font-family: var(--font-mono);
  font-size: .64rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-faint);
  padding-top: .18rem;
}
.expertise-tags {
  list-style: none;
  margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: .3rem;
  min-width: 0;
}
.expertise-tags li {
  font-size: .74rem;
  line-height: 1.2;
  padding: .24rem .5rem;
  border: 1px solid var(--line-soft);
  border-radius: 999px;
  color: var(--text-dim);
  background: var(--surface-2);
  white-space: nowrap;
}
/* Methods are the more specific claim of the two, so they carry the accent and
   the mechanics read as the broader setting. */
.expertise-tags--method li {
  border-color: color-mix(in srgb, var(--accent) 32%, var(--line-soft));
  color: var(--text);
}
@media (max-width: 40rem) {
  .expertise-row { flex-direction: column; gap: .3rem; }
  .expertise-label { flex: none; padding-top: 0; }
  .expertise-tags li { white-space: normal; }
}

/* who works on a project: a labelled row matching the mechanics/methods rows
   above it, so the page reads as a set of facts about the work rather than
   alternating blocks of prose and cards. */
.whoblock { display: flex; gap: var(--s2); align-items: baseline; flex-wrap: wrap; }
.whorow { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--s3); }
.whorow li { display: flex; flex-direction: column; gap: .1rem; }
.whorow a { font-weight: 600; }
.whorow em { font-style: normal; font-size: .76rem; color: var(--text-dim); }
@media (max-width: 40rem) { .whoblock { flex-direction: column; gap: .4rem; } }

.swrow-state {
  font-family: var(--font-mono);
  font-size: .62rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  padding: .12rem .42rem;
  border-radius: 3px;
  border: 1px solid var(--line-soft);
  color: var(--text-faint);
}
.swrow-state--archived { border-color: color-mix(in srgb, #b4643c 40%, var(--line-soft)); color: #b4643c; }
/* nothing to clone -- a different fact from "archived", which had a repo */
.swrow-state--closed { border-color: var(--line); color: var(--text-faint); }

.fact-note {
  display: block;
  font-style: normal;
  font-size: .74rem;
  line-height: 1.4;
  color: var(--text-dim);
  margin: .15rem 0 .6rem;
}

/* ---------- 20c. The reasoning-cycle diagram ----------
   Interlocking chevrons in the `inferno` ramp, so the teaching figure shares a
   palette with the simulation renders. The chevrons carry their own colour and
   need no theme variant; only the loop and the note follow the tokens. */
.cycle {
  margin: var(--s4) 0;
  overflow-x: auto;
  /* Break out of the prose measure. The figure was rendering at 824px inside a
     1263px container because it inherits the reading width, which is right for
     paragraphs and wrong for a five-stage diagram. The prose column is
     left-aligned, so widening simply uses the empty space to its right; the
     viewport term keeps it honest on narrow screens. */
  width: min(57.6rem, calc(100vw - 4rem));
  max-width: none;
}
.cycle-svg {
  display: block;
  width: 100%;
  min-width: 680px;
  height: auto;
  color: var(--text-faint);        /* the return arrowhead inherits this */
}
.cyc-key {
  font-family: var(--font-sans);
  font-size: 27px;
  font-weight: 600;
  letter-spacing: -.01em;
}
.cyc-sub {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0;
}
.cyc-loop path { stroke: var(--line); stroke-width: 1.6; stroke-dasharray: 5 4; }
.cyc-note {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: .08em;
  text-transform: uppercase;
  fill: var(--text-faint);
}
.cycle figcaption {
  margin-top: var(--s2);
  font-size: .84rem;
  line-height: 1.55;
  color: var(--text-dim);
  max-width: 66ch;
}

/* Column grids for the education narrative. Three classroom examples and four
   students were paragraphs that read as a wall; the reader's actual task is to
   compare them, which wants them side by side. */
.colgrid { display: grid; gap: var(--s3); }
.colgrid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.colgrid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 64rem) {
  .colgrid--3, .colgrid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 40rem) {
  .colgrid--3, .colgrid--4 { grid-template-columns: 1fr; }
}
.colcard {
  border-top: 2px solid var(--accent);
  padding-top: var(--s2);
  display: flex; flex-direction: column; gap: .4rem;
}
.colcard--person { border-top-color: var(--line); }
.colcard-kicker {
  font-family: var(--font-mono);
  font-size: .62rem; letter-spacing: .11em; text-transform: uppercase;
  color: var(--text-faint); margin: 0;
}
.colcard h3 { margin: 0; font-size: 1.02rem; line-height: 1.25; }
.colcard h3 a { text-decoration: none; }
.colcard p:last-child { margin: 0; font-size: .86rem; line-height: 1.5; color: var(--text-dim); }
.colgrid-note {
  margin-top: var(--s3);
  font-size: .86rem; line-height: 1.55;
  color: var(--text-dim); max-width: 68ch;
}

/* the education evidence strip reuses .metrics; its values are links */
/* Stacked one-per-row it was 360px tall on a phone, pushing the page's actual
   content below two screenfuls. Two by two keeps it to a glance. The modifier
   scopes this away from the lab homepage's own metrics row. */
@media (max-width: 48rem) {
  .metrics--evidence {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s3);
  }
}
/* Breadcrumb in the eyebrow slot. It inherits .eyebrow's size and letter
   spacing so a record's head keeps the same rhythm it had when this line was
   plain text; only the links and the separator are new. */
.crumbs a { color: inherit; text-decoration: none; border-bottom: 1px solid transparent; }
.crumbs a:hover { color: var(--accent); border-bottom-color: currentColor; }
.crumbs-sep { margin-inline: .35em; opacity: .5; }
.crumbs-tail { color: var(--text-faint); }

.metric-value a { text-decoration: none; color: inherit; border-bottom: 2px solid transparent; }
.metric-value a:hover { color: var(--accent); border-bottom-color: var(--accent); }

/* ---------- 20d. Education: jump nav + materials ---------- */
.jumpnav {
  border-block: 1px solid var(--line-soft);
  background: var(--surface-2);
  padding-block: .7rem;
  /* Stacks under the site header rather than sliding beneath it: the header is
     sticky at z-index 60, so a bar at top:0 with a lower z-index simply
     disappears behind it on scroll. `--header-h` is the convention already used
     by the publication filter. */
  position: sticky;
  top: var(--header-h, 4.3rem);
  z-index: 20;
}
.jumpnav .container { display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap; }
.jumpnav-label {
  font-family: var(--font-mono);
  font-size: .64rem; letter-spacing: .12em; text-transform: uppercase;
  color: var(--text-faint);
}
.jumpnav ul { list-style: none; display: flex; flex-wrap: wrap; gap: var(--s3); margin: 0; padding: 0; }
.jumpnav a {
  font-size: .82rem;
  color: var(--text-dim);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  padding-bottom: 1px;
}
.jumpnav a:hover { color: var(--accent); border-bottom-color: var(--accent); }
/* the sticky bar would otherwise sit on top of whatever you jumped to */
[id="materials"], [id="courses"], [id="classroom"], [id="students"], [id="planned"],
[id="openness"] {
  /* clear both the header and the jump bar */
  scroll-margin-top: calc(var(--header-h, 4.3rem) + 4rem);
}

/* On a phone the wrapped bar was 134px of a 667px viewport - a fifth of the
   screen, permanently. One scrolling row instead, with the label dropped. */
@media (max-width: 48rem) {
  .jumpnav { padding-block: .5rem; }
  .jumpnav .container { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
  .jumpnav .container::-webkit-scrollbar { display: none; }
  .jumpnav-label { display: none; }
  .jumpnav ul { flex-wrap: nowrap; gap: var(--s2); }
  .jumpnav li { white-space: nowrap; }
}

.matgrid {
  display: grid;
  gap: var(--s3);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (max-width: 52rem) { .matgrid { grid-template-columns: 1fr; } }
.matcard {
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  background: var(--surface-2);
  padding: var(--s3);
  display: flex; flex-direction: column; gap: .5rem;
}
.matcard-kind {
  font-family: var(--font-mono);
  font-size: .62rem; letter-spacing: .12em; text-transform: uppercase;
  color: var(--accent); margin: 0;
}
.matcard h3 { margin: 0; font-size: 1.1rem; line-height: 1.25; }
.matcard h3 a { text-decoration: none; }
.matcard-meta {
  font-family: var(--font-mono);
  font-size: .7rem; color: var(--text-faint); margin: 0;
}
.matcard-text { font-size: .86rem; line-height: 1.5; color: var(--text-dim); margin: 0; flex: 1; }
.matcard-links { margin: .2rem 0 0; display: flex; gap: var(--s2); }
.matcard-links a {
  font-family: var(--font-mono);
  font-size: .7rem; letter-spacing: .06em; text-transform: uppercase;
}
.matgrid-note {
  margin-top: var(--s3);
  font-size: .78rem;
  color: var(--text-faint);
  max-width: 62ch;
}

/* the closing invitation on the education page */
.invite { max-width: 68ch; }
.invite h2 { margin-top: 0; }
.invite-lede { font-size: 1.05rem; line-height: 1.55; color: var(--text); }
.invite-list { list-style: none; margin: var(--s3) 0 0; padding: 0; display: grid; gap: .7rem; }
.invite-list li {
  position: relative;
  padding-left: 1.1rem;
  font-size: .92rem;
  line-height: 1.55;
  color: var(--text-dim);
}
.invite-list li::before {
  content: "";
  position: absolute; left: 0; top: .62em;
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--accent);
}
.invite-list strong { color: var(--text); }
.invite-close { margin-top: var(--s3); color: var(--text-dim); }
.invite-cta { margin-top: var(--s3); }

/* ---------- 21. Research worlds (full-viewport panels) ----------
   The visual is the argument. Each panel is one image and one name; the
   scroll snaps so a visitor moves through named research worlds rather than
   scrolling past paragraphs. */
.worlds { position: relative; scroll-snap-type: y mandatory; }

.world {
  /* Copy used to sit on top of the render with only a vertical scrim, so a
     heading in the left 45% ran straight into a bright topology field and lost
     everything after "AI for". The media has its own column now — the same
     arrangement /research/ uses, which was legible all along. */
  position: relative;
  padding-block: var(--s5);
  border-bottom: 1px solid var(--line-soft);
}
.world-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--s5);
  align-items: center;
}
.world:nth-child(even) .world-shot { order: 2; }
.world-shot {
  position: relative;
  aspect-ratio: var(--media-ar, 16 / 10);
  max-height: 26rem;
  min-width: 0;
  overflow: hidden;
  border-radius: var(--radius);
  border: 1px solid var(--line-soft);
  background: var(--surface-2);
}
@media (max-width: 60rem) {
  .world-grid { grid-template-columns: 1fr; gap: var(--s3); }
  .world:nth-child(even) .world-shot { order: 0; }
}
.world-media { position: absolute; inset: 0; }
.world-media img, .world-media video {
  width: 100%; height: 100%; object-fit: cover;
  /* deliberately lighter treatment than .band — here the image IS the content */
  filter: saturate(1.12) contrast(1.03);
}
/* no scrim needed now that nothing sits on top of the render */
.world-media--none {
  background:
    radial-gradient(70% 90% at 50% 50%, var(--accent-glow) 0%, transparent 60%),
    repeating-linear-gradient(115deg, var(--surface) 0 2px, transparent 2px 9px),
    var(--ink);
}

.world-name {
  position: relative;
  z-index: 2;
  margin-top: 22vh;
  padding: .85rem 2.2rem;
  border: 1px solid rgba(255,255,255,.85);
  color: #fff;
  background: rgba(0,0,0,.18);
  backdrop-filter: blur(3px);
  font-family: var(--font-mono);
  font-size: clamp(.8rem, .7rem + .4vw, 1rem);
  letter-spacing: .22em;
  text-transform: uppercase;
  text-align: center;
  transition: background .25s var(--ease), color .25s var(--ease), border-color .25s var(--ease);
}
.world-name:hover { background: #fff; color: #000; border-color: #fff; }

.world-credit {
  position: absolute;
  left: 50%; bottom: var(--s3);
  transform: translateX(-50%);
  z-index: 2;
  margin: 0;
  font-family: var(--font-mono);
  font-size: .64rem;
  letter-spacing: .06em;
  color: rgba(255,255,255,.62);
  text-align: center;
  padding-inline: var(--s3);
}
@media (prefers-reduced-motion: reduce) { .worlds { scroll-snap-type: none; } }

/* ---------- 22. Work pages (Gazzola project-page model) ---------- */
.work-hero {
  /* Height follows the render's own shape, capped so it never eats a whole
     screen before the title. It used to be 100vh with object-fit: cover, which
     on a 2.8:1 figure meant a ~3x zoom into an unreadable crop. */
  position: relative;
  /* width: 100% matters. With `aspect-ratio` and an auto width, a `max-height`
     clamp makes the browser shrink the *width* to preserve the ratio — which
     left a 166px white gutter down the right of the hero only. */
  width: 100%;
  aspect-ratio: var(--media-ar, 21 / 9);
  max-height: 62vh;
  min-height: 20rem;
  overflow: hidden;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: var(--surface-2);
  border-bottom: 1px solid var(--line-soft);
}
.work-hero-media { position: absolute; inset: 0; }
.work-hero-media img, .work-hero-media video {
  /* a scientific render is not a photograph: show all of it */
  width: 100%; height: 100%; object-fit: contain;
  filter: saturate(1.12) contrast(1.03);
}
.work-hero-media--none {
  background:
    radial-gradient(70% 90% at 50% 45%, var(--accent-glow) 0%, transparent 62%),
    repeating-linear-gradient(115deg, var(--surface) 0 2px, transparent 2px 9px),
    var(--ink);
}
/* Bottom-centre put the label across the seam between two panels of a
   multi-panel figure, so it read as a caption for the middle one. */
.work-hero-mark {
  position: absolute; inset: auto auto var(--s3) 0; z-index: 2;
  padding-left: var(--s4);
}
.work-hero-name {
  font-family: var(--font-mono);
  font-size: clamp(.72rem, .65rem + .3vw, .88rem);
  letter-spacing: .24em;
  text-transform: uppercase;
  color: #fff;
  display: inline-block;
  padding: .4rem 1rem;
  border-radius: 999px;
  background: rgba(6,8,12,.72);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.work-chevron {
  position: absolute;
  left: 50%; bottom: var(--s4);
  transform: translateX(-50%);
  z-index: 2;
  color: rgba(255,255,255,.85);
  animation: nudge 2.4s var(--ease) infinite;
}
.work-chevron svg { width: 26px; height: 26px; }
@keyframes nudge { 0%,100% { transform: translate(-50%, 0); } 50% { transform: translate(-50%, 6px); } }
@media (prefers-reduced-motion: reduce) { .work-chevron { animation: none; } }

.work-title {
  font-size: clamp(2rem, 1.5rem + 2.2vw, 3.2rem);
  margin-bottom: var(--s4);
  max-width: 24ch;
}
/* one wide paragraph, not a narrow column — the reference sets it full width */
.work-lede { font-size: clamp(1.02rem, .97rem + .3vw, 1.2rem); line-height: 1.6; color: var(--text-dim); max-width: 68ch; }
.work-lede p { margin-bottom: var(--s3); }


/* edge-to-edge visual slabs, two up, no chrome */
.slabs {
  display: grid;
  /* minmax(0, 1fr), not 1fr. A grid item carrying `aspect-ratio` takes its
     intrinsic width from row-height x ratio, and a bare `1fr` track will not
     shrink below that — a 6.7:1 figure blew one column out to 1847px inside a
     1430px container and pushed its neighbour off the page entirely. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1px;
  background: var(--line-soft);
  border-block: 1px solid var(--line-soft);
  margin-block: var(--s5);
}
/* A bleed hero is as wide as the page and capped at 62vh, so for any media
   narrower than about 2.7:1 the cap wins, the box stops matching
   `aspect-ratio`, and the media is fitted into a box of the wrong shape --
   background down both sides that no amount of cropping the file can remove.
   Capping the width by the media's own ratio at that same height makes the box
   match the media again: the band narrows and centres instead of padding. */
.slab.slab--bleed {   /* both classes: plain .slab sets max-width:100% below,
                         and at equal specificity the later rule would win */
  /* The width this media needs to stand 62vh tall, or the page's width,
     whichever is smaller. `min()` and not `max-width: 100%`, because the two
     are not the same thing once the parent is anything but full width: a
     percentage resolves against the track, and a track that sizes itself to
     this figure can never cap it. That combination overflowed the page on a
     narrow window. .slabs--hero below therefore stays full width and only its
     background is dropped. */
  width: min(100%, calc(62vh * (var(--media-ar, 1.6))));
  margin-inline: auto;
}
.slab {
  margin: 0; position: relative; background: var(--surface);
  /* Paired theme variants inside a slab are absolutely positioned, so the slab
     has no in-flow child to take its height from and collapsed to nothing —
     the media on every publication, software and project page was invisible.
     The ratio comes from the media itself: a fixed 16:10 box fed a 3.8:1 figure
     was 57% dead space. */
  aspect-ratio: var(--media-ar, 16 / 10);
  max-height: 62vh;
  width: 100%;
  max-width: 100%;
  min-width: 0;
}
.slabs > .slab:only-child { grid-column: 1 / -1; }
/* The hero strip shrinks to its media. `.slabs` is a grid whose background is
   the gridline colour, so once .slab--bleed stopped filling the width -- which
   is what stops a narrow render being padded out -- that background became a
   full-width band with the media floating in the middle of it. The padding had
   moved from the file to the page rather than gone. A wide render still fills
   the viewport: `fit-content` capped at 100% is the same width it always had. */
.slabs--hero {
  /* Full width, one track, no background. The band the media used to be padded
     out to came from `.slabs`'s own background showing through beside a figure
     that no longer fills the width; dropping it is enough. Sizing the section
     to the figure instead is what broke the width cap above. */
  background: transparent;
  grid-template-columns: minmax(0, 1fr);
}
/* Every other grid in this file collapses on a phone; this one did not, so a
   1529x227 plate in a 194px column came out about 29px tall with an
   absolutely-positioned caption over most of it. */
@media (max-width: 40rem) { .slabs { grid-template-columns: 1fr; } }
/* scientific figures are not photographs: letterbox rather than crop, so a
   composite plot keeps its axes and legend */
.slab img, .slab video {
  width: 100%; height: 100%;
  object-fit: contain;
  aspect-ratio: 16/10;
  background: var(--surface-2);
  padding: var(--s3);
}
.slab--bleed img, .slab--bleed video { object-fit: cover; padding: 0; }
.slab figcaption {
  position: absolute; inset: auto 0 0 0;
  padding: var(--s3);
  font-family: var(--font-mono);
  font-size: .66rem;
  letter-spacing: .05em;
  color: var(--text-dim);
  background: linear-gradient(180deg, transparent, color-mix(in srgb, var(--ink) 85%, transparent) 55%);
}

/* ---------- 19b. Figure grid on research pages ----------
   Uniform cells. Width comes from two fixed tracks, height from --slab-h, and
   the caption sits in a bar of its own under the media rather than floating
   over the bottom of it at whatever depth that media happened to end at. A
   figure wider than 2.6:1 takes both tracks (.slab--span2); the template also
   widens a narrow figure that would otherwise be left alone in a row, so the
   grid has no holes. */
.slabs--gallery {
  --slab-h: clamp(13rem, 21vw, 19rem);
  gap: 1px;
}
.slab--boxed {
  aspect-ratio: auto;
  max-height: none;
  display: flex;
  flex-direction: column;
}
.slab--span2 { grid-column: 1 / -1; }
/* A narrow figure that had to fill a row on its own gets a taller box, so a
   near-square clip is not a postage stamp on a wide grey band. */
.slab--span2.slab--solo .slab-frame { height: calc(var(--slab-h) * 1.5); }
/* Every media wrapper fills the frame, so `position: relative` here is
   load-bearing: theme-paired variants are absolutely positioned and would
   otherwise anchor to .slab and spill over the caption bar. */
.slab-frame {
  position: relative;
  height: var(--slab-h);
  display: grid;
  place-items: center;
  background: var(--surface-2);
  padding: var(--s3);
  min-width: 0;
  box-sizing: border-box;
}
/* inset:0 on an absolutely positioned child resolves to the frame's padding
   box, so the frame's own padding still shows as a margin round the media. */
.slab-frame > div,
.slab-frame > img { position: absolute; inset: 0; }
.slab-frame img,
.slab-frame video {
  width: 100%; height: 100%;
  object-fit: contain;
  aspect-ratio: auto;
  padding: 0;
  background: none;
}
/* A fixed rectangle, so captions across a row start on the same line whether
   they run to one line or two. */
.slab--boxed figcaption {
  position: static;
  inset: auto;
  display: flex;
  align-items: flex-start;
  flex: 1 0 auto;
  min-height: 3.5rem;
  padding: var(--s2) var(--s3);
  background: var(--surface);
  border-top: 1px solid var(--line-soft);
}
@media (max-width: 40rem) {
  .slabs--gallery { --slab-h: clamp(11rem, 44vw, 15rem); }
  .slab--span2 { grid-column: auto; }
}

.biblio-head { font-size: clamp(1.4rem, 1.2rem + .8vw, 2rem); margin-bottom: var(--s4); }
.biblio { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s2); }
.biblio li {
  font-size: .92rem;
  line-height: 1.5;
  color: var(--text-dim);
  padding-left: 1.1rem;
  position: relative;
}
.biblio li::before { content: "\2022"; position: absolute; left: 0; color: var(--text-faint); }
.biblio a { color: var(--text); text-decoration: underline; text-decoration-color: var(--line); text-underline-offset: 3px; }
.biblio a:hover { color: var(--accent); text-decoration-color: var(--accent); }
.biblio b { color: var(--text-dim); font-weight: 600; }

/* ---------- 23. Dark-ground media ----------
   A ParaView render on black is already sitting in the page's own ground.
   Darkening it again (as figures-on-white need) erases the field entirely,
   so these keep full luminance and take only a directional scrim. */
.is-dark-media img, .is-dark-media video { filter: saturate(1.15) contrast(1.06) !important; }

.hero-media.is-dark-media::after {
  background:
    linear-gradient(90deg, rgba(6,8,12,.92) 0%, rgba(6,8,12,.55) 42%, transparent 72%),
    linear-gradient(180deg, rgba(6,8,12,.55) 0%, transparent 26%, transparent 74%, var(--ink) 100%);
}
.world-media.is-dark-media::after,
.work-hero-media.is-dark-media::after {
  background: radial-gradient(75% 65% at 50% 84%, rgba(6,8,12,.7) 0%, transparent 72%);
}
/* in light theme dark media cannot sit under a white veil: give it its own
   dark plate rather than washing it to grey */
:root[data-theme="light"] .is-dark-media { background: #06080c; }
:root[data-theme="light"] .hero-media.is-dark-media::after {
  background:
    linear-gradient(90deg, rgba(6,8,12,.92) 0%, rgba(6,8,12,.55) 42%, transparent 72%),
    linear-gradient(180deg, rgba(6,8,12,.55) 0%, transparent 26%, transparent 74%, var(--ink) 100%);
}
:root[data-theme="light"] .hero-media.is-dark-media ~ .hero-inner h1 { color: #fff; }
:root[data-theme="light"] .hero-media.is-dark-media ~ .hero-inner .lede { color: #c3cbd8; }

/* a dark render inside a light-theme band keeps its own plate, and the copy
   over it stays light rather than following the page */
:root[data-theme="light"] .band-media.is-dark-media::after {
  background: linear-gradient(90deg, rgba(6,8,12,.94) 0%, rgba(6,8,12,.62) 48%, rgba(6,8,12,.25) 100%);
}
:root[data-theme="light"] .band-media.is-dark-media ~ .band-inner { color: #e9edf4; }
:root[data-theme="light"] .band-media.is-dark-media ~ .band-inner h2 { color: #fff; }
:root[data-theme="light"] .band-media.is-dark-media ~ .band-inner p { color: #c3cbd8; }


/* ---------- 24. Record lists (teaching, talks) ----------
   Same rhythm as .pub, but the left rail carries a date rather than a year
   and there is no venue line to italicise. */
.record-list { display: flex; flex-direction: column; }
.record {
  display: grid;
  grid-template-columns: 7.5rem 1fr;
  gap: var(--s4);
  padding-block: var(--s3);
  border-bottom: 1px solid var(--line-soft);
  align-items: start;
}
.record:first-child { border-top: 1px solid var(--line-soft); }
.record-when {
  font-family: var(--font-mono);
  font-size: .74rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-faint);
  padding-top: .3rem;
  font-variant-numeric: tabular-nums;
}
.record-title { font-family: var(--font-display); font-size: 1.1rem; line-height: 1.3; margin: 0 0 .3rem; }
.record-title a { color: var(--text); }
.record-title a:hover { color: var(--accent); }
.record-meta { font-size: .86rem; color: var(--text-dim); margin: 0; }
.record-links {
  display: flex; gap: var(--s2); flex-wrap: wrap;
  margin-top: .5rem;
  font-family: var(--font-mono); font-size: .68rem;
  letter-spacing: .06em; text-transform: uppercase;
}
.record-links a { color: var(--text-faint); }
.record-links a:hover { color: var(--accent); }

/* inline citation from the {{< cite >}} shortcode */
.cite { white-space: nowrap; border-bottom: 1px solid var(--line); }
.cite--missing { color: var(--accent-2); border-bottom-style: dotted; cursor: help; }

.slab-media { position: absolute; inset: 0; }
.slab-media img, .slab-media video { width: 100%; height: 100%; object-fit: cover; }

@media (max-width: 720px) {
  .record { grid-template-columns: 1fr; gap: .3rem; }
  .record-when { padding-top: 0; }
}

/* Alumni are part of the record but not the current roster: same component,
   quieter presence. */
.people-group--alumni { margin-top: var(--s6); }
/* alumni use the same track as everyone else; only the treatment differs */
.people-group--alumni .person-photo img { filter: grayscale(1) contrast(.95); opacity: .8; }
.people-group--alumni .person:hover .person-photo img { filter: grayscale(.4); opacity: 1; }
.people-group--alumni .person-name { font-size: .86rem; }

/* ---------- 25. Theme-paired media ----------
   Where a page ships both a dark and a light render, both elements exist and
   the theme decides which is shown. The hidden one is preload="none", so the
   second variant is not fetched unless it is actually displayed. */
.media-theme--light { display: none; }
:root[data-theme="light"] .media-theme--dark  { display: none; }
:root[data-theme="light"] .media-theme--light { display: block; }
/* a light-theme render needs no dark plate or luminance rescue */
:root[data-theme="light"] .media-theme--light.is-dark-media { background: none; }
:root[data-theme="light"] .media-theme--light.is-dark-media img,
:root[data-theme="light"] .media-theme--light.is-dark-media video { filter: none !important; }
:root[data-theme="light"] .hero-media.media-theme--light::after {
  background:
    linear-gradient(180deg, rgba(251,250,247,.72) 0%, rgba(251,250,247,.3) 30%, rgba(251,250,247,.9) 78%, var(--ink) 100%),
    linear-gradient(90deg, rgba(251,250,247,.94) 0%, rgba(251,250,247,.6) 55%, rgba(251,250,247,.25) 100%);
}

/* Overlay labels sit on whichever render is showing, so they follow the theme
   rather than assuming a dark plate underneath. */
:root[data-theme="light"] .work-hero-name {
  color: var(--ink);
  background: rgba(20,23,28,.82);
}
:root[data-theme="light"] .world-name {
  color: var(--text);
  text-shadow: 0 1px 10px rgba(251,250,247,.9);
}
:root[data-theme="light"] .world-name {
  border-color: rgba(20,23,28,.5);
  background: rgba(251,250,247,.55);
}
:root[data-theme="light"] .world-name:hover { background: var(--text); color: var(--ink); border-color: var(--text); }
:root[data-theme="light"] .work-credit,
:root[data-theme="light"] .world-credit { color: var(--text-dim); }
:root[data-theme="light"] .work-chevron { color: rgba(20,23,28,.6); }

/* A hero that ships both variants needs no theme rescue on the text: the
   light render is a real render, not a darkened one. */
:root[data-theme="light"] .hero-media.media-theme--light ~ .hero-inner h1 { color: var(--text); }
:root[data-theme="light"] .hero-media.media-theme--light ~ .hero-inner .lede { color: var(--text-dim); }
:root[data-theme="light"] .hero-media.media-theme--light ~ .hero-inner .metric-line b { color: var(--text-dim); }
/* paired media inside a slab must still fill it */
.slab .media-theme { position: absolute; inset: 0; }
.slab .media-theme video { width: 100%; height: 100%; object-fit: contain; background: var(--surface-2); padding: 0; }

/* showcase media fills its tile; letterboxing a wide render inside a tile
   wastes the tile, so cover and accept the crop */
/* Each tile frames its own source: a wide triptych wants contain, a tall
   render wants the crop pushed toward the part that matters. */
.shot-media img, .shot-media video {
  object-fit: var(--shot-fit, cover);
  object-position: var(--shot-focus, center);
  /* object-position only bites when the source and tile aspects differ. A
     16:9 render in a 16:9 tile is never cropped, so framing a region needs an
     explicit scale about the point of interest. */
  transform: scale(var(--shot-zoom, 1));
  transform-origin: var(--shot-focus, center);
}


/* Captions are bottom-anchored, so a one-line caption sat lower than a
   two-line one and the labels did not line up across the rail. Fix the block
   height and let the text start at its top. */
/* The caption is bottom-anchored, so its height decides where the label sits.
   A one-line caption was shorter than a two-line one and its label dropped.
   Fix the height above the two-line case and clamp the subtitle so a longer
   caption cannot push it out again. */
.shot figcaption {
  min-height: 5.6rem;
  justify-content: flex-start;
}
.shot-sub {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Authorship position: evidence, not decoration. Stated only when it is a
   leading position; co-authorship carries no mark. */
.pub-role {
  font-family: var(--font-mono);
  font-size: .68rem;
  letter-spacing: .04em;
  color: var(--accent);
  white-space: nowrap;
}

/* Software grouped by maturity: a flagship tool and a paper companion are
   different kinds of object and should not sit in one undifferentiated grid. */

/* ---------- 26. Claim layer ----------
   The homepage otherwise shows what the group works ON. These say what it has
   actually established, which is the part a reader can weigh. */
.claims {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
}
.claim {
  display: flex;
  flex-direction: column;
  gap: .55rem;
  padding-top: var(--s3);
  border-top: 2px solid var(--accent);
  color: var(--text-dim);
  transition: color .2s var(--ease), border-color .2s var(--ease);
}
.claim:hover { color: var(--text); border-color: var(--accent-2); }
.claim-stat {
  font-family: var(--font-display);
  font-size: clamp(1.35rem, 1.1rem + .9vw, 1.9rem);
  line-height: 1.1;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.claim-text { font-size: .87rem; line-height: 1.5; }

/* A full-viewport panel is roughly 16:9; a render of a 5:1 beam cropped to fill
   it shows only a featureless middle section. Wide sources are contained and
   sit on the page ground instead. */
.world-media--contain img,
.world-media--contain video { object-fit: contain; }
.world-media--contain { background: var(--ink); }
.world-media--contain::after {
  background:
    linear-gradient(180deg, var(--ink) 0%, transparent 16%, transparent 84%, var(--ink) 100%),
    radial-gradient(70% 60% at 50% 82%, color-mix(in srgb, var(--ink) 45%, transparent) 0%, transparent 70%);
}

/* Cross-references: a paper should lead outward to the thrust, project,
   software and grant it belongs to, not sit as a dead end. */
.crossref { display: flex; flex-direction: column; gap: .6rem; }
.crossref-row {
  display: grid;
  grid-template-columns: 7.5rem 1fr;
  gap: var(--s3);
  align-items: baseline;
  font-size: .9rem;
}
.crossref-label {
  font-family: var(--font-mono);
  font-size: .66rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-faint);
}
@media (max-width: 720px) { .crossref-row { grid-template-columns: 1fr; gap: .15rem; } }

/* ---------- 27. Software records ----------
   A catalogue of seven codes reads as a list, not a gallery. Rows keep every
   entry the same weight regardless of whether it has media, and a group of one
   no longer stretches into a full-width panel. */
.swgroup { margin-bottom: var(--s5); max-width: 74rem; }
.swgroup:last-child { margin-bottom: 0; }
.swgroup-label {
  font-family: var(--font-mono); font-size: .68rem; font-weight: 500;
  letter-spacing: .16em; text-transform: uppercase; color: var(--text-faint);
  margin: 0 0 var(--s2); padding-bottom: .5rem;
  border-bottom: 1px solid var(--line-soft);
  display: flex; align-items: baseline; gap: .6rem;
}
.swgroup-count { color: var(--accent); letter-spacing: 0; }

.swlist { display: flex; flex-direction: column; }
/* Course rows are short - a title, one line, a level and a list of terms - so a
   single column left the right half of a desktop page empty. The row's own
   two-column interior collapses inside a narrower cell. */
.swlist--2col {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--s4);
}
@media (max-width: 60rem) { .swlist--2col { grid-template-columns: 1fr; } }
@media (min-width: 60.01rem) {
  .swlist--2col .swrow { grid-template-columns: minmax(0, 1fr); }
  .swlist--2col .swrow-meta { align-items: flex-start; }
}
.swrow {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 13rem);
  gap: var(--s2) var(--s4);
  align-items: baseline;
  padding: var(--s3) 0;
  border-bottom: 1px solid var(--line-soft);
  transition: background .18s var(--ease);
}
/* A small thumbnail, not a media card. These are rows precisely because 16:10
   media boxes made a group of one stretch across the page. */
.swrow--thumbed { grid-template-columns: 6rem minmax(0, 1fr) minmax(0, 13rem); }
.swrow-thumb {
  width: 6rem; aspect-ratio: 8 / 5; overflow: hidden;
  border-radius: 3px; border: 1px solid var(--line-soft);
  background: var(--surface-2);
  align-self: start;
}
.swrow-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.swrow-thumb--none {
  display: flex; align-items: center; justify-content: center;
  background: linear-gradient(140deg, var(--surface-2) 0%, var(--surface) 100%);
}
.swrow-thumb--none span {
  font-family: var(--font-mono); font-size: .62rem; letter-spacing: .08em;
  color: var(--text-faint);
}
.swrow:last-child { border-bottom: 0; }
.swrow::before {
  content: ""; position: absolute; inset: 0 calc(var(--s3) * -1);
  border-radius: var(--radius); background: transparent; z-index: 0;
  transition: background .18s var(--ease);
}
.swrow:hover::before { background: var(--surface); }
.swrow > * { position: relative; z-index: 1; }
.swrow-title {
  font-family: var(--font-display); font-size: 1.28rem; font-weight: 500;
  margin: 0 0 .3rem; line-height: 1.2;
}
.swrow-title a { color: var(--text); }
.swrow:hover .swrow-title a { color: var(--accent); }
.swrow-desc { font-size: .89rem; color: var(--text-dim); margin: 0; max-width: 54ch; }
.swrow-meta { display: flex; flex-direction: column; align-items: flex-start; gap: .45rem; }
.swrow-tags { display: flex; flex-wrap: wrap; align-items: baseline; gap: .5rem; }
.swrow-lang {
  font-family: var(--font-mono); font-size: .64rem; letter-spacing: .08em;
  text-transform: uppercase; color: var(--accent);
  border: 1px solid var(--accent-dim); border-radius: 2px; padding: .1rem .4rem;
}
.swrow-maturity {
  font-family: var(--font-mono); font-size: .64rem; letter-spacing: .06em;
  color: var(--text-faint);
}
.swrow-links {
  display: flex; flex-wrap: wrap; gap: .9rem; z-index: 2;
  font-family: var(--font-mono); font-size: .68rem;
}
.swrow-links a { color: var(--text-dim); border-bottom: 1px solid var(--line); padding-bottom: 1px; }
.swrow-links a:hover { color: var(--accent); border-bottom-color: var(--accent); }

@media (max-width: 40rem) {
  .swrow, .swrow--thumbed { grid-template-columns: 1fr; }
  .swrow-thumb { display: none; }
  /* the hover plate bleeds 20px past each side to sit under the row's padding;
     with no page gutter left at this width that became horizontal overflow */
  .swrow::before { inset: 0; }
}

/* Software appearing among "related work" keeps the row form inside a bordered
   block, so it does not masquerade as a project media card. */
.related-sw {
  border: 1px solid var(--line-soft); border-radius: var(--radius);
  padding: 0 var(--s3); background: var(--surface);
}

/* ---------- 28. Person pages ----------
   A profile is a portrait plus a claim about what someone is working on; the
   rest is their record. */
.person-head { display: flex; align-items: flex-start; gap: var(--s4); flex-wrap: wrap; }
.person-head-avatar {
  width: 8.5rem; height: 8.5rem; border-radius: 50%; object-fit: cover;
  border: 1px solid var(--line); flex: none;
  filter: grayscale(.5) contrast(1.06);
}
.person-head-avatar--mono {
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-display); font-size: 3.4rem; color: var(--text-faint);
  background: linear-gradient(140deg, var(--surface-2) 0%, var(--surface) 100%);
  filter: none;
}
.person-head h1 { margin: 0; }
.person-body { max-width: 62ch; }

.callout {
  border-left: 2px solid var(--accent);
  background: var(--surface);
  padding: var(--s3);
  border-radius: 0 var(--radius) var(--radius) 0;
  margin: 0 0 var(--s4);
}
.callout p:last-child { margin-bottom: 0; }
.callout-label {
  font-family: var(--font-mono); font-size: .64rem; letter-spacing: .16em;
  text-transform: uppercase; color: var(--text-faint); margin: 0 0 .4rem;
}

.taglist { display: flex; flex-wrap: wrap; gap: .45rem; }
.tag {
  font-family: var(--font-mono); font-size: .66rem; letter-spacing: .05em;
  color: var(--text-dim); border: 1px solid var(--line-soft);
  border-radius: 2px; padding: .18rem .5rem;
}

/* ---------- 29. Course pages ----------
   Prose on the left, the facts a reader checks (code, department, terms taught)
   pinned beside it rather than buried in the text. */
.course-body {
  display: grid;
  /* same measure as .recordgrid, so a course page and a publication page do
     not read as two different templates */
  grid-template-columns: minmax(0, 68ch) minmax(0, 17rem);
  gap: var(--s5) var(--s6);
  align-items: start;
}
.course-body .prose { max-width: none; }
.course-facts {
  position: sticky; top: calc(var(--header-h, 4rem) + var(--s3));
  display: flex; flex-direction: column; gap: var(--s3);
  border-left: 1px solid var(--line-soft); padding-left: var(--s3);
  font-size: .88rem; color: var(--text-dim);
}
.fact-label {
  display: block;
  font-family: var(--font-mono); font-size: .62rem; letter-spacing: .16em;
  text-transform: uppercase; color: var(--text-faint); margin-bottom: .3rem;
}
.fact-list { list-style: none; margin: 0; padding: 0; }
.fact-list li { padding: .12rem 0; }
.swrow-terms {
  font-family: var(--font-mono); font-size: .64rem; color: var(--text-faint);
  margin: 0; line-height: 1.5;
}
@media (max-width: 52rem) {
  .course-body { grid-template-columns: 1fr; }
  .course-facts { position: static; border-left: 0; padding-left: 0;
    border-top: 1px solid var(--line-soft); padding-top: var(--s3); }
}

/* ---------- 30. Body figures ----------
   Replaces the legacy markdown-table-of-GIFs pattern. One or two items per
   figure, captions under each when there are two. */
.bodyfig {
  /* the body text runs to ~92ch; a figure wider than that reads as a banner,
     and a square render at full container width filled the whole viewport */
  margin: var(--s5) 0;
  max-width: min(100%, 60rem);
}
.bodyfig-items { display: grid; gap: var(--s3); grid-template-columns: 1fr; }
.bodyfig--2 .bodyfig-items { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.bodyfig-item { display: flex; flex-direction: column; align-items: flex-start; }
.bodyfig-item img, .bodyfig-item video {
  display: block;
  /* Sized by both caps at once, with the aspect preserved between them: a
     figure fills the column, and a tall one shrinks in *both* directions when
     it hits the height cap. `width: 100%` here instead meant the element kept
     the full column width while the picture inside it shrank to 68vh, and
     `contain` then drew background down both sides — a portrait flyer rendered
     372px of image inside a 520px box. `max` still opts a small legacy clip out
     of being blown up. */
  width: auto; height: auto;
  max-width: min(100%, var(--fig-max, 100%));
  max-height: 68vh;
  object-fit: contain;
  border-radius: var(--radius); border: 1px solid var(--line-soft);
  background: var(--surface-2);
}
.bodyfig figcaption {
  margin-top: .7rem; font-size: .82rem; line-height: 1.55; color: var(--text-faint);
}
.bodyfig-item figcaption { margin-top: .55rem; }
@media (max-width: 44rem) {
  .bodyfig--2 .bodyfig-items { grid-template-columns: 1fr; }
}

/* ---------- Back to top ----------
   The only fixed element on the site, so the bottom-right corner is its own.
   It sits under the header's z-index (60) and hides itself while the mobile
   nav panel is open, so it never lands on top of an overlay. */
.totop {
  position: fixed;
  right: max(var(--s4), env(safe-area-inset-right));
  bottom: max(var(--s4), env(safe-area-inset-bottom));
  z-index: 40;
  width: 2.75rem; height: 2.75rem;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: blur(6px);
  color: var(--text-dim);
  cursor: pointer;
  box-shadow: 0 1px 12px rgba(0,0,0,.12);
  opacity: 0; transform: translateY(.4rem);
  transition: opacity .2s var(--ease), transform .2s var(--ease),
              color .15s var(--ease), border-color .15s var(--ease);
}
/* `display: grid` above outranks the UA sheet's `[hidden] { display: none }`,
   so without this the button stays laid out and hit-testable at opacity 0 and
   swallows clicks in the corner of every short page. */
.totop[hidden] { display: none; }
.totop svg { width: 1.05rem; height: 1.05rem; }
.totop.is-shown { opacity: 1; transform: none; }
.totop:hover { color: var(--accent); border-color: var(--accent); }
.totop:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.nav-open .totop { opacity: 0; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
  .totop { transition: none; transform: none; }
}

/* ---------- 31. Publication list ----------
   39 records is a wall unless it is broken by year and filterable by thrust. */
.pubfilter { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: var(--s4); }
.chip {
  font-family: var(--font-mono); font-size: .7rem; letter-spacing: .04em;
  color: var(--text-dim); background: none;
  border: 1px solid var(--line-soft); border-radius: 999px;
  padding: .34rem .8rem; cursor: pointer;
  transition: border-color .15s var(--ease), color .15s var(--ease), background .15s var(--ease);
}
.chip span { color: var(--text-faint); margin-left: .3rem; }
.chip:hover { border-color: var(--line); color: var(--text); }
.chip.is-on { border-color: var(--accent); color: var(--accent); background: var(--accent-wash, transparent); }
.chip.is-on span { color: var(--accent); }

.pub-year-group { margin-bottom: var(--s4); }
.pub-year-label {
  font-family: var(--font-mono); font-size: .7rem; font-weight: 500;
  letter-spacing: .16em; color: var(--text-faint);
  margin: 0 0 var(--s2); padding-bottom: .45rem;
  border-bottom: 1px solid var(--line-soft);
}
.pub-status {
  font-family: var(--font-mono); font-size: .66rem; letter-spacing: .05em;
  color: var(--accent);
}
.pub-empty { color: var(--text-faint); font-size: .9rem; }

/* ---------- 32. Cite block ----------
   Every publication bundle carries a cite.bib; this surfaces it without letting
   a wall of BibTeX compete with the abstract. */
.citebox { margin-top: var(--s5); border-top: 1px solid var(--line-soft); padding-top: var(--s3); }
.citebox > summary {
  cursor: pointer; list-style: none;
  font-family: var(--font-mono); font-size: .7rem; letter-spacing: .12em;
  text-transform: uppercase; color: var(--text-faint);
}
.citebox > summary::-webkit-details-marker { display: none; }
.citebox > summary::before { content: "+ "; color: var(--accent); }
.citebox[open] > summary::before { content: "− "; }
.citebox > summary:hover { color: var(--accent); }
.citebox-body { position: relative; margin-top: var(--s3); }
.citebox-body pre {
  max-height: 22rem; overflow: auto;
  font-size: .74rem; line-height: 1.55;
  white-space: pre-wrap; word-break: break-word;
}
.citebox-copy {
  position: absolute; top: .5rem; right: .5rem; z-index: 1;
  font-family: var(--font-mono); font-size: .64rem; letter-spacing: .06em;
  color: var(--text-dim); background: var(--surface);
  border: 1px solid var(--line); border-radius: 3px;
  padding: .25rem .55rem; cursor: pointer;
}
.citebox-copy:hover { color: var(--accent); border-color: var(--accent); }

/* ---------- 33. Curriculum vitae ----------
   Assembled from the same records as the rest of the site, and laid out so it
   prints as a document rather than a screenshot of a web page. */
.cv-body { padding-bottom: var(--s6); }
.cv-block { margin-top: var(--s5); }
.cv-block h2 {
  font-size: clamp(1.1rem, 1rem + .4vw, 1.35rem);
  margin: 0 0 var(--s3);
  padding-bottom: .5rem;
  border-bottom: 1px solid var(--line-soft);
  display: flex; align-items: baseline; gap: .6rem;
}
.cv-count {
  font-family: var(--font-mono); font-size: .68rem; letter-spacing: .06em;
  color: var(--text-faint); font-weight: 400;
}
.cv-row strong { font-weight: 500; font-family: var(--font-display); font-size: 1.05rem; }
.cv-row {
  display: grid;
  grid-template-columns: 9rem minmax(0, 1fr);
  gap: var(--s2) var(--s4);
  padding-block: .55rem;
  align-items: baseline;
  border-bottom: 1px solid var(--line-soft);
}
.cv-row:last-child { border-bottom: 0; }
.cv-when {
  font-family: var(--font-mono); font-size: .7rem; letter-spacing: .04em;
  color: var(--text-faint); line-height: 1.5;
}
.cv-biblio { margin: 0; padding-left: 1.4rem; font-size: .92rem; line-height: 1.6; }
.cv-biblio li { padding-block: .4rem; border-bottom: 1px solid var(--line-soft); }
.cv-biblio li:last-child { border-bottom: 0; }
.cv-print {
  font-family: inherit; font-size: inherit; color: inherit;
  background: none; border: 0; padding: 0; cursor: pointer;
  border-bottom: 1px solid var(--line);
}
.cv-print:hover { color: var(--accent); border-bottom-color: var(--accent); }
@media (max-width: 44rem) {
  .cv-row { grid-template-columns: 1fr; gap: .2rem; }
}

@media print {
  /* the page furniture is not part of the document */
  .site-header, .site-footer, .cv-noprint, .skip-link { display: none !important; }
  :root { --ink: #fff; }
  html, body { background: #fff !important; color: #111 !important; }
  a { color: #111 !important; text-decoration: none; }
  .cv-biblio a::after { content: ""; }
  .page-head { padding-block: 0 var(--s3) !important; border: 0; }
  .cv-block { break-inside: auto; margin-top: 1.1rem; }
  .cv-block h2 { break-after: avoid; font-size: 1.05rem; }
  .cv-row, .cv-biblio li { break-inside: avoid; }
  .container { max-width: none !important; padding-inline: 0 !important; }
  .record-meta, .cv-when { color: #444 !important; }
  @page { margin: 16mm 14mm; }
}


/* ---------- 34. Fixes from review ---------- */

/* A section that is only a heading plus a "more" link left 104px of void
   between the heading and the bands it introduces. */
.section--heading-only { padding-block: var(--s5) var(--s3); }
.section--heading-only .section-head { margin-bottom: 0; }

/* Record and course rows pinned their meta column to the far right of a 92rem
   container, leaving a ~530px empty band running down every row. Cap the row
   so the two columns stay in conversation with each other. */
.swgroup, .record-list, .swlist { max-width: 74rem; }
.people-group, .cv-block { max-width: none; }

/* The header is a translucent wash over whatever is behind it. Over a
   full-bleed render that produced a split or smeared backdrop; a heavier blur
   and a real surface tint keep it reading as chrome. */
.site-header {
  background: color-mix(in srgb, var(--ink) 88%, transparent);
  backdrop-filter: saturate(140%) blur(20px);
  -webkit-backdrop-filter: saturate(140%) blur(20px);
}

@media (max-width: 900px) {
  /* the panel spanned only part of the width and let page content show through */
  .site-nav {
    width: 100%;
    box-shadow: 0 18px 40px -20px rgba(0,0,0,.55);
  }
  /* the theme control belongs with the toggle, not orphaned in the panel */
  .site-nav .theme-toggle { align-self: stretch; justify-content: center; }
  /* a band's copy sits over the render at this width, so the scrim has to be
     strong enough to carry text on either theme */
  .band-inner { padding-inline: var(--s3); }
  .band-media::after {
    background: linear-gradient(180deg,
      color-mix(in srgb, var(--ink) 55%, transparent) 0%,
      color-mix(in srgb, var(--ink) 92%, transparent) 45%,
      var(--ink) 100%);
  }
}

/* ---------- 35. Research index ----------
   The section index is not the homepage pitch repeated: each thrust shows what
   is filed under it, so this is the page you come to for the whole of one line
   of work. */
.thrust {
  display: grid;
  grid-template-columns: minmax(0, 26rem) minmax(0, 1fr);
  gap: var(--s5);
  align-items: start;
  padding-block: var(--s5);
  border-bottom: 1px solid var(--line-soft);
}
.thrust:last-of-type { border-bottom: 0; }
.thrust-media { position: relative; }
.thrust-shot {
  position: relative;
  /* the ratio comes from the render; a fixed 16:10 letterboxed a 2.8:1 figure
     into 43% dead space */
  aspect-ratio: var(--media-ar, 16 / 10);
  overflow: hidden;
  border-radius: var(--radius);
  border: 1px solid var(--line-soft);
  background: var(--surface-2);
}
.thrust-shot img, .thrust-shot video { width: 100%; height: 100%; object-fit: cover; }
.thrust-body h2 {
  font-size: clamp(1.35rem, 1.15rem + .9vw, 1.95rem);
  margin: .4rem 0 var(--s2);
  line-height: 1.15;
}
.thrust-body h2 a { color: var(--text); }
.thrust-body h2 a:hover { color: var(--accent); }
.thrust-lede { color: var(--text-dim); margin: 0 0 var(--s3); max-width: 60ch; }
.thrust-counts {
  display: flex; flex-wrap: wrap; gap: var(--s3);
  margin: 0 0 var(--s3); padding: 0;
  border-top: 1px solid var(--line-soft);
  border-bottom: 1px solid var(--line-soft);
  padding-block: .6rem;
}
.thrust-counts div { display: flex; align-items: baseline; gap: .4rem; }
.thrust-counts dt {
  font-family: var(--font-mono); font-size: .64rem; letter-spacing: .12em;
  text-transform: uppercase; color: var(--text-faint);
}
.thrust-counts dd { margin: 0; font-size: .92rem; color: var(--text); }
.thrust-list { font-size: .88rem; margin: 0 0 .5rem; color: var(--text-dim); }
.thrust-list > span {
  font-family: var(--font-mono); font-size: .62rem; letter-spacing: .12em;
  text-transform: uppercase; color: var(--text-faint); margin-right: .5rem;
}
.thrust-more { margin: var(--s3) 0 0; }
@media (max-width: 60rem) {
  .thrust { grid-template-columns: 1fr; gap: var(--s3); }
}


/* ---------- 36. About head ---------- */
.about-head { display: flex; align-items: flex-start; gap: var(--s5); flex-wrap: wrap; }
.about-portrait {
  width: 15rem; flex: none;
  aspect-ratio: 4 / 5; object-fit: cover;
  border-radius: var(--radius); border: 1px solid var(--line-soft);
  filter: grayscale(.25) contrast(1.03);
}
.about-head h1 { margin: 0; }
.about-head > div { flex: 1 1 24rem; }
@media (max-width: 44rem) { .about-portrait { width: 10rem; } }

.plainlist { list-style: none; margin: .4rem 0 0; padding: 0; }
.plainlist li {
  font-size: .88rem; color: var(--text-dim); line-height: 1.5;
  padding: .3rem 0; border-bottom: 1px solid var(--line-soft);
}
.plainlist li:last-child { border-bottom: 0; }

/* ---------- 37. Round-2 fixes ---------- */

/* The closing pointer on the research index floated alone in an empty band. */
.thrust-outro {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: var(--s3);
  margin-top: var(--s4); padding-top: var(--s3);
  border-top: 1px solid var(--line-soft);
  max-width: 74rem;
}
.thrust-outro p { margin: 0; color: var(--text-dim); }

/* Link affordance. Titles that navigate looked like plain text: nothing said
   they were clickable until the cursor was already on them. */
.pub-title a, .swrow-title a, .card-title a, .thrust-body h2 a, .record-title a {
  background-image: linear-gradient(var(--line), var(--line));
  background-size: 0% 1px;
  background-position: 0 100%;
  background-repeat: no-repeat;
  transition: background-size .22s var(--ease), color .18s var(--ease);
}
.pub-title a:hover, .swrow-title a:hover, .card-title a:hover,
.thrust-body h2 a:hover, .record-title a:hover { background-size: 100% 1px; }
/* the mono link rows read as labels; a rule makes them read as links */
.record-links a, .swrow-links a, .pub-links a {
  border-bottom: 1px solid var(--line-soft);
  padding-bottom: 1px;
}
.record-links a:hover, .swrow-links a:hover, .pub-links a:hover {
  color: var(--accent); border-bottom-color: var(--accent);
}
/* an email address is not a label; do not shout it */
.record-links a[href^="mailto:"] { text-transform: none; letter-spacing: .01em; }

/* The monogram fallback was light grey on light grey. */
.person-mono { color: var(--text-dim); }
:root[data-theme="light"] .person-mono {
  background: linear-gradient(140deg, #e7e5df 0%, #f3f1ec 100%);
}

/* A group of one stretched across a five-column grid, leaving four empty
   columns and a row that read as 80% white. */
.people-grid { grid-template-columns: repeat(auto-fill, minmax(min(100%, 13rem), 14rem)); }
.people-group .people-grid { justify-content: start; }

/* One crop and one treatment across a mismatched set of photographs does more
   for the page than any individual picture can. */
.person-photo img { filter: grayscale(.55) contrast(1.06) brightness(1.02); }
.person:hover .person-photo img { filter: grayscale(.15) contrast(1.02); }

/* A record page: prose at a reading measure, and the things a reader acts on
   in the column that was otherwise empty. */
.recordgrid {
  display: grid;
  grid-template-columns: minmax(0, 68ch) minmax(0, 17rem);
  gap: var(--s5) var(--s6);
  align-items: start;
}
.recordrail {
  position: sticky; top: calc(4rem + var(--s3));
  display: flex; flex-direction: column; gap: var(--s3);
  border-left: 1px solid var(--line-soft); padding-left: var(--s3);
  font-size: .88rem; color: var(--text-dim);
}
.recordrail .record-links { flex-direction: column; align-items: flex-start; gap: .35rem; }
.recordrail .citebox { margin-top: 0; border-top: 0; padding-top: 0; }
.recordrail .citebox-body pre { max-height: 16rem; font-size: .66rem; }
.record-h {
  font-family: var(--font-mono); font-size: .68rem; letter-spacing: .16em;
  text-transform: uppercase; color: var(--text-faint);
  margin: 0 0 var(--s2);
}
@media (max-width: 64rem) {
  .recordgrid { grid-template-columns: 1fr; }
  .recordrail {
    position: static; border-left: 0; padding-left: 0;
    border-top: 1px solid var(--line-soft); padding-top: var(--s3);
  }
}

/* Selected work: the finding first, the citation under it. A list of titles
   says what was worked on; a list of findings says what came out. */
.findings { display: grid; gap: var(--s3); grid-template-columns: repeat(auto-fit, minmax(min(100%, 24rem), 1fr)); }
.finding {
  padding: var(--s3);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  background: var(--surface);
  display: flex; flex-direction: column; gap: var(--s2);
}
.finding-text {
  margin: 0; font-size: 1rem; line-height: 1.5; color: var(--text);
}
.finding-cite {
  margin: auto 0 0; padding-top: var(--s2);
  border-top: 1px solid var(--line-soft);
  font-size: .82rem; line-height: 1.45;
}
.finding-cite a { color: var(--text-dim); }
.finding-cite a:hover { color: var(--accent); }
.finding-cite span {
  display: block; margin-top: .25rem;
  font-family: var(--font-mono); font-size: .66rem; letter-spacing: .05em;
  color: var(--text-faint);
}

/* The strip is evidence for the claim the hero just made; it needs a line
   saying what it is, and that line used to float alone in the hero corner. */
.showcase-head {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: var(--s2);
  padding-block: var(--s3) var(--s2);
}
.showcase-credit {
  margin: 0;
  font-family: var(--font-mono); font-size: .66rem; letter-spacing: .04em;
  color: var(--text-faint);
}

/* Where a thrust is in its life. The newest one leads the site and has one
   paper, under review; the page says so rather than leaving it to be found. */
.thrust-stage {
  font-family: var(--font-mono); font-size: .66rem; letter-spacing: .05em;
  color: var(--accent);
  margin: 0 0 var(--s2);
  padding: .3rem .6rem;
  border: 1px solid currentColor;
  border-radius: 2px;
  display: inline-block;
}


/* Body scroll is locked while the nav panel is open. */
:root.nav-open, :root.nav-open body { overflow: hidden; }
@media (min-width: 901px) { :root.nav-open, :root.nav-open body { overflow: auto; } }

/* The hero standing line wrapped mid-list and left a separator dangling at the
   end of a line. Each item carries its own separator instead. */
.metric-line { display: flex; flex-wrap: wrap; align-items: baseline; gap: .35rem 1rem; }
.metric-line span[aria-hidden] { display: none; }
.metric-line b + b::before { content: ""; }
.metric-line b { position: relative; }
.metric-line b:not(:last-child)::after {
  content: "/";
  color: var(--text-faint);
  margin-left: 1rem;
}
@media (max-width: 44rem) {
  .metric-line b:not(:last-child)::after { content: ""; margin: 0; }
  .metric-line { flex-direction: column; gap: .3rem; }
}


/* Section heads share the row width, so the rule above a list lines up with
   the rules inside it. */
.swgroup .section-head, .pub-list + .section-head { max-width: 74rem; }


/* A figure much wider than its slot is letterboxed, not cropped: a three-panel
   comparison cut mid-label communicates nothing. */
/* Letterboxing is for a small slot, where a `cover` crop would cut a panel
   label off a multi-panel figure. A full-bleed band is not a small slot: there
   the same rule left one band painting 1122px of its 1430 while its neighbours
   bled to the edge. Scoped to the slots it was written for. */
.card-media.is-wide-media img, .card-media.is-wide-media video,
.card-media.is-narrow-media img, .card-media.is-narrow-media video,
.thrust-shot.is-wide-media img, .thrust-shot.is-wide-media video,
.thrust-shot.is-narrow-media img, .thrust-shot.is-narrow-media video,
.swrow-thumb.is-wide-media img { object-fit: contain; }
.card-media.is-wide-media, .card-media.is-narrow-media,
.thrust-shot.is-wide-media, .thrust-shot.is-narrow-media { background: var(--surface-2); }

/* ---------- 38. Body figures in dark theme ----------
   Most body figures are paper exports on white: matplotlib axes, Paraview
   colorbars, baked-in labels. There is no dark variant of any of them and
   inverting a colormapped field would misrepresent the data, so instead they
   get a light plate to sit on — the figure keeps its own colours and reads as a
   printed plate on the page rather than glaring off it. */
:root[data-theme="dark"] .bodyfig-item img,
:root[data-theme="dark"] .bodyfig-item video {
  background: #f7f6f3;
  padding: var(--s2);
  border-color: rgba(255,255,255,.14);
}
/* a render that is already dark-ground needs none of that */
:root[data-theme="dark"] .bodyfig-item .media-theme--dark img,
:root[data-theme="dark"] .bodyfig-item .media-theme--dark video {
  background: none; padding: 0;
}

/* ---------- 39. Trajectory ----------
   The research program's spine, drawn as one continuous line rather than as a
   list of rows that happen to be in date order. */
.track {
  list-style: none; margin: 0; padding: 0 0 0 var(--s4);
  position: relative; max-width: 62rem;
}
.track::before {
  content: ""; position: absolute; left: 5px; top: .55rem; bottom: .55rem;
  width: 1px; background: var(--line-soft);
}
.track-step { position: relative; padding-block: 0 var(--s4); }
.track-step:last-child { padding-bottom: 0; }
.track-step::before {
  content: ""; position: absolute; left: calc(var(--s4) * -1); top: .5rem;
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--ink); border: 1px solid var(--accent);
}
.track-step:last-child::before { background: var(--accent); }
.track-years {
  font-family: var(--font-mono); font-size: .68rem; letter-spacing: .1em;
  color: var(--accent); display: block; margin-bottom: .2rem;
}
.track-title {
  font-family: var(--font-display); font-size: 1.2rem; font-weight: 500;
  margin: 0 0 .4rem; line-height: 1.25;
}
.track-text { margin: 0; color: var(--text-dim); font-size: .93rem; max-width: 62ch; }

.track-now { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s3); max-width: 74rem; }
.track-now li {
  display: grid; grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);
  gap: var(--s2) var(--s4); align-items: baseline;
  padding-block: var(--s2); border-bottom: 1px solid var(--line-soft);
}
.track-now li:last-child { border-bottom: 0; }
.track-now a { font-family: var(--font-display); font-size: 1.08rem; color: var(--text); }
.track-now a:hover { color: var(--accent); }
.track-now span { color: var(--text-dim); font-size: .9rem; }
@media (max-width: 52rem) { .track-now li { grid-template-columns: 1fr; gap: .2rem; } }

/* ---------- 40. Not found ---------- */
.notfound-link {
  display: flex; flex-direction: column; gap: .3rem;
  padding: var(--s3);
  border: 1px solid var(--line-soft); border-radius: var(--radius);
  background: var(--surface); color: inherit;
  transition: border-color .18s var(--ease);
}
.notfound-link:hover { border-color: var(--accent); }
.notfound-name { font-family: var(--font-display); font-size: 1.1rem; }
.notfound-link:hover .notfound-name { color: var(--accent); }
.notfound-desc { font-size: .85rem; color: var(--text-dim); line-height: 1.45; }


/* Finished work is part of the record, not the pitch. Same convention as the
   alumni block on the people page. */
.is-retired .card-media img, .is-retired .card-media video {
  filter: saturate(.35) contrast(1.02) brightness(1.01);
  transition: filter .3s var(--ease);
}
.is-retired .card:hover .card-media img,
.is-retired .card:hover .card-media video { filter: saturate(.85); }
.is-retired .card-title { font-size: 1.05rem; }


/* Portrait sources are uneven — a studio shot on black, a phone photo against a
   white wall. One treatment per theme brings them to the same weight instead of
   letting the brightest one dominate the page. */
:root[data-theme="dark"] .person-photo img,
:root[data-theme="dark"] .person-head-avatar {
  filter: grayscale(.6) contrast(1.1) brightness(.82);
}
:root[data-theme="dark"] .person:hover .person-photo img { filter: grayscale(.2) contrast(1.04) brightness(.92); }


/* Thrust panels: the visual is the ground, the copy is the point. */
.world-inner { min-width: 0; }
.world-inner > * { max-width: 46ch; }
.world-index {
  font-family: var(--font-mono); font-size: .72rem; letter-spacing: .18em;
  color: var(--accent); display: block; margin-bottom: .5rem;
}
.world-title {
  font-size: clamp(1.5rem, 1.2rem + 1.4vw, 2.4rem);
  margin: var(--s2) 0; line-height: 1.14;
}
.world-sub { color: var(--text-dim); margin: 0 0 var(--s3); }
.world-counts {
  display: flex; flex-wrap: wrap; gap: .5rem 1.2rem; margin: 0 0 var(--s3);
  font-family: var(--font-mono); font-size: .66rem; letter-spacing: .06em;
  color: var(--text-faint);
}
/* Short counts stay on one line; the stage note is a sentence and must wrap —
   at 390px it was forcing the whole panel to 474px. */
.world-counts span { white-space: nowrap; }
.world-counts .world-stage { white-space: normal; }
.worlds { scroll-snap-type: none; }


/* A theme swap changes background and colour on elements whose transitions run
   at different rates, so a filled button's label went briefly invisible against
   its own fill. Transitions are suppressed for the duration of the swap only —
   hover states elsewhere are untouched. */
:root.is-theming, :root.is-theming * {
  transition: none !important;
}


/* The thrust name was a bordered pill because it used to float over a render.
   In its own column it is an eyebrow like every other. */
.world-inner .world-name {
  display: inline-block; padding: 0; margin: 0; border: 0; background: none;
  backdrop-filter: none; -webkit-backdrop-filter: none;
  font-family: var(--font-mono); font-size: .68rem; letter-spacing: .18em;
  text-transform: uppercase; color: var(--accent);
}
.world-inner .world-name:hover { color: var(--text); background: none; }

/* One measure for every list page. /grant/ and /post/ were 1394px while
   /teaching/ and /software/ were 1216, and a group label copied from the wide
   one overhung its own rows by 178px. */
.record-list, .feed, .pub-list { max-width: 74rem; }


/* The PI stood alone in a five-track row. Two tracks and a larger name give the
   card the weight it should have without breaking the grid. */
.person--lead { grid-column: span 2; }
.person--lead .person-link { flex-direction: row; align-items: flex-start; gap: var(--s3); }
.person--lead .person-photo { width: 13rem; flex: none; }
.person--lead .person-name { font-family: var(--font-display); font-size: 1.35rem; font-weight: 500; }
@media (max-width: 44rem) {
  .person--lead { grid-column: span 1; }
  .person--lead .person-link { flex-direction: column; }
  .person--lead .person-photo { width: 100%; }
}

/* ---------- 31. Header nav, mid widths ----------
   The nav collapses behind the toggle at 900px, but the personal site's
   twelve-item row needs about 1150px to fit on one line. In between, the
   theme control wrapped onto a second line and pushed the header to double
   height. Tighten the row through that band rather than collapsing the nav
   early on the wider screens where it does fit. */
@media (min-width: 1101px) and (max-width: 1360px) {
  .site-nav { gap: .72rem; }
  .site-nav a { font-size: .78rem; }
  .site-nav a.is-cta { padding: .3rem .62rem; }
}

/* ---------- 32. Thrust standing ----------
   Named `standing`, not `track`: the Program page timeline already owns
   `.track`, and reusing it put that timeline's rule and 32px indent behind
   these two lines while overriding the timeline's own display.
   Two lines under the tag rows on a research page: what the thrust has
   already established, and what is actually open. The cards further down
   render a published paper and an exploratory project identically, so
   without this the page has no maturity signal at all. */
.standing { display: flex; flex-direction: column; gap: .7rem; margin: 0 0 var(--s4); }
.standing-row { display: flex; gap: var(--s2); align-items: baseline; }
.standing-label {
  flex: 0 0 5.5rem;
  font-family: var(--font-mono);
  font-size: .64rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--accent);
}
.standing-row p { margin: 0; font-size: .92rem; color: var(--text-dim); max-width: 60ch; }
@media (max-width: 40rem) {
  .standing-row { flex-direction: column; gap: .2rem; }
  .standing-label { flex: none; }
}
