﻿/* Serif-led. Newsreader carries display AND body copy - that is where the
   elegance comes from. Libre Franklin appears only as small letterspaced
   labels; IBM Plex Mono only where digits need to line up.
   Palette: near-black with a green bias, cool paper, deep pine-teal accent.
   Amber and rust are reserved strictly for data marks. */
/* ============================================================ THEME
   Two colours and two greys, deliberately. The palette used to run five
   near-identical greys (ground/sheet/sunk, rule/rule-soft) because cards
   needed fills and borders. Without cards, a surface is just the ground with
   space around it, so the extra tones have nothing to do.

   Ground is near-white rather than the old tinted #EFF1EF: when the page
   itself is faintly green, the accent reads as a darker shade of the
   background instead of a decision.

   --data2 (green) and --data4 (rust) are retained ONLY for the research
   figures, which encode multiple series. Interface colour is teal and gold. */
:root{
  --ground:#FBFBFA; --sheet:#FBFBFA; --sunk:#F2F3F1;
  --ink:#14201E; --ink-soft:#46534F; --ink-mute:#6A7571;
  --rule:#C4CBC6; --rule-soft:#D9DED9;
  --accent:#14574E; --accent-soft:#E4EDEA; --accent-deep:#0B3B35;
  /* gold darkened from #A8792B: 1.8:1 on a light ground was too pale for text
     and marginal as a graphic. #8A6119 reads ~4.8:1 and is the same gold. */
  --data1:#14574E; --data2:#4A8C6B; --data3:#8A6119; --data4:#A8482B;
  --lw-light:block; --lw-dark:none; --lw-plate:transparent; --lw-pad:0; --lw-mute:.66;

  /* one type scale, seven steps. Everything picks from these. */
  --t-xs:0.74rem; --t-sm:0.85rem; --t-base:0.96rem; --t-md:1.1rem;
  --t-lg:1.36rem; --t-xl:2.05rem; --t-display:clamp(2.7rem,5.4vw,4.2rem);
  /* one spacing scale */
  --measure:68ch;--s-1:8px; --s-2:16px; --s-3:28px; --s-4:48px; --s-5:80px; --s-6:128px;
}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
  /* Dark is not the light theme inverted. The ground lifts off pure black so
     large areas do not smear on OLED, the ink stops short of pure white to cut
     halation on a serif at display size, and the rules are only just visible -
     on a dark ground a hairline reads far heavier than the same value does on
     a light one. */
  --ground:#0D1413; --sheet:#0D1413; --sunk:#151E1C;
  --ink:#E9ECE9; --ink-soft:#B6C1BD; --ink-mute:#828D89;
  --rule:#333F3C; --rule-soft:#252F2D;
  --accent:#6BBFAE; --accent-soft:#122A26; --accent-deep:#9AD8C9;
  --data1:#6BBFAE; --data2:#8FCC9F; --data3:#D8AB57; --data4:#DE8163;
}}
:root[data-theme="dark"]{
  /* Dark is not the light theme inverted. The ground lifts off pure black so
     large areas do not smear on OLED, the ink stops short of pure white to cut
     halation on a serif at display size, and the rules are only just visible -
     on a dark ground a hairline reads far heavier than the same value does on
     a light one. */
  --ground:#0D1413; --sheet:#0D1413; --sunk:#151E1C;
  --ink:#E9ECE9; --ink-soft:#B6C1BD; --ink-mute:#828D89;
  --rule:#333F3C; --rule-soft:#252F2D;
  --accent:#6BBFAE; --accent-soft:#122A26; --accent-deep:#9AD8C9;
  --data1:#6BBFAE; --data2:#8FCC9F; --data3:#D8AB57; --data4:#DE8163;
}
/* Logo art on a dark ground. Almost every official mark is dark ink drawn for
   white paper, so it vanishes here. Two answers, in order of preference: use
   the organisation's own reversed file when one exists, or set the single file
   on a small light plate - a chip, obviously deliberate, rather than an
   inverted mark in the wrong colours. Never invert a colour logo.
   The media block matters more than the toggle block: this site has no theme
   button, so system-dark is the ONLY way a reader gets dark mode. */
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
  --lw-light:none; --lw-dark:block; --lw-plate:#F2F3F0; --lw-pad:7px 9px; --lw-mute:.78;
}}
:root[data-theme="dark"]{
  --lw-light:none; --lw-dark:block; --lw-plate:#F2F3F0; --lw-pad:7px 9px; --lw-mute:.78;
}

*{box-sizing:border-box}
html{scroll-behavior:smooth;font-size:17.5px}
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*{animation:none!important;transition:none!important}.reveal{opacity:1!important;transform:none!important}}

body{
  background:var(--ground); color:var(--ink);
  font-family:"Newsreader",Georgia,"Times New Roman",serif;
  font-size:19px; font-weight:400; line-height:1.62; margin:0;
  -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
}
.skip{position:absolute;left:-9999px}
.skip:focus{left:12px;top:12px;background:var(--accent);color:#fff;padding:8px 16px;z-index:99}

:root{--r-lg:14px;--r:10px;--r-sm:6px}
main,header.site,footer.site{max-width:1180px;margin:0 auto;padding:0 40px}
main>section,main>div.exp-wrap,main>figure{margin:0 0 var(--s-5)}
main>section:last-child{margin-bottom:var(--s-5)}
a{color:var(--accent);text-decoration-thickness:1px;text-underline-offset:2px}
a:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:1px}

/* label face: sans, small, letterspaced. Used sparingly. */
.eyebrow,h2.rule,.lab,.flabel,nav,.btn,.credit,.evidence,.legend,.meta,.idx,.when,.colophon,.flab,.fignum,.axt{
  font-family:"Libre Franklin",-apple-system,Segoe UI,sans-serif;
}
/* Mono is for things that are actually machine-readable: counts, years, DOIs.
   It was on sixteen rules, which made it texture rather than signal. */
.num,.idx,.when,.fignum{font-family:"IBM Plex Mono",monospace}

/* ---------- header ---------- */
header.site{display:flex;justify-content:space-between;align-items:center;gap:24px;flex-wrap:wrap}
.wordmark{text-decoration:none;color:var(--ink);display:flex;align-items:baseline;gap:8px}
.wordmark strong{font-weight:400;font-size:1.5rem;letter-spacing:-.014em}
/* NOT uppercased. "Ph.D." is an abbreviation whose capitalisation is part of
   how it is spelled; "PH.D." is simply wrong, and the periods make it read as
   a typo rather than a styling choice. Tracking eases off too - letter-spacing
   is an all-caps device and looks loose on mixed case. */
.wordmark span{font-family:"IBM Plex Mono",monospace;font-size:var(--t-xs);letter-spacing:.06em;color:var(--ink-mute)}
nav{display:flex;flex-wrap:wrap;gap:8px 28px;font-size:var(--t-sm);letter-spacing:.07em;text-transform:uppercase}
/* Same two-colour rule as the icons and the engine: theme teal at rest, the
   engine's gold when a page is current or being pointed at. The underline
   follows, so the marker never disagrees with the label it sits under. */
nav a{color:var(--accent);text-decoration:none;padding-bottom:3px;border-bottom:1px solid transparent;
  transition:color .28s ease,border-color .28s ease}
nav a.active,nav a:hover,nav a:focus-visible{color:var(--data3)}
/* The marker is the animated ::after line alone. Colouring the border-bottom
   too drew a second rule right under it - two lines under the current page. */

/* ---------- sticky header ---------- */
header.site{
  position:sticky;top:0;z-index:50;
  background:var(--ground);
  border-bottom:1px solid var(--rule);
  padding-top:16px;padding-bottom:16px;
}
@supports (backdrop-filter:blur(8px)){
  header.site{background:color-mix(in srgb, var(--ground) 88%, transparent);backdrop-filter:blur(8px)}
}
.headright{display:flex;align-items:center;gap:24px;flex-wrap:wrap}
.hicons{display:flex;align-items:center;gap:5px;padding-left:20px;border-left:1px solid var(--rule)}
.hicons a{color:var(--accent);display:flex;text-decoration:none}
.hicons a:hover{color:var(--data3)}
.hicons .icon{width:1.15rem;height:1.15rem;opacity:1}
/* anchor targets must clear the sticky header */
:target,[id]{scroll-margin-top:96px}
@media(max-width:580px){
  .hicons{padding-left:16px;gap:12px}
  .headright{gap:16px}
}
/* ---------- type ---------- */
section,.exp-wrap{padding:0}
.eyebrow{font-size:var(--t-xs);letter-spacing:.16em;text-transform:uppercase;color:var(--ink-mute);margin:0 0 24px;line-height:1.9}
.eyebrow span{color:var(--accent)}
h1{font-weight:200;font-size:var(--t-display);line-height:1.04;letter-spacing:-.028em;margin:0 0 var(--s-3);text-wrap:balance}
/* Section headings become serif and large. They were 0.72rem uppercase sans
   with a rule under them - a label, not a heading. The serif was carrying five
   rules against the sans's twenty-six, which is why the page never read as a
   serif site. */
h2.rule{font-family:"Newsreader",Georgia,serif;font-weight:300;font-size:var(--t-xl);
  letter-spacing:-.02em;text-transform:none;color:var(--ink);
  margin:0 0 var(--s-3);padding-bottom:0;border-bottom:0;line-height:1.14;text-wrap:balance}
h3{font-family:"Newsreader",Georgia,serif;font-weight:400;font-size:var(--t-lg);line-height:1.28;letter-spacing:-.012em;margin:0 0 var(--s-1)}
p{margin:0 0 14px;max-width:86ch}
.lede{font-size:var(--t-lg);font-weight:300;font-style:italic;line-height:1.42;color:var(--ink-soft);max-width:62ch}

/* ---------- hero ---------- */
.hero picture{display:block}
.hero{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:72px;align-items:start;padding:var(--s-4) 0 0}
/* The hero opens the page rather than sitting in the section rhythm, so it
   closes tighter than the 128px used between peer sections. */
main>section.hero{margin-bottom:var(--s-5)}
main>section.pagehead{margin-bottom:var(--s-4)}
.hero-side{display:block}
.hero-text h1{font-size:var(--t-display);line-height:1.04;letter-spacing:-.03em;margin:0 0 12px;max-width:14ch;text-wrap:balance}
.hstatsrc{font-family:"Libre Franklin",sans-serif;font-size:var(--t-xs);color:var(--ink-mute);margin:0 0 24px}
.hstatsrc a{color:var(--ink-mute);text-decoration-color:var(--rule)}
.hstatsrc a:hover{color:var(--accent)}
.hero-claim{font-family:"Newsreader",Georgia,serif;font-weight:300;font-size:clamp(1.35rem,2.1vw,1.72rem);line-height:1.22;letter-spacing:-.015em;color:var(--accent);margin:0 0 20px;max-width:none}
.hero .lede{font-size:var(--t-md);font-style:italic;font-weight:300;max-width:64ch;margin:0 0 24px}
.roles{list-style:none;margin:0 0 22px;padding:0;display:grid;gap:13px}
.roles li{display:block;font-size:var(--t-base);line-height:1.45}
/* Both roles take the same shape: title, then organisation beneath it. Running
   them inline with a middot meant one role fitted on a line and the other did
   not, so the pair never looked like a pair. */
.rl{font-family:"Libre Franklin",sans-serif;font-size:var(--t-sm);color:var(--ink-mute);display:block}
/* The two organisation names carry the gold, so the palette's second colour
   appears in the hero type and not only on hover. The role titles beside them
   stay muted, which lets the institutions read first. */
.ro{font-family:"Libre Franklin",sans-serif;font-size:var(--t-sm);font-weight:500;color:var(--data3);
  display:block;white-space:nowrap}
.roles li{display:block}
.herostats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:0;background:none;border:none;border-top:1px solid var(--rule);border-bottom:1px solid var(--rule);margin:0 0 10px;padding:16px 0;max-width:440px}
.herostats>div{background:none;padding:0;text-align:left}
.herostats>div+div{border-left:1px solid var(--rule-soft);padding-left:20px}
.herostats>div:not(:last-child){padding-right:20px}
.herostats dt{font-family:"Libre Franklin",sans-serif;font-size:var(--t-xs);letter-spacing:.12em;text-transform:uppercase;color:var(--ink-mute);margin:0 0 4px;line-height:1.35}
.herostats dd{font-family:"IBM Plex Mono",monospace;font-size:var(--t-lg);color:var(--accent);margin:0;font-variant-numeric:tabular-nums;line-height:1}
.hero-portrait{margin:0}
.portrait{width:100%;aspect-ratio:4/5;object-fit:cover;display:block;border-radius:var(--r)}
.monogram{display:flex;align-items:center;justify-content:center;background:var(--sunk);border:1px solid var(--rule);border-radius:var(--r)}
.monogram span{font-weight:200;font-size:4.8rem;color:var(--ink-mute);letter-spacing:.05em}
.cta{display:flex;gap:16px;flex-wrap:wrap;margin-top:32px}
.btn{display:inline-block;font-size:var(--t-sm);letter-spacing:.11em;text-transform:uppercase;text-decoration:none;padding:12px 24px;background:var(--accent);color:var(--sheet);border:1px solid var(--accent);border-radius:var(--r-sm)}
.btn:hover{background:var(--accent-deep);border-color:var(--accent-deep)}
.btn.ghost{background:transparent;color:var(--accent);border-color:var(--rule)}
.btn.ghost:hover{border-color:var(--accent);background:var(--accent-soft)}

/* ---------- story ---------- */
.chapter{display:grid;grid-template-columns:210px 1fr;gap:32px;padding:24px 0;border-top:1px solid var(--rule-soft)}
.chapter:first-of-type{border-top:none;padding-top:0}
.chapter h3{font-style:italic;font-weight:300;color:var(--accent);font-size:var(--t-md);margin:0}
.chapter p{font-size:var(--t-md);color:var(--ink-soft);max-width:86ch}
.chapter p:last-child{margin-bottom:0}

/* ---------- contribution ---------- */
/* When the row wraps, the leftover cell is empty - and the container's own
   background shows through it as a solid block. Stretching the last item over
   the remainder fills the grid instead. The nth-child tests are count-agnostic:
   :nth-child(odd) as the last item means one cell short of a pair, and 3n+1 /
   3n+2 mean the final row of three holds one or two. */
/* Wrapping rows are balanced, not left-aligned with one stretched card. The
   trick is a grid of 6 (or 4) with each card spanning 2, so a short final row
   can widen its cards EVENLY to fill the width instead of one card growing to
   twice its neighbour. The nth-child tests read the remainder: 3n+2 as the last
   item means two cards on the final row, 3n+1 means one. */
@media (min-width:561px) and (max-width:1040px){
  .stats>div:last-child:nth-child(3n+2),
}
.sicon{width:34px;height:28px;color:var(--accent);margin-bottom:4px;display:block}
.lab{font-size:var(--t-xs);letter-spacing:.12em;text-transform:uppercase;color:var(--ink-mute);margin:0;line-height:1.5}

/* ---------- expertise ---------- */
.exp-wrap{padding-bottom:0}
.exp-wrap{display:flex;flex-wrap:wrap;justify-content:center;gap:40px 32px}
.exp-wrap>.xcard{flex:0 1 calc((100% - 2 * 32px) / 3)}
.exp-wrap>h2{flex:0 0 100%;margin-bottom:0}
.xcard{display:flex;flex-direction:column;align-items:center;text-align:center}
.xring{width:96px;height:96px;display:grid;place-items:center;margin:0 0 var(--s-2);border:1px solid var(--rule);border-radius:50%}
.xglyph{width:52px;height:39px;color:var(--accent);display:block}
/* .acc marks the emphasised part of a glyph. It used to pin those shapes to
   the accent colour outright, which meant a glyph whose shapes are ALL .acc -
   step 01 "Sweep" is entirely one .acc group - was already accent at rest and
   could not change on hover; only its size moved. Following currentColor
   instead lets every glyph go muted-to-accent together, and the two-tone
   emphasis survives through the opacity attributes already in the artwork. */
.xglyph .acc{stroke:currentColor}
.xglyph .acc[fill]{fill:currentColor}
.xcard h3{font-size:var(--t-md);margin:0 0 8px;max-width:none}
.xcard h3 a{color:inherit;text-decoration:none;border-bottom:1px solid var(--rule)}
.xcard h3 a:hover{color:var(--accent);border-bottom-color:var(--accent)}
.xcard .exp-lead{font-size:var(--t-base);margin-bottom:14px}
.xcard .evidence li{max-width:none;font-size:var(--t-sm);padding-left:0;text-align:center}
.xcard .evidence li::before{display:none}
@media(max-width:1200px){.exp-wrap{gap:44px 32px}}
@media(max-width:620px){.exp-wrap{gap:34px}.exp-wrap>.xcard{flex:0 1 100%}}
.idx{font-size:var(--t-xs);color:var(--ink-mute);margin:8px 0 0;font-variant-numeric:tabular-nums}
.exp-lead{font-size:var(--t-md);color:var(--ink-soft);max-width:72ch;margin-bottom:16px}

/* publication groups: a rule and real space between types, a serif heading
   instead of an 11px label, and a count so the shape of the record reads
   at a glance. Six colours across six types would be arbitrary; hierarchy
   and space are what actually separate them. */
.pubgroup{padding-top:52px;margin-top:52px;border-top:1px solid var(--rule)}
.pubgroup:first-of-type{border-top:none;margin-top:0;padding-top:4px}
.pubgroup-head{display:flex;align-items:baseline;gap:14px;margin:0 0 var(--s-3);padding-bottom:14px;border-bottom:1px solid var(--rule-soft)}
.pubgroup-head h2{font-family:"Newsreader",Georgia,serif;font-weight:300;font-size:var(--t-xl);letter-spacing:-.02em;line-height:1.1;color:var(--accent);margin:0}
@media(max-width:580px){.pubgroup{padding-top:36px;margin-top:36px}.pubgroup-head h2{font-size:var(--t-lg)}}

/* the research page lists fragility work, so its header carries a fragility
   figure beside the type - quiet, but not decoration: it is the subject */
.headnote{font-family:"Libre Franklin",sans-serif;font-size:0.9rem;line-height:1.65;color:var(--ink-mute);max-width:52ch}
.headnote em{font-style:italic}
.pagehead-fig{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,0.8fr);gap:40px;align-items:center}
/* A continuous ramp rather than a fade that finishes early: the previous mask
   reached full opacity at 42% and stayed flat, which read as a hard edge. These
   stops keep climbing all the way across, so the image never resolves into a
   rectangle - it just gets denser toward the far corner. */
.pagehead-art{opacity:1;pointer-events:none;display:flex;align-items:center;justify-content:center}
.pagehead-art img{width:100%;height:auto;display:block}
.pagehead-art svg{width:100%;height:auto;max-width:520px;max-height:345px;display:block;margin:0 auto}
@media(max-width:900px){.pagehead-fig{grid-template-columns:1fr;gap:28px}.pagehead-art{display:none}}

/* ---------- figures ---------- */
svg.fragility{width:100%;height:auto;display:block}
svg.fragility .grid line{stroke:var(--rule-soft);stroke-width:1}
svg.fragility .ax{font-family:"IBM Plex Mono",monospace;font-size:9px;fill:var(--ink-mute)}
svg.fragility .axt{font-size:8px;fill:var(--ink-mute);letter-spacing:.12em;text-transform:uppercase}
svg.fragility .curve{stroke-width:1.9;stroke-linecap:round}
svg.fragility .c1{stroke:var(--data1)} svg.fragility .c2{stroke:var(--data2)}
svg.fragility .c3{stroke:var(--data3)} svg.fragility .c4{stroke:var(--data4)}
svg.fragility .dot.c1{fill:var(--data1)} svg.fragility .dot.c2{fill:var(--data2)}
svg.fragility .dot.c3{fill:var(--data3)} svg.fragility .dot.c4{fill:var(--data4)}
figcaption p{font-size:var(--t-base);color:var(--ink-soft);max-width:54ch;line-height:1.5}
.fignum{font-size:var(--t-xs);letter-spacing:.18em;text-transform:uppercase;color:var(--accent);margin:0 0 12px}
.legend{list-style:none;margin:16px 0 0;padding:0;display:flex;flex-direction:column;gap:8px}
.legend li{font-family:"IBM Plex Mono",monospace;font-size:var(--t-xs);color:var(--ink-soft);display:flex;align-items:center;gap:8px}
.legend em{color:var(--ink-mute);font-style:normal}
.legend .sw{width:15px;height:2px;flex:none}
.legend .c1 .sw{background:var(--data1)} .legend .c2 .sw{background:var(--data2)}
.legend .c3 .sw{background:var(--data3)} .legend .c4 .sw{background:var(--data4)}

/* ---------- interactive explorer ---------- */
.explorer{padding:0}
.exp-intro{max-width:none;margin-bottom:32px}
.exp-intro>p{margin:16px 0 0}
.exp-introhead{display:block}
.exp-introhead h2{margin:0}

.exp-intro h2{font-family:"Newsreader",Georgia,serif;font-weight:300;font-size:clamp(1.6rem,3.6vw,2.05rem);letter-spacing:-.02em;line-height:1.16;margin:0 0 16px;text-wrap:balance}
.exp-intro>p{color:var(--ink-soft);font-size:var(--t-md)}
.exp-panel{display:grid;grid-template-columns:340px 1fr;gap:var(--s-4);align-items:start;padding:0}
.exp-controls{display:grid;grid-template-columns:1fr;gap:14px;padding-top:18px;margin-top:16px;border-top:1px solid var(--rule-soft)}
/* stage selector: the two hazards are separated in time, so the figure shows
   one at a time and this chooses which */
.phase{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:16px}
.ph{font-family:"Libre Franklin",sans-serif;font-size:var(--t-xs);letter-spacing:.06em;
    padding:9px 12px;border:1px solid var(--rule);border-radius:var(--r-sm);
    background:none;color:var(--ink-mute);cursor:pointer;text-align:center;
    transition:color .28s ease,border-color .28s ease,background-color .28s ease}
.ph:hover{color:var(--ink);border-color:var(--rule-soft)}
.ph.on{color:var(--accent);border-color:var(--accent);background:var(--accent-soft)}
.ph:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.ctrl.muted{opacity:.42;transition:opacity .3s ease}


.readout{display:flex;align-items:baseline;gap:24px;flex-wrap:wrap}
.ro-num{margin:0!important;flex:none}

.readout{border-left:3px solid var(--accent);padding:2px 0 2px 16px;margin-bottom:24px}
.ro-num{font-family:"IBM Plex Mono",monospace;font-size:2.6rem;line-height:1;color:var(--accent);margin:0 0 12px;font-variant-numeric:tabular-nums;letter-spacing:-.03em}
.ro-text{font-size:var(--t-base);color:var(--ink-soft);margin:0;line-height:1.65;flex:1 1 340px}

.ctrl{margin-bottom:0}
.ctrl label{display:flex;justify-content:space-between;align-items:baseline;gap:12px;font-family:"Libre Franklin",sans-serif;font-size:var(--t-xs);letter-spacing:.13em;text-transform:uppercase;color:var(--ink-mute);margin-bottom:6px}
.ctrl label span{font-family:"IBM Plex Mono",monospace;font-size:var(--t-sm);letter-spacing:0;text-transform:none;color:var(--accent);font-variant-numeric:tabular-nums}
.ctrl input[type=range]{width:100%;-webkit-appearance:none;appearance:none;height:2px;background:var(--rule);outline:none;margin:0 0 5px}
.ctrl input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;border-radius:50%;background:var(--accent);cursor:grab;border:none;margin-top:-7px}
.ctrl input[type=range]::-webkit-slider-thumb:active{cursor:grabbing}
.ctrl input[type=range]::-moz-range-thumb{width:16px;height:16px;border-radius:50%;background:var(--accent);cursor:grab;border:none}
.ctrl input[type=range]:focus-visible{outline:2px solid var(--accent);outline-offset:6px}
.ctrl input[type=range]:disabled{cursor:not-allowed}
.ctrl input[type=range]:disabled::-webkit-slider-thumb{background:var(--ink-mute);cursor:not-allowed}
.ctrl input[type=range]:disabled::-moz-range-thumb{background:var(--ink-mute);cursor:not-allowed}
.disclaim{font-family:"Libre Franklin",sans-serif;font-size:var(--t-sm);line-height:1.65;color:var(--ink-mute);margin:16px 0 0;grid-column:1/-1}
.eqn{font-family:"IBM Plex Mono",monospace;font-size:var(--t-sm);color:var(--ink-mute);margin:16px 0 0;padding-top:16px;border-top:1px solid var(--rule-soft)}

@media(max-width:860px){.exp-panel{grid-template-columns:1fr;gap:32px;padding:0}}
/* ---------- tsunami explorer ---------- */
.eqn sub{font-size:.8em}
/* ---------- surface explorer ---------- */
.mh-struct,.mh-surf{overflow:hidden}
.exp-panel canvas{width:100%;height:auto;display:block}
.exp-top .mh-struct{display:flex}
.exp-top .mh-struct canvas{width:auto;height:360px;max-width:100%;margin:0 auto;display:block}
.mh-surf canvas{width:auto;height:470px;max-width:100%;margin:0 auto;display:block}
@media(max-width:1040px){.exp-panel{padding:0}.exp-top .mh-struct canvas{height:300px}.exp-controls{grid-template-columns:repeat(auto-fit,minmax(190px,1fr))}}
#surfCanvas{width:100%;height:auto;display:block}

/* ---------- how it works ---------- */
/* ---------- coupled engine diagram ---------- */
.engine{margin:0 0 var(--s-4);padding:0}
.engine svg{width:100%;height:auto;display:block;overflow:visible}
.engine .ebox{fill:none;stroke:var(--rule);stroke-width:1.6}
.engine .epanel{fill:none;stroke:none}
.engine .etitle{font-family:"Libre Franklin",sans-serif;font-size:8.5px;letter-spacing:.16em;text-transform:uppercase;fill:var(--ink-mute)}
.engine .ecode{font-family:"Newsreader",Georgia,serif;font-size:17px;fill:var(--ink)}
.engine .esub{font-family:"Libre Franklin",sans-serif;font-size:9.5px;fill:var(--ink-mute)}
.engine .elab{font-family:"Libre Franklin",sans-serif;font-size:12px;fill:var(--ink-soft)}
.engine .eglyph{font-family:"Libre Franklin",sans-serif;font-size:8.5px;letter-spacing:.08em;text-transform:uppercase;fill:var(--ink-mute)}
/* AT REST the whole diagram sits in one theme colour, exactly like the logo
   walls and the icons: the graph reads as a single object rather than a chart
   with six unrelated hues. Depth comes from opacity, not from colour. The
   palette is held back for hover, where each part of the loop takes the colour
   that belongs to it. */
.engine .earr{fill:none;stroke:var(--accent);stroke-width:1.5}
.engine .earr.out{stroke:var(--accent)}
.engine .elab.out{fill:var(--accent);font-size:12px}
.engine .eaxis{stroke:var(--accent);stroke-width:1;opacity:.45}
.engine .epdf{fill:none;stroke:var(--accent);stroke-width:2}
.engine .emode{stroke:var(--accent);stroke-width:1;stroke-dasharray:2 3;opacity:.55}
.engine .ecurve{fill:none;stroke:var(--accent);stroke-width:1.6}
.engine .etrial{fill:var(--accent)}
.engine .elimit{fill:none;stroke:var(--accent);stroke-width:2}
.engine .efail{fill:var(--accent);opacity:.10}
.engine .eorigin{fill:var(--accent)}
.engine .edesign{fill:var(--accent)}
.engine .edash{stroke:var(--accent);stroke-width:1.2;stroke-dasharray:3 3;opacity:.6}
.engine .eframe line{stroke:var(--accent);stroke-width:2;stroke-linecap:round}
.engine .ehyst{fill:none;stroke:var(--accent);stroke-width:1.8;stroke-linejoin:round}
.engine .ehyst.inner{stroke-width:1.2;opacity:.5}
.engine .emesh path{fill:none;stroke:var(--accent);stroke-width:.8;opacity:.7}
.engine .espread line{stroke:var(--accent);stroke-width:1.1;opacity:.55}
.engine:hover .emesh path.r{stroke:var(--accent)}
.engine:hover .emesh path.c{stroke:var(--data3)}
.engine:hover .elab.out{fill:var(--data3)}
.engine .ecyc{fill:none;stroke:var(--accent);stroke-width:2.4}
.engine .ecycHead{fill:var(--accent)}
.engine .ecyclab{font-family:"Libre Franklin",sans-serif;font-size:9.5px;fill:var(--ink-mute)}

/* ON HOVER the loop takes its real colours: the outbound trial stays accent,
   the returning response and the history it produces go gold, the reliability
   search goes rust, and the plumbing recedes to grey so the data reads first. */
.engine .earr,.engine .eaxis,.engine .epdf,.engine .emode,.engine .ecurve,
.engine .etrial,.engine .elimit,.engine .efail,.engine .eorigin,.engine .edesign,
.engine .edash,.engine .eframe line,.engine .ehyst,
.engine .emesh path,.engine .ecyc,.engine .ecycHead,.engine .ecyclab,
.engine .elab{transition:stroke .28s ease,fill .28s ease,opacity .28s ease}

.engine:hover .earr,.engine:hover .eaxis,
.engine:hover .edash,.engine:hover .ecurve{stroke:var(--ink-mute)}
.engine:hover .emode{stroke:var(--accent)}
.engine:hover .etrial,.engine:hover .eorigin{fill:var(--ink-mute)}
.engine:hover .earr.out{stroke:var(--data3)}
.engine:hover .epdf{stroke:var(--data3)}
.engine:hover .ecyc.ecyc-back{stroke:var(--data3)}
.engine:hover .ecycHead.ecyc-back{fill:var(--data3)}
.engine:hover .ecyclab.lab-back{fill:var(--data3)}
.engine:hover .ecyclab.lab-out{fill:var(--accent)}
.engine:hover .elimit{stroke:var(--data3)}
.engine:hover .efail{fill:var(--data3)}
.engine:hover .edesign{fill:var(--accent)}
.engine:hover .eframe line{stroke:var(--accent)}
.engine:hover .ehyst{stroke:var(--data3)}
.engine:hover .efit circle{fill:var(--accent)}
.engine figcaption{font-family:"Libre Franklin",sans-serif;font-size:var(--t-sm);line-height:1.6;color:var(--ink-mute);margin-top:16px;max-width:none}
.rnote{font-family:"Libre Franklin",sans-serif;font-size:var(--t-sm);line-height:1.6;color:var(--ink-mute);margin:24px 0 0}
@media(max-width:900px){.engine{padding:0;overflow-x:auto}.engine svg{min-width:660px}}
.hsteps{list-style:none;margin:0 0 68px;padding:0;display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:0 18px}
.hstep{display:flex;flex-direction:column;align-items:center;text-align:center;position:relative}
/* The connector runs node-edge to node-edge and ends in an arrowhead pointing
   INTO the next node, so the row reads as one sequence rather than six
   separate circles. Previously it was a hairline spanning centre-to-centre
   with its arrow at the wrong end. */
.hstep::before{content:"";position:absolute;top:38px;left:calc(-50% + 30px);width:calc(100% - 60px);height:2px;background:var(--rule)}
.hstep::after{content:"";position:absolute;top:32px;left:calc(50% - 48px);border-left:9px solid var(--rule);border-top:6px solid transparent;border-bottom:6px solid transparent}
.hstep:first-child::before,.hstep:first-child::after{display:none}
.hrail{display:flex;align-items:center;justify-content:center;margin-bottom:16px;position:relative;z-index:1}
.hdot{flex:0 0 auto;width:78px;height:78px;border:1.5px solid var(--rule);border-radius:50%;background:var(--ground);display:grid;place-items:center;position:relative;z-index:1}
.hglyph{width:44px;height:33px;color:var(--accent);flex:none}
.hglyph .acc{stroke:currentColor}
.hlink{display:none}


.hn{font-family:"IBM Plex Mono",monospace;font-size:var(--t-xs);color:var(--accent);margin:0 0 6px;font-variant-numeric:tabular-nums}
.hsteps h3{font-size:var(--t-md);margin:0 0 8px}
.hd{font-size:var(--t-sm);color:var(--ink-soft);margin:0;line-height:1.55;max-width:none}
/* Six across only when there is genuinely room for it. Below that the old
   layout dropped to three columns and stretched each connector across ~240px
   of empty line - sparse dots on a wire. A vertical timeline keeps the steps
   adjacent, gives the descriptions a sane measure, and still reads as one
   sequence running top to bottom. */
@media(max-width:1199px){
  .hsteps{grid-template-columns:1fr;gap:0}
  .hstep{display:grid;grid-template-columns:78px minmax(0,1fr);column-gap:26px;text-align:left;align-items:start;padding-bottom:34px}
  .hrail{grid-column:1;grid-row:1 / span 3;margin:0;justify-content:flex-start}
  .hstep > .hn,.hstep > h3,.hstep > .hd{grid-column:2}
  .hstep > .hn{margin-top:14px}
  .hstep > .hd{max-width:62ch}
  .hstep:last-child{padding-bottom:0}
  .hstep::before{content:"";top:78px;left:38px;width:2px;height:calc(100% - 78px);background:var(--rule);display:block}
  .hstep:first-child::before{display:block}
  .hstep:last-child::before{display:none}
  .hstep::after{content:"";top:auto;bottom:18px;left:31px;border-left:8px solid transparent;border-right:8px solid transparent;border-top:12px solid var(--rule);border-bottom:0;display:block}
  .hstep:hover::after{border-left-color:transparent;border-right-color:transparent;border-top-color:var(--accent)}
  .hstep::after{transition:border-top-color .28s ease}
  .hstep:first-child::after{display:block}
  .hstep:last-child::after{display:none}
}
@media(max-width:520px){
  .hstep{grid-template-columns:58px minmax(0,1fr);column-gap:18px}
  .hdot{width:58px;height:58px}
  .hglyph{width:34px;height:26px}
  .hstep::before{top:58px;left:28px;height:calc(100% - 58px)}
  .hstep::after{left:21px}
}

/* ---------- research images ---------- */
.rlede{font-size:var(--t-md);color:var(--ink-soft);margin:0 0 16px}
.rstep{display:flex;align-items:baseline;gap:12px;margin:0 0 12px;font-family:"Libre Franklin",sans-serif;font-size:var(--t-xs);letter-spacing:.13em;text-transform:uppercase;color:var(--ink-mute)}
.ryear{font-family:"IBM Plex Mono",monospace;font-size:var(--t-xs);letter-spacing:0;color:var(--accent);font-variant-numeric:tabular-nums}
.rgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(264px,1fr));gap:24px}
.rfig{margin:0}
.rfig img{width:100%;height:auto;aspect-ratio:4/3;object-fit:contain;display:block;padding:0;border:1px solid var(--rule)}
.rfig figcaption{font-size:var(--t-sm);color:var(--ink-soft);margin-top:12px;line-height:1.55}
.credit{display:block;font-size:var(--t-xs);letter-spacing:.1em;text-transform:uppercase;color:var(--ink-mute);margin-top:8px}

/* ---------- selected work ---------- */
.work{display:grid;grid-template-columns:56px 1fr;gap:24px;padding:24px 0;border-top:1px solid var(--rule-soft)}
.work:first-of-type{border-top:none;padding-top:4px}
.work h3{max-width:74ch}
.work h3 a{color:var(--ink);text-decoration:none}
.work h3 a:hover{color:var(--accent);text-decoration:underline}
.meta{font-size:var(--t-xs);color:var(--ink-mute);margin:8px 0 0;display:flex;gap:16px;flex-wrap:wrap;align-items:center}
.venue{color:var(--data3)}
.repnum{font-family:"IBM Plex Mono",monospace;font-size:var(--t-xs);color:var(--ink-mute);letter-spacing:.02em}
.tag{font-family:"Libre Franklin",sans-serif;padding:2px 8px;font-size:var(--t-xs);letter-spacing:.1em;text-transform:uppercase}
.tag.oa{background:none;color:var(--accent);border:1px solid var(--rule);border-radius:999px}
main p,/* --- writing. No cards: a hairline between entries, same as the publication
   list, so the two indexes read as one system. */
main>section.writinghead{margin-bottom:var(--s-4)}
/* --- activity log: date column, one line each, no cards. */
.newslist{list-style:none;margin:0;padding:0;display:grid;gap:var(--s-2)}
.newslist li{display:grid;grid-template-columns:96px minmax(0,1fr);gap:var(--s-2);align-items:baseline}
.newslist .when{white-space:nowrap}
.newslist span:last-child{font-size:var(--t-base);color:var(--ink-soft);line-height:1.6}
.newslist a{color:var(--ink-soft);text-decoration:none;border-bottom:1px solid var(--rule)}
.newslist a:hover{color:var(--accent);border-bottom-color:var(--accent)}
@media(max-width:560px){.newslist li{grid-template-columns:1fr;gap:2px}}
.postsources{margin-top:var(--s-4)}
/* A reference list, not a year-indexed table: the year belongs after the
   authors, where a citation puts it. Used by the CV and by every essay, from
   one builder, so the same paper reads identically in both places. */
.reflist{list-style:none;margin:0;padding:0;display:grid;gap:13px}
.reflist li{font-size:var(--t-sm);line-height:1.62;color:var(--ink-soft);
  padding-left:24px;text-indent:-24px}
.reflist a{color:var(--ink-soft);text-decoration:none;border-bottom:1px solid var(--rule)}
.reflist a:hover{color:var(--accent);border-bottom-color:var(--accent)}
.reflist .srcdoi{color:var(--ink-mute);border-bottom-color:var(--rule-soft);word-break:break-word}
/* A note following a reference list is a new thought, not the next entry.
   Sibling rule, so it needs no class and covers wherever this recurs. */
.reflist + .rl,.reflist + p{margin-top:var(--s-3)}
/* "What to try" prompts. Lived only in the demo's own stylesheet, so the
   list rendered unstyled the moment the Locus page carried it too. It is
   shared content now, so it is shared style. Measure cap dropped with the
   rest of them. */
.tryl{list-style:none;margin:0;padding:0;display:grid;gap:12px}
.tryl li{font-size:var(--t-sm);color:var(--ink-soft);line-height:1.6;
  padding-left:16px;position:relative}
.tryl li::before{content:"";position:absolute;left:0;top:.68em;width:8px;height:1px;
  background:var(--data3)}
.postthumb{margin:0}
.postthumb.art,.postfig.art{border:0;padding:0;background:none}
/* A 420-wide drawing stretched to the full text column renders at 2.6x,
   which makes a diagram look like a poster. Cap it and centre it. */
.postfig.art .figsvg{max-width:600px;margin:0 auto}
.postthumb.art .figsvg{width:100%;height:auto;max-height:none}
.postthumb img{width:100%;height:150px;object-fit:contain;display:block;border:1px solid var(--rule)}
.post.haspic{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:var(--s-3);align-items:center}
.postfig{margin:var(--s-4) 0 0}
.postfig img{width:100%;height:auto;max-height:420px;object-fit:contain;display:block;border:1px solid var(--rule)}
.postfig figcaption{font-family:"Libre Franklin",sans-serif;font-size:var(--t-xs);color:var(--ink-mute);margin-top:12px}
@media(max-width:700px){.post.haspic{grid-template-columns:1fr}.postthumb{order:-1}}
/* --- lines of work: the argument above the record. */
.proj{padding:0 0 var(--s-4);margin:0 0 var(--s-4);border-bottom:1px solid var(--rule-soft)}
.proj:last-child{border-bottom:0;margin-bottom:0;padding-bottom:0}
.proj.haspic{display:grid;grid-template-columns:minmax(0,1fr) 260px;gap:var(--s-4);align-items:start}
.proj h3{font-family:"Newsreader",Georgia,serif;font-weight:300;font-size:var(--t-lg);margin:0 0 8px;color:var(--ink)}
.proj .plain{margin:0 0 var(--s-2)}
.projfig{margin:0}
.projfig img{width:100%;height:180px;object-fit:contain;display:block;border:1px solid var(--rule)}
.projpubs{list-style:none;margin:0;padding:0;display:grid;gap:6px}
.projpubs li{display:grid;grid-template-columns:52px minmax(0,1fr);gap:12px;align-items:baseline;font-size:var(--t-sm);line-height:1.5}
.projpubs a{color:var(--ink-soft);text-decoration:none;border-bottom:1px solid var(--rule)}
.projpubs a:hover{color:var(--accent);border-bottom-color:var(--accent)}
.proj .more{margin-top:var(--s-2)}
@media(max-width:700px){.proj.haspic{grid-template-columns:1fr}.projfig{order:-1}.projpubs li{grid-template-columns:44px minmax(0,1fr);gap:10px}}
.locusfig{margin:0}
.locusfig img{width:100%;height:auto;max-height:460px;object-fit:contain;display:block;border:1px solid var(--rule)}
.locusfig figcaption{font-family:"Libre Franklin",sans-serif;font-size:var(--t-xs);color:var(--ink-mute);margin-top:12px}
.cvrow{display:grid;grid-template-columns:118px minmax(0,1fr);gap:32px;padding:18px 0;border-top:1px solid var(--rule-soft);align-items:baseline}
.cvrow h3{font-family:"Newsreader",Georgia,serif;font-weight:400;font-size:var(--t-md);margin:0 0 4px;color:var(--ink)}
/* Gold marks WHERE: the employer, the institution, the journal, the active nav
   item. Teal marks interaction. `.org` is gold sitewide and the CV follows it. */
.cvrow .org{color:var(--data3);font-weight:500;font-size:var(--t-sm);margin:0 0 8px}
.cvrow ul{margin:8px 0 0;padding-left:16px}
.cvrow li{font-size:var(--t-sm);color:var(--ink-soft);margin-bottom:5px;line-height:1.55}
.cvlist{list-style:none;margin:0;padding:0;display:grid;gap:9px}
.cvlist li{display:grid;grid-template-columns:118px minmax(0,1fr);gap:32px;align-items:baseline;font-size:var(--t-sm);line-height:1.55;color:var(--ink-soft)}
.cvlist a{color:var(--ink-soft);text-decoration:none;border-bottom:1px solid var(--rule)}
.cvlist a:hover{color:var(--accent);border-bottom-color:var(--accent)}
/* Only `.prose strong` had a weight, so every <strong> in these lists fell
   through to the browser's 700 - heavier than anything else on the site,
   and heavier than the h3 job titles doing the same job at 400. */
.cvlist strong,.cvrow strong{font-weight:500;color:var(--ink)}
/* .when is shared with the news log, where it carries a top margin. In a
   baseline-aligned CV grid that margin pushes every date off its row. */
.cvrow .when,.cvlist .when{margin:0;white-space:nowrap}
/* `main p,main li` caps text at the 68ch prose measure. That is right for
   running prose and wrong for a CV, whose rows are scanned, not read: it was
   stopping every row ~370px short of its own column. `main`'s own width cap
   keeps the longest line near 88 characters, which is fine for structured rows. */
.cvrow li,.cvrow .plain,.cvlist li,.cvlist li>span,.cvnote{max-width:none}
/* An undated row still has to hold its baseline: an empty span collapses to
   zero height and drags the whole row up by a line. */
.cvlist .when:empty::before{content:" a0"}
.cvhead .lede{margin-bottom:10px}
.cvcontact{margin:0}
/* A row under both columns: it shares the panel, not the plot. */
.exp-share{grid-column:1/-1;margin:4px 0 0;padding-top:22px;
  border-top:1px solid var(--rule-soft);
  display:flex;flex-wrap:wrap;align-items:center;gap:14px}
.exp-share button{display:inline-flex;align-items:center;gap:10px;
  font-family:"Libre Franklin",sans-serif;font-size:var(--t-xs);
  letter-spacing:.12em;text-transform:uppercase;color:var(--ink-soft);
  background:none;border:1px solid var(--rule);border-radius:var(--r);
  padding:9px 16px;cursor:pointer;transition:border-color .2s ease,color .2s ease}
.exp-share button:hover{border-color:var(--accent);color:var(--accent)}
.exp-share button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.exp-share svg{width:15px;height:15px;flex:none}
.exp-share .rl{margin:0}
.postshare{margin:0 0 18px}
.sharebtn{display:inline-flex;align-items:center;gap:10px;
  font-family:"Libre Franklin",sans-serif;font-size:var(--t-xs);
  letter-spacing:.12em;text-transform:uppercase;color:var(--ink-soft);
  background:none;border:1px solid var(--rule);border-radius:var(--r);
  padding:9px 16px;cursor:pointer;transition:border-color .2s ease,color .2s ease}
.sharebtn:hover{border-color:var(--accent);color:var(--accent)}
.sharebtn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.sharebtn svg{width:15px;height:15px;flex:none}
@media print{.postshare{display:none!important}}
.cvprint{margin:18px 0 0;display:flex;flex-wrap:wrap;gap:10px}
.cvprint button{display:inline-flex;align-items:center;gap:10px;
  font-family:"Libre Franklin",sans-serif;font-size:var(--t-xs);
  letter-spacing:.12em;text-transform:uppercase;color:var(--ink-soft);
  background:none;border:1px solid var(--rule);border-radius:var(--r);
  padding:9px 16px;cursor:pointer;transition:border-color .2s ease,color .2s ease}
.cvprint .pico{width:15px;height:15px;flex:none}
.cvprint button:hover{border-color:var(--accent);color:var(--accent)}
.cvprint button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* ---------- drawn research figures ----------
   Everything takes its colour from the stylesheet, so the figures follow the
   theme instead of being baked into pixels the way the MATLAB exports were. */
.figsvg{width:100%;height:auto;display:block;overflow:visible}
.figsvg text{font-family:"Libre Franklin",sans-serif;fill:var(--ink-mute)}
.figsvg .flab,.figsvg .plab{font-size:12px;letter-spacing:.04em}
.figsvg .fnote,.figsvg .pnote{font-size:11.5px;font-style:italic;fill:var(--ink-mute)}
.figsvg .pnote.acc,.figsvg .plab.acc{fill:var(--data3)}
.figsvg .faxis,.figsvg .fgrid line{stroke:var(--rule);stroke-width:1;fill:none}
.figsvg .fgrid line{stroke:var(--rule-soft)}
.figsvg .fmesh path{fill:none;stroke:var(--data3);stroke-width:.85}
.figsvg .fscale line{stroke:var(--rule);stroke-width:1}
.figsvg .ftk{font-size:11px;fill:var(--ink-mute)}
.figsvg .flead{stroke:var(--rule);stroke-width:1}
.figsvg .fslice{fill:none;stroke:var(--accent);stroke-width:2.6;stroke-linecap:round}
/* Axis names are scaffolding. Quieter than the surface they describe. */
.figsvg.surf .flab{font-size:10.5px;opacity:.68}
.figsvg .kwater{fill:var(--sunk);stroke:none}
.figsvg .kbank{fill:none;stroke:var(--rule);stroke-width:1.2}
/* opaque, so the deck reads as passing OVER what it crosses */
.figsvg .kroad{fill:var(--sheet);stroke:var(--ink-mute);stroke-width:1.6}
.figsvg .kmid{fill:none;stroke:var(--rule);stroke-width:1;stroke-dasharray:8 7}
.figsvg .kref{fill:none;stroke:var(--rule);stroke-width:1}
.figsvg .karc{fill:none;stroke:var(--data3);stroke-width:1.4}
.figsvg .karr{fill:none;stroke:var(--data3);stroke-width:2.2;stroke-linecap:round}
.figsvg .kah{fill:var(--data3);stroke:none}
.figsvg .ubar{fill:var(--rule);stroke:none}
.figsvg .ubar.hot{fill:var(--data3)}

.figsvg .pwall{fill:none;stroke:var(--ink-mute);stroke-width:1.6}
.figsvg .pcore{fill:var(--sunk);stroke:var(--rule);stroke-width:1}
.figsvg .pexit{stroke:var(--data3);stroke-width:3.4;fill:none;stroke-linecap:round}
.figsvg .proute{fill:none;stroke-width:2.2;stroke-linecap:round}
.figsvg .proute.direct{stroke:var(--data1)}
.figsvg .proute.step{stroke:var(--data3);stroke-dasharray:5 4}
.figsvg .pnode{fill:var(--ink);stroke:none}
.figsvg .ppinch line{stroke:var(--ink-mute);stroke-width:2.4}
.figsvg .ppinch circle{fill:none;stroke:var(--data3);stroke-width:1.4;stroke-dasharray:3 3}

.figsvg .msec{fill:var(--sunk);stroke:var(--rule);stroke-width:1}
.figsvg .magg{fill:var(--rule);stroke:none}
.figsvg .magg.plastic{fill:var(--data3);fill-opacity:.75}
.figsvg .mcurve{fill:none;stroke:var(--data1);stroke-width:2.2}
.figsvg .mcurve.dim{stroke:var(--rule);stroke-dasharray:4 4;stroke-width:1.6}
.figsvg .mmark{fill:var(--data3);stroke:none}
.figsvg .mdrop{fill:none;stroke:var(--rule);stroke-width:1;stroke-dasharray:3 3}

.figsvg .bdeck,.figsvg .btruss,.figsvg .bpier{fill:none;stroke:var(--ink-mute);stroke-width:1.8}
.figsvg .bground{stroke:var(--rule);stroke-width:1}
.figsvg .bsensor circle{fill:var(--data3);stroke:none}
.figsvg .bwave{fill:none;stroke:var(--data1);stroke-width:1.6}
.figsvg .barrow{fill:var(--rule);stroke:none}

.figsvg .sframe path{fill:none;stroke:var(--ink-mute);stroke-width:1.8}
.figsvg .sframe .sbrace{stroke:var(--data1);stroke-width:1.6}
.figsvg .sframe .slink{stroke:var(--data3);stroke-width:4;stroke-linecap:round}
.figsvg .sframe .sground{stroke:var(--rule);stroke-width:1}

.projfig.art figcaption{font-family:"Libre Franklin",sans-serif;font-size:var(--t-xs);line-height:1.55;color:var(--ink-mute);margin-top:10px}
.projfig.art,.locusfig.art{border:0;padding:0}
.projfig.art .figsvg{max-width:100%}
.locusfig.art .figsvg{max-width:620px;margin:0 auto}
.adv .advh{display:flex;align-items:center;gap:12px}
.adv .hglyph{color:var(--data3)}
/* The 80px section step is for pages carrying figures and prose. Ten dense
   list sections at that rhythm is 720px of air, so the CV drops one step. */
main>section.cvsec{margin-bottom:var(--s-4)}
.cvsec h2.rule{margin-top:var(--s-4)}
.cvnote{font-family:"Libre Franklin",sans-serif;font-size:var(--t-xs);color:var(--ink-mute);margin:0 0 14px;max-width:none}
.adv{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--s-3) var(--s-4)}
.adv li{border-top:1px solid var(--rule);padding-top:14px}
.adv .advh{font-family:"Newsreader",Georgia,serif;font-size:var(--t-md);color:var(--accent);margin:0 0 6px;font-weight:400}
.adv p{font-size:var(--t-sm);color:var(--ink-soft);margin:0;line-height:1.6;max-width:none}
@media(max-width:760px){.adv{grid-template-columns:1fr}}
.cvstamp{font-family:"Libre Franklin",sans-serif;font-size:var(--t-xs);color:var(--ink-mute);margin:10px 0 0}
.biocta{margin:var(--s-3) 0 0;display:flex;flex-wrap:wrap;gap:12px}
@page{margin:16mm 15mm}
@media print{
  header,nav,footer,.biocta,.cvprint{display:none!important}
  /* Content the reader never scrolled to is still at opacity 0. It must not
     reach paper that way. This is the guarantee; beforeprint below is belt. */
  .reveal{opacity:1!important;transform:none!important;transition:none!important}
  body{background:#fff;color:#000;font-size:10pt;line-height:1.45;
       -webkit-print-color-adjust:exact;print-color-adjust:exact}
  main{max-width:none;padding:0}
  a{color:#000;border-bottom:0;text-decoration:none}
  section{page-break-inside:auto;margin:0 0 13pt}
  .cvrow,.cvlist li{page-break-inside:avoid}
  .cvrow{padding:8pt 0;grid-template-columns:88pt minmax(0,1fr);gap:16pt}
  .cvlist li{grid-template-columns:88pt minmax(0,1fr);gap:16pt}
  .cvlist{gap:5pt}

  /* Masthead: name, role, one contact line, one rule. Nothing else. */
  .cvhead{padding:0 0 10pt;margin:0 0 16pt;border-bottom:1.2pt solid #000;
          position:relative;display:block}
  .cvhead .eyebrow{font-size:6.6pt;letter-spacing:.24em;color:#555;margin:0 0 4pt}
  .cvhead h1{font-size:23pt;line-height:1;letter-spacing:-.015em;font-weight:400;
             margin:0 0 3pt;color:#000}
  .cvhead .lede{font-size:10.5pt;font-style:normal;color:#222;margin:0 0 7pt;
                max-width:none;letter-spacing:.005em}
  .cvcontact{font-size:8.6pt;color:#333;margin:0;max-width:none;letter-spacing:.01em}
  /* The stamp belongs on the same line as the rule, out of the way. */
  .cvstamp{position:absolute;top:0;right:0;font-size:7.4pt;color:#666;margin:0}

  h2.rule{page-break-after:avoid;font-size:10.5pt;letter-spacing:.14em;
          text-transform:uppercase;color:#000;margin:0 0 7pt;padding-bottom:2pt;
          border-bottom:.5pt solid #999}
  h2.rule svg,h2.rule img{display:none}
  .cvrow h3{font-size:11pt;margin:0 0 1pt}
  .cvrow .org{font-size:9.4pt;color:#333;margin:0 0 3pt}
  .cvrow .plain,.cvrow li,.cvlist li{color:#222}
  .cvrow li{margin-bottom:2pt}
  .cvsub{font-size:8pt;color:#444;margin:9pt 0 5pt}
  .venue,.cvrow .org{color:#8A6119}
  .cvlist strong,.cvrow strong{font-weight:500;color:#000}
  .cvnote{font-size:8pt;color:#555;margin:-3pt 0 6pt;max-width:none}
  .when{color:#555}
}
.cvsub{font-family:"Libre Franklin",sans-serif;font-size:var(--t-xs);letter-spacing:.14em;text-transform:uppercase;color:var(--ink-mute);margin:var(--s-3) 0 12px;font-weight:400}
.cvsub:first-of-type{margin-top:0}
@media(max-width:640px){.cvrow,.cvlist li{grid-template-columns:1fr;gap:4px}}
.posts{display:flex;flex-direction:column}
.post{padding:var(--s-3) 0;border-bottom:1px solid var(--rule-soft)}
.post:first-child{border-top:1px solid var(--rule-soft)}
.post h2{font-family:"Newsreader",Georgia,serif;font-weight:300;font-size:var(--t-lg);line-height:1.18;letter-spacing:-.015em;margin:6px 0 0;max-width:30ch;color:var(--ink)}
.post h2 a{color:var(--ink);text-decoration:none}
.post h2 a:hover{color:var(--accent)}
.post .meta{margin:0}
.post .plain{margin-top:10px}
.post .more{margin-top:14px}
.post .tag{letter-spacing:.14em;color:var(--accent);padding:0}
/* --- a single post. Narrower than the grid: this page is only ever read. */
.postpage .pagehead{border-bottom:1px solid var(--rule-soft);padding-bottom:var(--s-3)}
.postpage .pagehead .meta{margin-top:var(--s-2)}
.prose{margin-top:var(--s-4)}
.prose p{font-size:var(--t-md);line-height:1.72;color:var(--ink-soft);margin:0 0 var(--s-2);max-width:var(--measure);overflow-wrap:break-word}
.prose p.source{font-family:"Libre Franklin",sans-serif;font-size:var(--t-sm);line-height:1.6;color:var(--ink-mute);margin-top:var(--s-3);padding-top:var(--s-2);border-top:1px solid var(--rule-soft);overflow-wrap:anywhere}
.prose p.source + p.source{margin-top:0;padding-top:0;border-top:0}
.prose h2.rule{margin:var(--s-4) 0 var(--s-2);font-size:var(--t-lg)}
.prose strong{font-weight:500;color:var(--ink)}
.prose em{font-style:italic}
.postfoot{margin-top:var(--s-5);padding-top:var(--s-3);border-top:1px solid var(--rule);display:flex;flex-wrap:wrap;gap:var(--s-1) var(--s-4)}
.postfoot p{margin:0}
.plain{font-size:var(--t-base);color:var(--ink-soft);margin:12px 0 0}
.more{font-family:"Libre Franklin",sans-serif;font-size:var(--t-sm);letter-spacing:.08em;text-transform:uppercase;margin-top:32px}

/* ---------- recognition ---------- */
/* Teaching and collaborators follow the same deboxed row pattern as
   Recognition: a hairline between entries, no container, no fill. */
@media(max-width:620px){
}


/* ---------- research + about ---------- */
.pagehead{padding:48px 0 8px}
.pub.haspic{display:grid;grid-template-columns:1fr 260px;gap:32px;align-items:start}
.pubfig{margin:0}
.pubfig img{width:100%;height:200px;object-fit:contain;padding:0;display:block;border:1px solid var(--rule)}
.pubfig figcaption{font-family:"Libre Franklin",sans-serif;font-size:var(--t-sm);letter-spacing:.06em;color:var(--ink-mute);margin-top:8px;line-height:1.4}
@media(max-width:700px){.pub.haspic{grid-template-columns:1fr;gap:16px}.pubfig{max-width:280px}}
.pub{padding:24px 0;border-bottom:1px solid var(--rule-soft)}
.pub:last-child{border-bottom:none}
.pub h3 a{color:var(--ink);text-decoration:none}
.pub h3 a:hover{color:var(--accent);text-decoration:underline}
.authors{font-size:var(--t-sm);color:var(--ink-mute);margin:0;font-style:italic}
.authors strong{font-weight:500;color:var(--ink-soft);font-style:normal}
.work .authors{margin:2px 0 6px}
.bio p:not(.eyebrow){font-size:var(--t-md);font-weight:300;line-height:1.54;max-width:var(--measure)}
.entry{display:grid;grid-template-columns:118px 1fr;gap:32px;padding:24px 0;border-top:1px solid var(--rule-soft);align-items:baseline}
.when{font-size:var(--t-xs);color:var(--ink-mute);margin:8px 0 0;font-variant-numeric:tabular-nums;line-height:1.7}
.when span{opacity:.7}
/* Employers and institutions carry the gold here exactly as they do in the home
   hero - same information, same treatment. Upright rather than italic for the
   same reason: the hero sets the pattern and the About page should not restate
   it differently. */
.org{color:var(--data3);font-weight:500;font-size:var(--t-base);margin:0 0 12px}
.entry ul{margin:12px 0 0;padding-left:16px}
.entry li{font-size:var(--t-base);color:var(--ink-soft);margin-bottom:8px}

/* Logo wall. Cells are a fixed height so marks of wildly different aspect
   ratios still line up on a common baseline - the thing that separates a
   considered logo band from a row of pasted images. Art is muted at rest and
   comes up to full colour on hover, so the band reads as one texture rather
   than seven competing brand palettes. */
/* One rule for every width. auto-fit with a 1fr max counts tracks from the
   MIN, so it never under-fits the way a fixed track max does; the wall's own
   max-width - the item count, emitted by the build - is what stops three cells
   from stretching to 380px each. Between them, every wall lands on one row on
   desktop and degrades to two-up on a phone with no breakpoint doing the work. */
.logowall{list-style:none;padding:0;margin:0 auto;
  display:grid;grid-template-columns:repeat(auto-fit,minmax(min(150px,100%),1fr));gap:12px;
  max-width:calc(var(--lw-n,3) * 244px)}
.lw{margin:0}
.lw-a{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:7px;
  height:100%;min-height:104px;padding:18px 16px;text-align:center;text-decoration:none;
  border:1px solid var(--rule);border-radius:var(--r);background:var(--sheet);
  transition:border-color .28s ease,background-color .28s ease,transform .28s ease}
a.lw-a:hover{border-color:var(--accent);background:var(--accent-soft);transform:translateY(-3px)}
a.lw-a:focus-visible{outline:2px solid var(--accent);outline-offset:3px}

/* The image slot is a constant 62px in every cell, so cells in a row share a
   height whatever shape the mark is. A crest is inset within that slot rather
   than given a taller one: a square mark set to a wordmark's full height reads
   about three times heavier, and changing the slot instead would push its cell
   taller than its neighbours. */
.lw-art{width:100%;height:62px;object-fit:contain;object-position:center;box-sizing:border-box;
  filter:grayscale(1);opacity:var(--lw-mute);transition:filter .28s ease,opacity .28s ease}
.lw-art.lw-sq{padding:6px 0}

/* Themed at rest, true colours on hover.
   The tint is the mark's own alpha channel used as a mask over a block of the
   site's accent - so it is the real logo silhouette in the site's colour, not a
   filter approximation. The colour artwork sits underneath at opacity 0 and
   cross-fades in on hover or keyboard focus. Both layers share the same 62px
   slot and the same contain geometry, so nothing shifts during the swap.
   Browsers without mask-image get the plain colour logo, which is a fine
   fallback rather than a broken one. */
.lw-mark{position:relative;display:block;width:100%;height:62px}
.lw-mark .lw-art{opacity:0}
@supports (mask-image:url("")) or (-webkit-mask-image:url("")){
  .lw-mark::before{content:"";position:absolute;inset:0;background:var(--accent);
    -webkit-mask-image:var(--lw-mask);mask-image:var(--lw-mask);
    -webkit-mask-size:contain;mask-size:contain;
    -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
    -webkit-mask-position:center;mask-position:center;
    opacity:1;transition:opacity .3s ease}
  .lw-mark.lw-sq::before{inset:6px 0}
  a.lw-a:hover .lw-mark::before,
  a.lw-a:focus-visible .lw-mark::before{opacity:0}
  a.lw-a:hover .lw-mark .lw-art,
  a.lw-a:focus-visible .lw-mark .lw-art{opacity:1}
}
@supports not ((mask-image:url("")) or (-webkit-mask-image:url(""))){
  .lw-mark .lw-art{opacity:var(--lw-mute);filter:grayscale(1)}
  a.lw-a:hover .lw-mark .lw-art{opacity:1;filter:none}
}

/* Bare variant: no card around the mark - just the logo and its line of text
   on the page ground, and a larger slot since there is no chrome to hold it.
   The hover still does the work of a boundary: the mark takes its true colours
   and the note lifts to full ink. */
/* The bare wall has a wider column gap, and auto-fit counts tracks as
   (space + gap) / (min + gap) - so the same 150px minimum that fits five on the
   research page fits only four in the narrower About column. A lower minimum
   restores one row without letting cells shrink in practice, since 1fr still
   shares out all the space. */
.logowall.bare{max-width:calc(var(--lw-n,3) * 300px);gap:8px 28px;
  grid-template-columns:repeat(auto-fit,minmax(min(120px,100%),1fr))}
.logowall.bare .lw-a{border:0;background:none;min-height:0;padding:10px 4px;gap:0;
  align-items:center;justify-content:center}
.logowall.nolabels .lw-a{gap:0}
.logowall.bare a.lw-a:hover{border:0;background:none;transform:none}
.logowall.bare .lw-mark,.logowall.bare .lw-art{height:calc(104px * var(--lw-scale,1))}
.logowall.bare .lw-mark .lw-art{height:100%}
.logowall.bare .lw-art.lw-sq{padding:0}

/* Walls containing a wordmark lay out by CONTENT, not in equal columns. In a
   uniform grid a 7.7:1 wordmark is limited by cell width and lands ~20px tall -
   beside a square crest at ~90px that is a 5x gap, and insetting the crest
   barely dents it, because the problem is the wordmark being starved of width.
   Flex lets each mark take the width its shape needs at a shared cap height,
   which is how a logo band is actually set. Crests get a little more height
   than wordmarks, since a square mark reads lighter at identical height. */
.logowall.bare.lw-flow{display:flex;flex-wrap:wrap;align-items:center;
  justify-content:space-evenly;gap:20px 24px;max-width:none;width:100%}
.logowall.bare.lw-flow .lw{flex:0 0 auto}
/* Forces the next mark onto a new line, so the build controls where a band
   wraps without touching how any mark is sized. Each row still centres itself
   through justify-content. */
.logowall.bare.lw-flow .lw-break{flex:0 0 100%;height:0;margin:0;padding:0;list-style:none}
.logowall.bare.lw-flow .lw-a{padding:4px 2px}
/* Size follows the item count. A band of three can run large and still leave
   room to breathe; the same size across five wraps to a second row. Dividing a
   width budget by the count (emitted as --lw-n) gets both right from one rule,
   and the clamps stop it going silly at the extremes. */
.logowall.bare.lw-flow .lw-mark{display:inline-block;width:auto;
  height:calc(clamp(30px, 130px / var(--lw-n,4), 52px) * var(--lw-scale,1))}
.logowall.bare.lw-flow .lw-mark.lw-sq{
  height:calc(clamp(56px, 300px / var(--lw-n,4), 104px) * var(--lw-scale,1))}
/* Only size here - do NOT set display. The light/dark pair is switched by
   display through --lw-light / --lw-dark, and a bare 'display:block' at this
   specificity beat those rules, so BOTH files rendered side by side and every
   two-file mark appeared doubled on hover. */
.logowall.bare.lw-flow .lw-art{width:auto;height:100%}
.logowall.bare.lw-flow .lw-solo{display:block}
.logowall.bare.lw-flow .lw-light{display:var(--lw-light)}
.logowall.bare.lw-flow .lw-dark{display:var(--lw-dark)}
.logowall.bare.lw-flow .lw-note{max-width:210px}
/* A typographic stand-in has no artwork to size against, so it gets the band
   height explicitly - otherwise it sits smaller than the marks beside it. */
.logowall.bare .lw-word{display:flex;align-items:center;justify-content:center;
  min-height:calc(104px * var(--lw-scale,1));font-size:var(--t-base)}
.logowall.bare.lw-flow .lw-word{min-height:calc(52px * var(--lw-scale,1));font-size:var(--t-sm)}
@media (max-width:620px){
  .logowall.bare.lw-flow{gap:16px 18px;justify-content:center}
  .logowall.bare.lw-flow .lw-mark{height:calc(30px * var(--lw-scale,1))}
  .logowall.bare.lw-flow .lw-mark.lw-sq{height:calc(58px * var(--lw-scale,1))}
}
/* no light chip behind the artwork on hover, dark theme included - the user
   chose bare over legible-at-any-cost */
.logowall.bare .lw-art.lw-solo{background:none;padding:0}
.logowall.bare .lw-mark.lw-sq::before{inset:0}
/* Logos only, where the mark alone identifies the organisation. The name still
   travels in the link title and the image alt, so nothing is lost to assistive
   tech. Walls whose marks do NOT identify the entry - journals showing a
   publisher's wordmark, funders showing a seal - keep their caption. */
.logowall.nolabels .lw-note{display:none}
.logowall.bare .lw-note{margin-top:10px;font-size:var(--t-xs);color:var(--ink-mute);transition:color .28s ease}
.logowall.bare a.lw-a:hover .lw-note{color:var(--accent)}
@media (max-width:620px){
  .logowall.bare{gap:18px 14px;max-width:none;grid-template-columns:repeat(auto-fit,minmax(88px,1fr))}
  .logowall.bare .lw-mark,.logowall.bare .lw-art{height:calc(64px * var(--lw-scale,1))}
  .logowall.bare .lw-a{padding:6px 2px}
}
a.lw-a:hover .lw-art{filter:none;opacity:1}
.lw-solo{background:var(--lw-plate);padding:var(--lw-pad);border-radius:var(--r-sm)}

/* The wordmark stands in for missing artwork. It says the name straight rather
   than imitating a mark, which would be a worse answer than no logo at all. */
.lw-word{font-family:"Libre Franklin",sans-serif;font-weight:600;font-size:var(--t-sm);
  line-height:1.25;letter-spacing:.04em;color:var(--ink-soft);text-wrap:balance;
  transition:color .28s ease}
a.lw-a:hover .lw-word{color:var(--accent)}
.lw-note{font-family:"IBM Plex Mono",monospace;font-size:var(--t-xs);letter-spacing:.04em;
  color:var(--ink-mute);transition:color .28s ease}
a.lw-a:hover .lw-note{color:var(--accent-deep)}

.affilband{margin:0 0 var(--s-4)}
.affil-gap{margin-top:26px}
.affilband .rl{margin:0 0 16px}
h2.rule + .rl{margin-top:-20px;margin-bottom:var(--s-3)}

@media (prefers-reduced-motion:reduce){
  a.lw-a:hover{transform:none}
}
@media (max-width:620px){
  .lw-a{min-height:92px;padding:14px 10px}
}

/* ---------- phone ----------
   Three things the desktop layout got wrong on a small screen: a four-item nav
   that ran 19px past a 320px viewport, wide logos ignoring their container, and
   128px section gaps. With the card borders gone that much empty ground reads
   as "page ended" rather than "next section", so the rhythm tightens here. */
@media (max-width:820px){
  main>section,main>div.exp-wrap,main>figure{margin-bottom:var(--s-5)}
}
@media (max-width:560px){
  main,header.site,footer.site{padding:0 22px}
  main>section,main>div.exp-wrap,main>figure{margin-bottom:var(--s-4)}
  nav{gap:6px 18px;font-size:var(--t-xs)}
  .hicons{flex-wrap:wrap;gap:2px;padding-left:12px}
  .lab{letter-spacing:.06em;overflow-wrap:break-word}
  h2.rule{font-size:var(--t-lg)}
}
@media (max-width:400px){
  nav{gap:4px 14px}
}
/* a 1000x200 logo was scaling to 308px inside a 272px container and breaking
   out of both sides - height alone does not constrain a wide aspect ratio */
.lw-art,.logowall img{max-width:100%}

/* ---------- touch targets ----------
   The explorer sliders had a 2px hit box, which is the page's main interaction
   and effectively undraggable by thumb. The input keeps its 2px visual track;
   the height and padding give it something to hit. */
.ctrl input[type=range]{height:22px;padding:10px 0;background:transparent;background-clip:content-box;margin:0}
@media(pointer:coarse),(max-width:820px){.ctrl input[type=range]{height:44px;padding:21px 0}}
.ctrl input[type=range]::-webkit-slider-runnable-track{height:2px;background:var(--rule)}
.ctrl input[type=range]::-moz-range-track{height:2px;background:var(--rule)}
.hicons a{padding:9px}
@media (max-width:820px){
  .headright nav a{padding:6px 0}
  .fcols a{padding:5px 0}
}

/* ---------- icons ---------- */
.icon{width:1em;height:1em;flex:none;vertical-align:-0.12em;opacity:.85}
.fcols a{display:inline-flex;align-items:center;gap:8px}
.fcols a .icon{color:var(--accent)}
.fcols a:hover .icon,.fcols a:focus-visible .icon{color:var(--data3)}
.fplain{display:flex;align-items:center;gap:8px}
h2.rule .icon{width:1.5em;height:1.5em;margin-right:8px;opacity:.75}
h2.rule{display:flex;align-items:center}
.creds .icon{width:1.15rem;height:1.15rem;color:var(--accent)}
.creds li.pending .icon{color:var(--ink-mute)}

/* ---------- about: bio + portrait ---------- */
.bio-split{display:grid;grid-template-columns:minmax(0,1fr) clamp(200px,24vw,300px);gap:56px;align-items:start;padding-top:var(--s-4)}
.bio-portrait{margin:0}

@media(max-width:860px){.bio-split{grid-template-columns:1fr;gap:32px}.bio-portrait{width:100%;max-width:280px}}
/* ---------- footer ---------- */
footer.site{border-top:1px solid var(--rule);padding-top:32px;padding-bottom:48px;margin-top:48px}
.fcols{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:32px}
.fcols div{display:flex;flex-direction:column;gap:8px;align-items:flex-start}
.flab{font-size:var(--t-xs);letter-spacing:.18em;text-transform:uppercase;color:var(--ink-mute);margin:0 0 8px}
/* Every footer link behaves the same: theme teal at rest, gold and underlined
   on hover, icon following the text. The email link used to change only its
   icon while the text stayed teal, so hovering it looked different from
   hovering the location right beneath it. */
.fcols a{font-size:var(--t-base);text-decoration:none;color:var(--accent)}
.fcols a:hover,.fcols a:focus-visible{color:var(--data3);text-decoration:underline}
.fplain{font-size:var(--t-base);color:var(--ink-mute);margin:0}
.fplain a{display:inline-flex;align-items:center;gap:8px;color:var(--accent);text-decoration:none}
.fplain a:hover,.fplain a:focus-visible{color:var(--data3);text-decoration:underline}
.fplain a .icon{color:var(--accent);transition:color .28s ease}
.fplain a:hover .icon{color:var(--data3)}
.footbar{display:flex;flex-wrap:wrap;justify-content:space-between;gap:8px 32px;margin-top:36px;padding-top:20px;border-top:1px solid var(--rule-soft)}
.colophon{font-size:var(--t-xs);color:var(--ink-mute);margin:0;letter-spacing:.06em}

/* ---------- motion ----------
   Sections lift in once as they enter view; interactive things respond to the
   pointer. Everything here is additive - the page is complete and readable
   before any of it runs, and all of it is switched off under
   prefers-reduced-motion by the rule at the top of this sheet. */

.reveal{opacity:0;transform:translateY(18px);transition:opacity .62s cubic-bezier(.2,.6,.2,1),transform .62s cubic-bezier(.2,.6,.2,1)}
.reveal.seen{opacity:1;transform:none}

a,.btn,.xcard,.work,.entry,.recog li,.hdot,.icon{transition:color .28s ease,background-color .28s ease,border-color .28s ease,transform .28s cubic-bezier(.2,.6,.2,1),opacity .28s ease}
.btn:hover{transform:translateY(-2px)}
.btn:active{transform:translateY(0)}
.xcard:hover .xring{transform:translateY(-4px);border-color:var(--accent)}
.xring{transition:transform .28s cubic-bezier(.2,.6,.2,1),border-color .28s ease}
.hstep:hover .hdot{border-color:var(--accent);transform:translateY(-3px)}
.hsteps:hover .hstep::before{background:var(--rule-soft)}
.hstep:hover::before{background:var(--accent)}
@media(min-width:1200px){.hstep:hover::after{border-left-color:var(--accent)}}
.hstep::before,.hstep::after{transition:background-color .28s ease,border-left-color .28s ease}
.hdot{transition:transform .28s cubic-bezier(.2,.6,.2,1),border-color .28s ease}
.work:hover h3 a,.entry:hover h3 a{color:var(--accent)}
.hicons a:hover .icon{transform:translateY(-2px)}
.pagehead-art img{transition:opacity .8s ease}
nav a{position:relative}
nav a::after{content:"";position:absolute;left:0;right:0;bottom:-5px;height:1px;background:var(--data3);transform:scaleX(0);transform-origin:left;transition:transform .28s cubic-bezier(.2,.6,.2,1)}
nav a:hover::after{transform:scaleX(1)}
nav a.active::after{transform:scaleX(1)}   /* the build emits .active, not .on - this rule never matched */


/* ---------- icon motion ----------
   Every glyph animates the thing it depicts: the sweep fills cell by cell, the
   distribute arrows advance, the loop cycles, the fit points land along the
   curve, the surface drifts, the bars rank. Small amplitudes and long periods -
   these sit beside body text and must not pull the eye off it. All of it is
   killed by the global prefers-reduced-motion rule. */
@keyframes gPulse{0%,72%,100%{opacity:.28}12%,50%{opacity:1}}
@keyframes gAdvance{0%,100%{transform:translateX(0)}45%{transform:translateX(5px)}}
@keyframes gSpin{to{transform:rotate(360deg)}}
@keyframes gDot{0%,70%,100%{opacity:.25;transform:scale(.8)}18%,46%{opacity:1;transform:scale(1)}}
@keyframes gDrift{0%,100%{transform:translateY(0)}50%{transform:translateY(-2.5px)}}
@keyframes gRise{0%,100%{transform:scaleY(.82)}50%{transform:scaleY(1)}}
@keyframes gTrace{0%{stroke-dashoffset:120}55%,100%{stroke-dashoffset:0}}
@keyframes gSlide{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}}

/* 01 sweep - the parameter space filling in */
.hstep:nth-child(1) .hglyph rect{animation:gPulse 3.4s ease-in-out infinite}
.hstep:nth-child(1) .hglyph rect:nth-child(1){animation-delay:0s}
.hstep:nth-child(1) .hglyph rect:nth-child(2){animation-delay:0.18s}
.hstep:nth-child(1) .hglyph rect:nth-child(3){animation-delay:0.36s}
.hstep:nth-child(1) .hglyph rect:nth-child(4){animation-delay:0.54s}
.hstep:nth-child(1) .hglyph rect:nth-child(5){animation-delay:0.72s}
.hstep:nth-child(1) .hglyph rect:nth-child(6){animation-delay:0.9s}
.hstep:nth-child(1) .hglyph rect:nth-child(7){animation-delay:1.08s}
.hstep:nth-child(1) .hglyph rect:nth-child(8){animation-delay:1.26s}
.hstep:nth-child(1) .hglyph rect:nth-child(9){animation-delay:1.44s}
.hstep:nth-child(1) .hglyph rect:nth-child(10){animation-delay:1.62s}
.hstep:nth-child(1) .hglyph rect:nth-child(11){animation-delay:1.8s}
.hstep:nth-child(1) .hglyph rect:nth-child(12){animation-delay:1.98s}

/* 02 distribute - cases going out concurrently */
.hstep:nth-child(2) .hglyph polygon{animation:gAdvance 2.6s ease-in-out infinite}
.hstep:nth-child(2) .hglyph polygon:nth-child(1){animation-delay:0s}
.hstep:nth-child(2) .hglyph polygon:nth-child(2){animation-delay:0.16s}
.hstep:nth-child(2) .hglyph polygon:nth-child(3){animation-delay:0.32s}
.hstep:nth-child(2) .hglyph polygon:nth-child(4){animation-delay:0.48s}

/* 03 iterate - the two arms of the loop firing in turn. Spinning the whole
   glyph would orbit its two circles around each other, which is not what a
   coupled loop does; alternating the arrows shows the exchange. */
.hstep:nth-child(3) .hglyph path.acc,.hstep:nth-child(3) .hglyph polygon.acc{animation:gPulse 2.8s ease-in-out infinite}
.hstep:nth-child(3) .hglyph path.acc:nth-of-type(2),.hstep:nth-child(3) .hglyph polygon.acc:nth-of-type(2){animation-delay:1.4s}

/* 04 fit - points landing, then the curve through them */
.hstep:nth-child(4) .hglyph circle{animation:gDot 3.2s ease-in-out infinite;transform-box:fill-box;transform-origin:center}
.hstep:nth-child(4) .hglyph circle:nth-child(1){animation-delay:0s}
.hstep:nth-child(4) .hglyph circle:nth-child(2){animation-delay:0.22s}
.hstep:nth-child(4) .hglyph circle:nth-child(3){animation-delay:0.44s}
.hstep:nth-child(4) .hglyph circle:nth-child(4){animation-delay:0.66s}
.hstep:nth-child(4) .hglyph circle:nth-child(5){animation-delay:0.88s}
.hstep:nth-child(4) .hglyph path.acc{stroke-dasharray:120;animation:gTrace 3.2s ease-in-out infinite}

/* 05 surface - sheets settling into one */
.hstep:nth-child(5) .hglyph path.acc{animation:gDrift 4.4s ease-in-out infinite}

/* 06 compare - structures ranked against each other */
.hstep:nth-child(6) .hglyph rect{transform-box:fill-box;transform-origin:bottom;animation:gRise 3.6s ease-in-out infinite}
.hstep:nth-child(6) .hglyph rect:nth-child(2){animation-delay:.4s}
.hstep:nth-child(6) .hglyph rect:nth-child(3){animation-delay:.8s}

/* practice: the specification filling in beside the frame */
.xcard:nth-child(5) .xglyph line.acc{animation:gPulse 3.4s ease-in-out infinite}
.xcard:nth-child(5) .xglyph line.acc:nth-of-type(2){animation-delay:.35s}
.xcard:nth-child(5) .xglyph line.acc:nth-of-type(3){animation-delay:.7s}
.xcard:nth-child(5) .xglyph polygon.acc{animation:gAdvance 3.4s ease-in-out infinite}

/* expertise: the located point, the index marker, the sensor array */
.xcard:nth-child(2) .xglyph circle{transform-box:fill-box;transform-origin:center;animation:gDot 3.6s ease-in-out infinite}
.xcard:nth-child(3) .xglyph circle{animation:gSlide 4.6s ease-in-out infinite}
.xcard:nth-child(4) .xglyph circle{transform-box:fill-box;transform-origin:center;animation:gDot 3s ease-in-out infinite}
.xcard:nth-child(4) .xglyph circle:nth-child(5){animation-delay:0s}
.xcard:nth-child(4) .xglyph circle:nth-child(6){animation-delay:0.3s}
.xcard:nth-child(4) .xglyph circle:nth-child(7){animation-delay:0.6s}
.xcard:nth-child(4) .xglyph path.acc{stroke-dasharray:140;animation:gTrace 4s ease-in-out infinite}


/* The response history is a CYCLIC record, so it draws itself over and over:
   the outer loop traces, the inner loop follows half a beat later. Base state
   is the finished loop for the no-animation case. */
@keyframes hystTrace{0%{stroke-dashoffset:1}55%,88%{stroke-dashoffset:0}100%{stroke-dashoffset:-1}}
.engine .ehyst{stroke-dasharray:1;stroke-dashoffset:0;animation:hystTrace 4.2s ease-in-out infinite}
.engine .ehyst.inner{animation-delay:.9s;animation-duration:4.2s}

/* ---------- hover motion ----------
   The engine diagram is static until you point at it, then the exchange it
   describes actually runs: dashes travel along the two cycle arcs in their
   own directions, the arrowheads fire in turn, the design point converges and
   the fragility curve draws. Hovering a glyph accelerates it and pulls it to
   the accent, so the icon answers the pointer. */
@keyframes eFlowFwd{to{stroke-dashoffset:-56}}
@keyframes eFlowBack{to{stroke-dashoffset:56}}

.engine .ecyc,.engine .ecycHead,.engine .edesign,.engine .efrag,.engine .epdf,.engine .ebox{transition:opacity .28s ease,stroke .28s ease}
/* Both arcs animate with the SAME keyframe, and that is deliberate. Dash motion
   follows the path's own direction, and the two arcs are drawn in opposite
   directions: the outbound arc runs 312 -> 456 (left to right), the return arc
   runs 456 -> 312 (right to left). A negative dashoffset moves dashes along
   whichever direction its path was authored in, so one keyframe gives each arc
   the travel its arrowhead points to. The old eFlowBack override reversed the
   return arc against its own arrowhead. */
.engine:hover .ecyc{stroke-dasharray:10 6;animation:eFlowFwd 1.15s linear infinite}
@keyframes eHeadPulse{0%,100%{opacity:.42}45%{opacity:1}}
.engine:hover .ecycHead{animation:eHeadPulse 1.15s ease-in-out infinite}
.engine:hover .ecycHead.ecyc-back{animation-delay:.575s}
.engine:hover .edesign{transform-box:fill-box;transform-origin:center;animation:gDot 1.5s ease-in-out infinite}
/* The mesh builds itself line by line on hover. Rows and columns carry the
   same --i, so the two families sweep across together and meet at the far
   corner, which is how you would actually draw the thing by hand. */
@keyframes meshDraw{0%{stroke-dashoffset:1}38%{stroke-dashoffset:0}100%{stroke-dashoffset:0}}
.engine:hover .emesh path{stroke-dasharray:1;stroke-dashoffset:1;
  animation:meshDraw 3.6s ease-out infinite;animation-delay:calc(var(--i,0) * .11s)}
/* the two feed arrows advance toward the engine, staggered */
.engine:hover .earr{animation:gAdvance 1.7s ease-in-out infinite}
.engine:hover .earr:nth-of-type(2){animation-delay:.35s}
.engine:hover .earr.out{animation-delay:.7s}
/* the uncertainty distributions draw themselves */
.engine:hover .epdf{stroke-dasharray:180;animation:gTrace 2.3s ease-in-out infinite}
.engine:hover .epdf:nth-of-type(2){animation-delay:.4s}
.engine:hover .emode{animation:gPulse 2.3s ease-in-out infinite}
/* the hysteresis loop retraces, the frame breathes with it */
.engine:hover .ehyst{stroke-dasharray:150;animation:gTrace 2.4s ease-in-out infinite}
.engine:hover .ehyst.inner{animation-delay:.5s}
.engine:hover .eframe line{animation:gPulse 2.6s ease-in-out infinite}
/* the fitted points appear along the curve they produced */
.engine:hover .efit circle{transform-box:fill-box;transform-origin:center;
  animation:gDot 2s ease-in-out infinite}
.engine:hover .efit circle:nth-child(2){animation-delay:.12s}
.engine:hover .efit circle:nth-child(3){animation-delay:.24s}
.engine:hover .efit circle:nth-child(4){animation-delay:.36s}
.engine:hover .efit circle:nth-child(5){animation-delay:.48s}
.engine:hover .efit circle:nth-child(6){animation-delay:.60s}
.engine:hover .ebox{stroke:var(--accent);opacity:.7}

/* glyphs answer the pointer: faster, and pulled to the accent */
/* Icon hover. Colour alone was not enough: ink-mute to accent is grey-green to
   deep teal at the same stroke weight, a change you have to be looking for.
   Weight and a small scale carry it, so the icon reacts as plainly as a logo
   does when it takes its true colours. stroke-width is set in CSS, which beats
   the stroke-width presentation attribute on the SVG. */
/* Icons behave like the logo walls: one flat theme colour at rest, their own
   colours on hover. A line glyph has no "real" colours of its own, so the
   reveal uses the site's data palette - the same tokens the charts use, so it
   is a colour the page already owns rather than a decoration bolted on.
   .acc marks the emphasised part of a glyph and takes the second colour. */
.sicon,.hglyph,.xglyph{stroke-width:1.7;opacity:.88;
  transition:color .28s ease,stroke .28s ease,fill .28s ease,opacity .28s ease,
             stroke-width .28s ease,transform .28s cubic-bezier(.2,.7,.3,1)}
.sicon .acc,.hglyph .acc,.xglyph .acc{transition:stroke .28s ease,fill .28s ease}

/* Hover mixes the two palette colours rather than flooding the glyph with one:
   the body takes the engine's gold, the .acc shapes hold the theme teal. Two
   colours, the same two the cycle arrows use, on every icon. */
.stats>div:hover .sicon,
.hstep:hover .hglyph,
.xcard:hover .xglyph{opacity:1;stroke-width:2.4;transform:scale(1.16);color:var(--data3)}
.stats>div:hover .sicon .acc,
.hstep:hover .hglyph .acc,
.xcard:hover .xglyph .acc{stroke:var(--accent)}
.xcard:hover .xglyph .acc[fill]{fill:var(--accent)}

.hicons a .icon{opacity:.85;transition:color .28s ease,opacity .28s ease,transform .28s ease}
.hicons a:hover .icon{opacity:1;transform:scale(1.12)}
@media (prefers-reduced-motion:reduce){
  .stats>div:hover .sicon,.hstep:hover .hglyph,.xcard:hover .xglyph{transform:none}
}
.hstep:hover .hglyph *,.xcard:hover .xglyph *{animation-duration:1.15s!important}


/* ---------- fragility figure: scatter, then the fit ----------
   The base state is the FINISHED figure - points visible, curves fully drawn.
   The keyframes start hidden and end at the base, so if animation is switched
   off (prefers-reduced-motion, an old browser) the reader still sees a complete
   chart rather than an empty grid. Curves cascade by damage state. */
svg.fragility .pt{opacity:1}
svg.fragility .curve{stroke-dasharray:1;stroke-dashoffset:0}
svg.fragility .pt.c1{fill:var(--data1)}
svg.fragility .pt.c2{fill:var(--data2)}
svg.fragility .pt.c3{fill:var(--data3)}
svg.fragility .pt.c4{fill:var(--data4)}

@keyframes fragPt{0%,3%{opacity:0}11%,90%{opacity:1}98%,100%{opacity:0}}
@keyframes fragDraw{0%,9%{stroke-dashoffset:1}44%,90%{stroke-dashoffset:0}98%,100%{stroke-dashoffset:1}}

svg.fragility .pt{animation:fragPt 13s ease-in-out infinite}
svg.fragility .curve{animation:fragDraw 13s ease-in-out infinite}
svg.fragility .pt.c2,svg.fragility .curve.c2{animation-delay:.55s}
svg.fragility .pt.c3,svg.fragility .curve.c3{animation-delay:1.1s}
svg.fragility .pt.c4,svg.fragility .curve.c4{animation-delay:1.65s}
svg.fragility .dot{animation:fragPt 13s ease-in-out infinite}

/* ---------- responsive ---------- */
@media(max-width:860px){
  .hero{grid-template-columns:1fr;gap:32px}
  .roles{grid-template-columns:1fr;gap:2px 0}
  .roles li{display:flex;flex-direction:column;gap:2px;margin-bottom:10px}
  .hero-portrait{max-width:250px}
  .chapter{grid-template-columns:1fr;gap:12px}
}
@media(max-width:580px){
  body{font-size:18px}
  main,header.site,footer.site{padding:0 24px}
  .entry,.work,.exp-head{grid-template-columns:1fr;gap:8px}
  .idx{display:none}
}
