/* ══════════════════════════════════════════════════════════════
   kmhuynh.github.io — Scientific Editorial
   Fraunces (display) · Public Sans (body) · IBM Plex Mono (metadata)
   ══════════════════════════════════════════════════════════════ */

:root{
  --f-display:'Fraunces',Georgia,'Times New Roman',serif;
  --f-body:'Public Sans',system-ui,-apple-system,sans-serif;
  --f-mono:'IBM Plex Mono',ui-monospace,'SF Mono',monospace;

  /* --k is the single size dial for the whole page. 1 = the calibrated
     default (19px body at 1440). Nudge it if you want everything larger or
     smaller; every step below is multiplied by it. */
  --k:1;

  /* Fluid scale, 360px -> 2560px, rem+vw so browser zoom and user font-size
     settings keep working. Comments are the REAL rendered px at each end. */
  --step--3: calc(clamp(0.71875rem, 0.6881rem + 0.1364vw, 0.90625rem) * var(--k)); /* 11.5->14.5 */
  --step--2: calc(clamp(0.78125rem, 0.7403rem + 0.1818vw, 1.03125rem) * var(--k)); /* 12.5->16.5 */
  --step--1: calc(clamp(0.875rem, 0.8239rem + 0.2273vw, 1.1875rem) * var(--k));    /* 14  ->19   */
  --step-0 : calc(clamp(1rem, 0.9386rem + 0.2727vw, 1.375rem) * var(--k));         /* 16  ->22  body */
  --step-1 : calc(clamp(1.1875rem, 1.1057rem + 0.3636vw, 1.6875rem) * var(--k));   /* 19  ->27   */
  --step-2 : calc(clamp(1.40625rem, 1.2886rem + 0.5227vw, 2.125rem) * var(--k));   /* 22.5->34   */
  --step-3 : calc(clamp(1.6875rem, 1.5239rem + 0.7273vw, 2.6875rem) * var(--k));   /* 27  ->43   */
  --step-4 : calc(clamp(2rem, 1.7648rem + 1.0455vw, 3.4375rem) * var(--k));        /* 32  ->55   */
  --step-5 : calc(clamp(2.375rem, 2.0273rem + 1.5455vw, 4.5rem) * var(--k));       /* 38  ->72  h1 */

  /* Fraunces' opsz axis removes ~71% of its own sidebearing between opsz 28
     and 132. A constant negative tracking on top of that collides stems at
     display sizes (measured -2.3px on the h1). Tracking must RELAX as size
     grows, so it is per-step, not global. */
  --ls-1:-.018em; --ls-2:-.015em; --ls-3:-.010em; --ls-4:-.004em; --ls-5:0em;

  /* Mono tracking held near-constant in px rather than em */
  --ls-mono: clamp(0.075em, 0.1195em - 0.0584vw, 0.11em);

  /* Space re-solved on the same 360→2560 endpoints as the type. The previous
     set saturated at ~1660px, so space/type ratio fell 15% on large screens. */
  --sp-2xs: clamp(0.375rem, 0.341rem + 0.152vw, 0.584rem);
  --sp-xs : clamp(0.5rem,   0.443rem + 0.256vw, 0.851rem);
  --sp-s  : clamp(0.75rem,  0.652rem + 0.434vw, 1.347rem);
  --sp-m  : clamp(1.125rem, 0.964rem + 0.716vw, 2.110rem);
  --sp-l  : clamp(1.75rem,  1.401rem + 1.553vw, 3.885rem);
  --sp-xl : clamp(2.5rem,   1.848rem + 2.898vw, 6.485rem);
  --sp-2xl: clamp(3.25rem,  2.256rem + 4.417vw, 9.320rem);
  --gut   : clamp(1.25rem,  0.658rem + 2.629vw, 4.865rem);

  --t-fast:.2s; --t-base:.35s; --t-slow:.6s;
  --ease:cubic-bezier(.16,1,.3,1);
  --mast-h: clamp(3.25rem, 2.9rem + 1.2vw, 4.5rem);
}

/* ── LIGHT ─────────────────────────────────────────────────── */
:root, :root[data-theme="light"]{
  --paper:#FBFAF7;  --paper-2:#F1ECE1;
  --ink:#15120E;    /* 17.9:1 */
  --read:#201D17;   /* 16.1:1 */
  --ink-2:#453F36;  /* 10.0:1 */
  --ink-3:#5A5348;  /*  7.3:1 */
  --rule:rgba(21,18,14,.26);       /* decorative frames */
  --rule-ui:rgba(21,18,14,.55);    /* 3.1:1 — borders that ARE the control */
  --rule-soft:rgba(21,18,14,.13);
  --accent:#13294B; --link:#0E4670; --mark:#6F540E;
  --tag-award-bg:rgba(201,162,39,.13); --tag-award-fg:#6E5610;
  --tag-jrnl-bg:rgba(19,41,75,.08);    --tag-jrnl-fg:#13294B;
  --tag-conf-bg:rgba(0,89,76,.09);     --tag-conf-fg:#0A4F44;
  --tag-pre-bg:rgba(160,45,75,.09);    --tag-pre-fg:#9B2F4E;
  --tag-talk-bg:rgba(90,60,130,.09);   --tag-talk-fg:#553A80;
  --tag-abs-bg:rgba(21,18,14,.055);    --tag-abs-fg:#453F36;
  --tag-role-bg:rgba(140,74,20,.10);   --tag-role-fg:#7A4310;
  --img-filter:grayscale(.24) contrast(1.03);
  --thumb-filter:saturate(.85);
  --shadow:rgba(21,18,14,.16);
}

/* ── DARK. Warm ink ground — a warm paper inverting to navy would
      read as two unrelated designs. ─────────────────────────── */
:root[data-theme="dark"]{
  --paper:#14110D;  --paper-2:#1F1B15;
  --ink:#F5EFE5;    /* 16.5:1 */
  --read:#EDE6DA;   /* 15.2:1 */
  --ink-2:#CCC3B3;  /* 10.8:1 */
  --ink-3:#B0A593;  /*  7.8:1 */
  --rule:rgba(245,239,229,.22);
  --rule-ui:rgba(245,239,229,.42);  /* 3.2:1 */
  --rule-soft:rgba(245,239,229,.11);
  --accent:#6FB4E0; --link:#93CDF0; --mark:#E5BE52;
  --tag-award-bg:rgba(217,174,58,.14); --tag-award-fg:#E5BE52;
  --tag-jrnl-bg:rgba(111,180,224,.13); --tag-jrnl-fg:#8FC5E8;
  --tag-conf-bg:rgba(45,212,191,.11);  --tag-conf-fg:#5FCFC0;
  --tag-pre-bg:rgba(239,120,150,.12);  --tag-pre-fg:#E88DA5;
  --tag-talk-bg:rgba(178,150,230,.12); --tag-talk-fg:#B69AE6;
  --tag-abs-bg:rgba(245,239,229,.07);  --tag-abs-fg:#CCC3B3;
  --tag-role-bg:rgba(226,150,86,.12);  --tag-role-fg:#E29656;
  --img-filter:grayscale(.3) contrast(1.02) brightness(.92);
  /* Every img/research/* figure is line art on WHITE. Untreated they render
     as glowing slabs punched into the ink ground. */
  --thumb-filter:saturate(.85) brightness(.82) contrast(1.06);
  --shadow:rgba(8,5,2,.5);
}

/* System-preference fallback so a JS-blocked visitor still gets dark. */
@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]){
    --paper:#14110D;  --paper-2:#1F1B15;
    --ink:#F5EFE5;    --read:#EDE6DA;  --ink-2:#CCC3B3; --ink-3:#B0A593;
    --rule:rgba(245,239,229,.22); --rule-ui:rgba(245,239,229,.42);
    --rule-soft:rgba(245,239,229,.11);
    --accent:#6FB4E0; --link:#93CDF0; --mark:#E5BE52;
    --tag-award-bg:rgba(217,174,58,.14); --tag-award-fg:#E5BE52;
    --tag-jrnl-bg:rgba(111,180,224,.13); --tag-jrnl-fg:#8FC5E8;
    --tag-conf-bg:rgba(45,212,191,.11);  --tag-conf-fg:#5FCFC0;
    --tag-pre-bg:rgba(239,120,150,.12);  --tag-pre-fg:#E88DA5;
    --tag-talk-bg:rgba(178,150,230,.12); --tag-talk-fg:#B69AE6;
    --tag-abs-bg:rgba(245,239,229,.07);  --tag-abs-fg:#CCC3B3;
    --tag-role-bg:rgba(226,150,86,.12);  --tag-role-fg:#E29656;
    --img-filter:grayscale(.3) contrast(1.02) brightness(.92);
    --thumb-filter:saturate(.85) brightness(.82) contrast(1.06);
    --shadow:rgba(8,5,2,.5);
  }
}

*{box-sizing:border-box}
html{
  -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
  scroll-behavior:smooth;
  scroll-padding-top:calc(var(--mast-h) + var(--sp-s));
}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}

body{
  margin:0;background:var(--paper);color:var(--read);
  font-family:var(--f-body);font-size:var(--step-0);font-weight:430;
  line-height:1.55;
  font-optical-sizing:auto;
  transition:background-color var(--t-base) var(--ease), color var(--t-base) var(--ease);
}
/* width via `width`, not padding, so content grows monotonically. Padding-based
   sizing made content NARROWER from 1216→1643px while type kept growing. */
.wrap{width:min(100% - 2 * var(--gut), 52rem + 16vw);max-width:80rem;margin-inline:auto}
img{max-width:100%;height:auto}

h1,h2,h3,h4{font-family:var(--f-display);font-weight:600;color:var(--ink);
  font-optical-sizing:auto;text-wrap:balance;margin:0;overflow-wrap:break-word}
p,li{text-wrap:pretty;overflow-wrap:break-word}

.mono{font-family:var(--f-mono);font-size:var(--step--1);font-weight:450;
  letter-spacing:var(--ls-mono);text-transform:uppercase;color:var(--ink-3);line-height:1.5}

a.skip{position:absolute;left:-9999px;top:0;z-index:200;background:var(--ink);color:var(--paper);
  padding:.75em 1.25em;font-family:var(--f-mono);font-size:var(--step--2);
  letter-spacing:var(--ls-mono);text-transform:uppercase;text-decoration:none}
a.skip:focus{left:0}
:focus-visible{outline:2px solid var(--link);outline-offset:3px}
main:focus{outline:none}
.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* ── masthead ──────────────────────────────────── */
.masthead{position:sticky;top:0;z-index:60;
  background:var(--paper);                                     /* fallback first */
  background:color-mix(in srgb, var(--paper) 94%, transparent);
  -webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);
  border-bottom:1px solid var(--rule)}
.masthead .wrap{display:flex;align-items:center;gap:var(--sp-s);
  min-height:var(--mast-h);flex-wrap:nowrap}
.mh-name{font-family:var(--f-mono);font-size:var(--step--1);font-weight:500;letter-spacing:.16em;
  text-transform:uppercase;color:var(--ink);text-decoration:none;white-space:nowrap;
  min-width:0;overflow:hidden;text-overflow:ellipsis}
.mh-nav{margin-left:auto;display:flex;gap:clamp(.9rem,.4rem + 1.5vw,1.85rem)}
.mh-nav a{font-size:var(--step-0);font-weight:500;color:var(--ink-2);font-family:var(--f-body);
  text-transform:none;letter-spacing:0;text-decoration:none;padding-bottom:2px;white-space:nowrap;
  border-bottom:1px solid transparent;
  transition:color var(--t-base) var(--ease),border-color var(--t-base) var(--ease)}
.mh-nav a:hover,.mh-nav a[aria-current="page"]{color:var(--ink);border-bottom-color:var(--ink)}
.theme-toggle,.nav-toggle{border:1px solid var(--rule-ui);background:none;cursor:pointer;
  width:clamp(2.5rem,1.6rem + 1.4vw,3.25rem);height:clamp(2.5rem,1.6rem + 1.4vw,3.25rem);
  color:var(--ink);display:grid;place-items:center;flex:none;font-size:var(--step--2);
  line-height:1;font-family:var(--f-mono);
  transition:border-color var(--t-fast) var(--ease),background-color var(--t-fast) var(--ease)}
.theme-toggle:hover,.nav-toggle:hover{border-color:var(--ink);background:var(--paper-2)}
.nav-toggle{display:none}

/* ── hero ──────────────────────────────────────── */
.hero{padding:var(--sp-2xl) 0 var(--sp-l)}
.hero-grid{display:grid;grid-template-columns:minmax(0,1.55fr) minmax(0,.85fr);
  gap:var(--sp-xl);align-items:start}
.eyebrow{display:block;margin-bottom:var(--sp-m)}
h1.lede{font-size:var(--step-5);line-height:1.06;letter-spacing:var(--ls-5);
  margin-bottom:var(--sp-m)}
h1.lede em{font-style:italic;color:var(--accent)}
.lede-sub{font-family:var(--f-display);font-size:var(--step-2);line-height:1.16;
  letter-spacing:var(--ls-2);color:var(--ink);font-weight:400;
  margin:0 0 var(--sp-m);max-width:22ch;text-wrap:balance}
.lede-sub strong{font-weight:600;font-style:italic;color:var(--accent)}
.hero-rule{width:clamp(3.5rem,2rem + 4vw,9rem);height:clamp(2px,.11vw,4px);
  background:var(--ink);margin-bottom:var(--sp-m)}
.bio{max-width:54ch;color:var(--read);margin:0 0 var(--sp-s)}
.bio strong{font-weight:600;color:var(--ink)}
a.inline,.bio a,.news-text a{color:var(--link);text-decoration:none;
  border-bottom:1px solid var(--rule-ui);                       /* fallback first */
  border-bottom-color:color-mix(in srgb,var(--link) 70%,transparent);
  transition:border-color var(--t-fast) var(--ease)}
a.inline:hover,.bio a:hover,.news-text a:hover{border-bottom-color:var(--link)}
.hero-links{margin-top:var(--sp-l);display:flex;flex-wrap:wrap;
  gap:var(--sp-2xs) var(--sp-m);align-items:baseline}
.hero-links a{font-size:var(--step-0);font-weight:600;color:var(--ink);font-family:var(--f-body);
  text-decoration:none;border-bottom:1.5px solid var(--rule-ui);padding-bottom:3px;
  transition:border-color var(--t-base) var(--ease),color var(--t-base) var(--ease)}
.hero-links a:hover{border-bottom-color:var(--accent);color:var(--accent)}
/* `class: primary` in hero.yaml marks Google Scholar. Distinguish it by the
   underline only — a size jump made one of four sibling links look arbitrary.
   (The live stylesheet has no .primary rule at all; all four render alike.) */
.hero-links a.primary{border-bottom-color:var(--accent);border-bottom-width:2.5px}
.portrait{position:relative;margin:0}
.portrait img{width:100%;height:auto;display:block;filter:var(--img-filter)}
.portrait::after{content:"";position:absolute;inset:0;border:1px solid var(--rule);pointer-events:none}

figure.band{margin:var(--sp-l) 0 0}
.band-frame{width:100%;border:1px solid var(--rule);background:var(--paper-2);
  overflow:hidden;position:relative;aspect-ratio:280/32}
@supports not (aspect-ratio:1){.band-frame{height:0;padding-top:11.43%}}
.band-frame canvas{display:block;width:100%;height:100%;
  image-rendering:pixelated;image-rendering:crisp-edges}

/* ── sections ──────────────────────────────────── */
section{padding:var(--sp-xl) 0}
.sec-head{display:flex;align-items:baseline;gap:var(--sp-s);flex-wrap:wrap;
  border-top:1px solid var(--ink);padding-top:var(--sp-xs);margin-bottom:var(--sp-l)}
.sec-title{font-size:var(--step-3);line-height:1.08;letter-spacing:var(--ls-3);font-weight:500}
.sec-meta{margin-left:auto}
.sec-foot{margin-top:var(--sp-l);display:flex;justify-content:flex-end}
.sec-foot a{font-family:var(--f-body);font-size:var(--step-0);font-weight:600;
  color:var(--ink);text-decoration:none;border-bottom:1.5px solid var(--rule-ui);padding-bottom:4px;
  transition:border-color var(--t-base) var(--ease),color var(--t-base) var(--ease)}
.sec-foot a:hover{border-bottom-color:var(--accent);color:var(--accent)}

/* ── filters ───────────────────────────────────── */
.filters{display:flex;flex-wrap:wrap;gap:var(--sp-2xs);margin-bottom:var(--sp-m)}
.filters button{font-family:var(--f-mono);font-size:var(--step--2);letter-spacing:var(--ls-mono);
  text-transform:uppercase;border:1px solid var(--rule-ui);background:none;cursor:pointer;
  padding:.55em .95em;color:var(--ink-2);line-height:1;
  transition:border-color var(--t-fast) var(--ease),background-color var(--t-fast) var(--ease),
             color var(--t-fast) var(--ease)}
.filters button:hover{border-color:var(--ink-2);color:var(--ink)}
.filters button[aria-pressed="true"]{background:var(--ink);border-color:var(--ink);color:var(--paper)}
/* A 61-item list is not scannable end to end. The search field is the fastest
   path for a reader who already knows the paper they want; the type filters
   stay for the reader who is browsing. */
.search{margin-bottom:var(--sp-l)}
.search input{font-family:var(--f-body);font-size:var(--step-0);font-weight:430;
  color:var(--ink);background:none;-webkit-appearance:none;appearance:none;
  border:0;border-bottom:1.5px solid var(--rule-ui);border-radius:0;
  padding:.35em 0;width:min(100%,30rem);
  transition:border-color var(--t-fast) var(--ease)}
.search input::placeholder{color:var(--ink-3);opacity:1}
.search input:focus{border-bottom-color:var(--accent);border-bottom-width:2.5px}

.show-all{margin-top:var(--sp-m);font-family:var(--f-mono);font-size:var(--step--2);
  letter-spacing:var(--ls-mono);text-transform:uppercase;background:none;border:0;cursor:pointer;
  color:var(--ink-2);padding:.5em 0;border-bottom:1px solid var(--rule-ui);
  transition:color var(--t-fast) var(--ease),border-color var(--t-fast) var(--ease)}
.show-all:hover{color:var(--ink);border-bottom-color:var(--ink)}
.empty-state{color:var(--ink-3);font-family:var(--f-body);font-size:var(--step--1);
  padding:var(--sp-m) 0;display:none}
.empty-state.on{display:block}

/* ── news ──────────────────────────────────────── */
.news{border-top:1px solid var(--rule-soft)}
/* Fixed tracks, not max-content: each row is its own grid, so max-content
   resolved per row and the text column started up to 94px apart row to row. */
.news-row{display:grid;
  grid-template-columns:clamp(5.75rem,4.6rem + 2.3vw,8rem) clamp(7rem,5.6rem + 2.8vw,10rem) minmax(0,1fr);
  gap:var(--sp-s);align-items:baseline;
  padding:var(--sp-s) var(--sp-xs);border-bottom:1px solid var(--rule-soft);
  margin-inline:calc(var(--sp-xs) * -1);
  transition:background-color var(--t-base) var(--ease)}
.news-row:hover{background:var(--paper-2)}
.news-row.hidden{display:none}
.news-date{font-family:var(--f-mono);font-size:var(--step--1);letter-spacing:var(--ls-mono);
  text-transform:uppercase;color:var(--ink-3);white-space:nowrap;
  transition:color var(--t-base) var(--ease)}
.news-row:hover .news-date{color:var(--accent)}
.news-text{color:var(--read);font-size:var(--step-0);max-width:58ch;
  line-height:1.5}

.tag{font-family:var(--f-mono);font-size:var(--step--2);letter-spacing:var(--ls-mono);
  text-transform:uppercase;border:1px solid var(--rule);padding:.3em .6em;color:var(--ink-2);
  white-space:nowrap;display:inline-block;line-height:1}
.tag-award{border-color:color-mix(in srgb,var(--tag-award-fg) 55%,transparent);
  color:var(--tag-award-fg);background:var(--tag-award-bg)}
.tag-journal{border-color:color-mix(in srgb,var(--tag-jrnl-fg) 50%,transparent);
  color:var(--tag-jrnl-fg);background:var(--tag-jrnl-bg)}
.tag-conference,.tag-workshop{border-color:color-mix(in srgb,var(--tag-conf-fg) 50%,transparent);
  color:var(--tag-conf-fg);background:var(--tag-conf-bg)}
.tag-preprint{border-color:color-mix(in srgb,var(--tag-pre-fg) 50%,transparent);
  color:var(--tag-pre-fg);background:var(--tag-pre-bg)}
.tag-talk{border-color:color-mix(in srgb,var(--tag-talk-fg) 50%,transparent);
  color:var(--tag-talk-fg);background:var(--tag-talk-bg)}
.tag-abstract{border-color:color-mix(in srgb,var(--tag-abs-fg) 40%,transparent);
  color:var(--tag-abs-fg);background:var(--tag-abs-bg)}
.tag-newrole{border-color:color-mix(in srgb,var(--tag-role-fg) 50%,transparent);
  color:var(--tag-role-fg);background:var(--tag-role-bg)}
.tag-other{color:var(--ink-3)}

/* ── research ──────────────────────────────────── */
/* text gets the larger track — the previous split put a 72px headline in a
   466px column, i.e. a 15-character measure over six lines. */
/* The figure track is shaped from the figure's OWN aspect ratio (--ar, emitted
   per card by build.py) instead of a fixed fraction, because a fixed fraction
   is wrong for every figure that is not the ratio it was tuned for: a 2.26:1
   panel left 139px of dead height beside the text, while a 1:1 panel pushed
   its row from 228px to 353px just to fit. Sizing the box from --ar makes the
   box *be* the figure — the image fills it completely, at its true ratio,
   and --fig-h (tuned to the height the text column actually occupies) is what
   decides how big that is. The min() cap stops a very wide figure from
   starving the text column. */
.feature{display:grid;grid-template-columns:minmax(0,1fr);
  /* solved against the text-column heights actually measured at 1200/1440/1920
     (315/330/359px), so the figure lands on the text's baseline box instead of
     overshooting it and growing the card */
  --fig-h:clamp(11rem, 14.7rem + 5.6vw, 24rem);
  gap:var(--sp-l);align-items:center;
  background:var(--paper-2);border-top:3px solid var(--accent);
  padding:var(--sp-m) var(--sp-m) var(--sp-l)}
.feature:has(.thumb){
  grid-template-columns:minmax(0,1fr) min(calc(var(--fig-h) * var(--ar,1.8)), 52%)}
.feature + .feature{margin-top:var(--sp-m)}
/* The word "Featured" inside the venue string was invisible. It is a label,
   so it gets a label's treatment — filled, not set in running text. */
.feature-label{display:inline-block;font-family:var(--f-mono);font-size:var(--step--2);
  font-weight:500;letter-spacing:var(--ls-mono);text-transform:uppercase;
  background:var(--accent);color:var(--paper);padding:.4em .85em;line-height:1;
  margin-bottom:var(--sp-s)}
.venue{display:block;margin-bottom:var(--sp-xs);color:var(--accent)}
.feature h3{font-size:var(--step-3);line-height:1.12;letter-spacing:var(--ls-3);font-weight:700;
  margin-bottom:var(--sp-s)}
.authors{color:var(--ink-2);font-size:var(--step--1);font-family:var(--f-body);
  text-wrap:pretty;
  line-height:1.45;
  margin:0 0 var(--sp-m);max-width:44ch}
.authors strong{font-weight:600;color:var(--ink);border-bottom:2px solid var(--mark);padding-bottom:1px}
.paper-links{display:flex;gap:var(--sp-m);flex-wrap:wrap}
.paper-links a{font-size:var(--step-0);font-weight:600;font-family:var(--f-body);
  color:var(--ink);text-decoration:none;
  border-bottom:2px solid var(--rule-ui);                      /* fallback first */
  border-bottom-color:color-mix(in srgb,var(--accent) 55%,transparent);
  padding-bottom:3px;
  transition:border-color var(--t-base) var(--ease),color var(--t-base) var(--ease)}
.paper-links a:hover{border-bottom-color:var(--accent);color:var(--accent)}
.thumb{width:100%;height:auto;display:block;border:1px solid var(--rule);
  filter:var(--thumb-filter);transition:filter var(--t-slow) var(--ease)}
.thumb:hover{filter:none}
.paper-list{margin-top:var(--sp-l);display:grid;gap:var(--sp-l)}
.paper-item{display:grid;grid-template-columns:minmax(0,1fr);
  /* same solve against this card's own text heights (~200/212/239px) */
  --fig-h:clamp(7rem, 8.3rem + 5.5vw, 17rem);
  gap:var(--sp-m);align-items:center;padding-bottom:var(--sp-m);border-bottom:1px solid var(--rule-soft)}
.paper-item:has(.thumb){
  grid-template-columns:minmax(0,1fr) min(calc(var(--fig-h) * var(--ar,1.8)), 44%)}
.paper-item-title{font-size:var(--step-2);line-height:1.18;letter-spacing:var(--ls-2);
  margin:var(--sp-2xs) 0 var(--sp-xs)}
/* these sit beside a ~470px figure; at --step--1 the text column read as empty */
.paper-item .authors,.feature .authors{font-size:var(--step-0);max-width:52ch}

/* ── publications ──────────────────────────────── */
.pub-group{margin-top:var(--sp-l)}
.pub-group.hidden{display:none}
/* a running head, not a heading — differentiating by register rather than by
   a 1.2x size step the eye cannot read as a level change */
/* sticky: scrolling 40 rows deep into Abstracts should never leave you
   guessing which group you are in */
.pub-group-title{position:sticky;top:var(--mast-h);z-index:20;
  background:var(--paper);
  font-family:var(--f-mono);font-size:var(--step--1);font-weight:500;
  letter-spacing:var(--ls-mono);text-transform:uppercase;color:var(--ink);line-height:1.4;
  padding-top:var(--sp-xs);padding-bottom:var(--sp-2xs);
  border-bottom:1px solid var(--ink);margin-bottom:var(--sp-s)}
.pub-group-title span{color:var(--ink-3)}
.pub-row{display:grid;grid-template-columns:clamp(3.5rem,3rem + 1vw,5rem) minmax(0,1fr);
  gap:var(--sp-m);padding:var(--sp-m) var(--sp-xs);border-bottom:1px solid var(--rule-soft);
  margin-inline:calc(var(--sp-xs) * -1);
  transition:background-color var(--t-base) var(--ease)}
.pub-row:hover{background:var(--paper-2)}
.pub-row.hidden{display:none}
.pub-year{font-family:var(--f-mono);font-size:var(--step--2);letter-spacing:var(--ls-mono);
  color:var(--ink-3)}
.pub-row h3{font-size:var(--step-1);line-height:1.22;letter-spacing:var(--ls-1);
  margin-bottom:var(--sp-2xs)}
/* journal names are proper nouns; all-caps tracked mono was the wrong register
   for the one element carrying the most prestige */
.pub-venue{display:block;margin:var(--sp-2xs) 0 var(--sp-s);font-family:var(--f-display);
  font-style:italic;font-weight:400;font-size:var(--step--1);letter-spacing:0;
  text-transform:none;color:var(--ink-2)}

/* ── tools ─────────────────────────────────────── */
.tool-list{display:grid;gap:var(--sp-l)}
.tool{padding-bottom:var(--sp-l);border-bottom:1px solid var(--rule-soft)}
.tool-head{display:flex;align-items:baseline;gap:var(--sp-s);flex-wrap:wrap;margin-bottom:var(--sp-s)}
.tool h3{font-size:var(--step-2);line-height:1.18;letter-spacing:var(--ls-2)}
.tool p{max-width:54ch;margin:0 0 var(--sp-m)}
.status{font-family:var(--f-mono);font-size:var(--step--2);letter-spacing:var(--ls-mono);
  text-transform:uppercase;border:1px solid var(--rule);padding:.3em .6em;line-height:1;
  color:var(--ink-2)}
.status-active{border-color:color-mix(in srgb,var(--tag-conf-fg) 50%,transparent);
  color:var(--tag-conf-fg);background:var(--tag-conf-bg)}
.status-coming{border-color:color-mix(in srgb,var(--tag-award-fg) 50%,transparent);
  color:var(--tag-award-fg);background:var(--tag-award-bg)}
.status-dev{border-color:color-mix(in srgb,var(--tag-role-fg) 50%,transparent);
  color:var(--tag-role-fg);background:var(--tag-role-bg)}
.tool p.citation{font-family:var(--f-body);font-size:var(--step--1);line-height:1.45;
  color:var(--ink-3);border-left:2px solid var(--rule);
  padding:var(--sp-xs) 0 var(--sp-xs) var(--sp-s);margin:var(--sp-s) 0 0;max-width:60ch}
.citation-label{display:block;font-family:var(--f-mono);font-size:var(--step--2);
  letter-spacing:var(--ls-mono);text-transform:uppercase;color:var(--ink-3);
  margin-bottom:var(--sp-2xs)}

/* ── beyond the lab ────────────────────────────── */
.life-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,14rem),1fr));
  gap:var(--sp-m)}
.life-card{border:1px solid var(--rule-ui);padding:var(--sp-m);text-decoration:none;color:inherit;
  display:block;transition:border-color var(--t-base) var(--ease),
                           background-color var(--t-base) var(--ease)}
a.life-card:hover{border-color:var(--ink);background:var(--paper-2)}
.life-card .emoji{font-size:var(--step--1);line-height:1;margin-bottom:var(--sp-s);
  display:block;filter:grayscale(.55);opacity:.85}
.life-card h3{font-size:var(--step-1);line-height:1.2;letter-spacing:var(--ls-1)}
.life-card p{color:var(--ink-2);font-size:var(--step--1);margin:var(--sp-2xs) 0 0}

/* ── footer + back to top ──────────────────────── */
.foot{border-top:1px solid var(--ink);padding:var(--sp-m) 0 var(--sp-xl)}
.foot .wrap{display:flex;justify-content:space-between;gap:var(--sp-s);flex-wrap:wrap}
.back-to-top{position:fixed;right:var(--sp-m);bottom:var(--sp-m);z-index:70;
  width:clamp(2.75rem,2rem + 1.2vw,3.5rem);height:clamp(2.75rem,2rem + 1.2vw,3.5rem);
  border:1px solid var(--rule-ui);background:var(--paper);color:var(--ink);
  cursor:pointer;display:grid;place-items:center;
  /* visibility, not just opacity — opacity alone leaves it in the tab order */
  opacity:0;visibility:hidden;pointer-events:none;transform:translateY(8px);
  box-shadow:0 6px 22px var(--shadow);
  transition:opacity var(--t-base) var(--ease),transform var(--t-base) var(--ease),
             border-color var(--t-fast) var(--ease),visibility 0s var(--t-base)}
.back-to-top.visible{opacity:1;visibility:visible;pointer-events:auto;transform:none;
  transition:opacity var(--t-base) var(--ease),transform var(--t-base) var(--ease),
             border-color var(--t-fast) var(--ease),visibility 0s}
.back-to-top:hover{border-color:var(--ink)}

/* ── motion. Reveals are opt-IN via the .js root class, so a JS failure
      leaves content visible rather than blank. ─────────────────── */
.rise{opacity:0;transform:translateY(18px);animation:rise .9s var(--ease) forwards;
  animation-delay:var(--d,0ms)}
@keyframes rise{to{opacity:1;transform:none}}
.js [data-reveal]{opacity:0;transform:translateY(18px);
  transition:opacity .8s var(--ease),transform .8s var(--ease)}
.js [data-reveal].is-visible{opacity:1;transform:none}
@media (prefers-reduced-motion:reduce){
  .rise{animation:none;opacity:1;transform:none}
  .js [data-reveal]{opacity:1;transform:none;transition:none}
  *,*::before,*::after{animation-duration:.01ms!important;transition-duration:.01ms!important}
}

/* ── responsive ────────────────────────────────── */
/* 76rem, not 60rem: the desktop grids used to engage at 961px while the
   container only reached design width at 1216px — an 11-character measure. */
@media (max-width:64rem){
  /* the :has() rules above are (0,2,0), so the stacked override has to match */
  .hero-grid,
  .feature,.feature:has(.thumb),
  .paper-item,.paper-item:has(.thumb){grid-template-columns:minmax(0,1fr)}
  .portrait{max-width:20rem}
  .paper-item{gap:var(--sp-s)}
  /* Stacked, one height cap governs every figure, so the list still reads as a
     single band down the page rather than alternating between a letterbox and
     a square as tall as the screen. Width is the binding constraint only for
     the widest panels. */
  .feature .thumb,.paper-item .thumb{width:auto;max-width:100%;
    max-height:calc(var(--fig-h) * 1.3);margin-inline:auto}
}
@media (max-width:60rem){
  .news-row{grid-template-columns:minmax(0,1fr);gap:var(--sp-2xs)}
  .pub-row{grid-template-columns:minmax(0,1fr);gap:var(--sp-2xs)}
  .nav-toggle{display:grid;margin-left:auto}
  .mh-nav{position:absolute;top:100%;left:0;right:0;flex-direction:column;gap:0;
    background:var(--paper);border-bottom:1px solid var(--rule);
    padding:0 var(--gut);display:none;margin-left:0}
  .mh-nav.open{display:flex}
  .mh-nav a{padding:var(--sp-s) 0;border-bottom:1px solid var(--rule-soft)}
  .mh-nav a:last-child{border-bottom:0}
  .mh-nav a[aria-current="page"]{border-bottom-color:var(--rule-soft);color:var(--ink);
    text-decoration:underline;text-underline-offset:4px}
  .masthead .wrap{position:relative}
}
