@charset "utf-8";

/* ==========================================================================
   bkleinberg.net — design system

   One idea: every page is a spine and a column. A narrow rail on the left
   carries the label; the column carries the work. One left edge for all
   content, from the name at the top to the last line of the footer.

   Rules worth keeping if you edit this file:
     · Two text colours. A third grey fails AA on white.
     · Two weights, 400 and 600. There is no third.
     · No logotype. The site signs itself in words, never in a mark.
     · Orange is the thread that runs through the pages: the overline labels,
       the kicker above a heading, the rule under a group, the warm chips and
       tiles. Fills stay soft (--accent-soft); orange on text is --accent-ink
       only.
     · One face. Numerals align because they are tabular, not monospaced.
     · One drawn icon set: 24×24, stroke near 1.3px at render size
       (11px → 2.8 · 13px → 2.4 · 15–16px → 2 · 17–20px → 1.6), every glyph
       aria-hidden beside a text label, and never adjacent to the name.
       Adding one is a decision; a logo is still not on the table.
     · A link to someone else's product is the one exception, and it only
       points outwards: that product's mark belongs to it, so GitHub, Google
       Scholar and Semantic Scholar are set in their published marks rather
       than in a drawing of them, wherever they are named — the footer, the
       index of links, the pills above the publication list. The site itself
       still has none.
     · A control says what it does. The appearance switch is the one place
       where a symbol says it alone: the sun and the moon are a pair, both
       are on screen at once, and the knob under one of them shows which is
       on — each still names itself to a screen reader and on hover.
   Everything that moves is in §20, moves without a single line of script,
   and is switched off for print and for reduced motion.
   No web fonts, no framework, no build step, no external requests.
   ========================================================================== */

/* --- 1. Tokens ----------------------------------------------------------- */

:root {
  color-scheme: light;

  /* surfaces */
  --bg:        #ffffff;
  --surface:   #f5f5f7;   /* sunken panel — 1.05:1 against the page */
  --surface-2: #ebebef;   /* the same panel, pressed */
  --line:      #e6e6e9;   /* quiet hairline: inside a list */
  --line-2:    #d2d2d7;   /* structural hairline: under a heading */
  --line-3:    #8e8e93;   /* the edge of a control — 3.3:1, clears 1.4.11 */

  /* ink — only two are ever allowed on text */
  --ink:       #1d1d1f;   /* 16.8:1 on --bg */
  --ink-2:     #66666b;   /*  5.7:1 on --bg, 5.4:1 on --surface */
  --ink-3:     #8e8e93;   /*  3.3:1 — glyphs and rules only, never text */

  /* the one colour */
  --accent:    #e2560f;   /* 3.8:1 — marks and fills. Never text. */
  --accent-ink:#ab420b;   /* 6.0:1 on --bg, 5.5:1 on --accent-soft */
  --accent-soft:#fff3ec;

  /* the card surface — a gradient, a hairline and two soft shadows; the only
     place on the site that gets this treatment */
  --card-line: var(--line);
  --card-grad: linear-gradient(180deg, #fbfbfd 0%, var(--surface) 42%);
  --shadow-1:  0 1px 2px rgba(29, 29, 31, .05), 0 8px 24px -14px rgba(29, 29, 31, .14);
  --shadow-2:  0 2px 4px rgba(29, 29, 31, .06), 0 16px 36px -16px rgba(29, 29, 31, .20);

  /* SF Text below 20px, SF Display above it — and the same distinction on
     Windows 11, which ships Segoe UI Variable in matching optical cuts. */
  --text-face: -apple-system, BlinkMacSystemFont, "SF Pro Text",
               "Segoe UI Variable Text", "Segoe UI", system-ui, Roboto,
               "Helvetica Neue", Arial, sans-serif;
  --display-face: -apple-system, BlinkMacSystemFont, "SF Pro Display",
               "Segoe UI Variable Display", "Segoe UI", system-ui, Roboto,
               "Helvetica Neue", Arial, sans-serif;

  /* fixed space — a 4px scale, no arbitrary numbers; clamp() bounds stay on
     the 4px grid too */
  --s1: 4px;   --s2: 8px;   --s3: 12px;  --s4: 16px;  --s5: 24px;
  --s6: 32px;  --s7: 48px;  --s8: 64px;  --s9: 96px;  --s10: 128px;

  /* the spine */
  --rail:    9.5rem;
  --gap:     var(--s7);
  --measure: 40rem;      /* 640px — 62 characters at 17px */
  --wide:    44rem;

  --bar:     57px;   /* what the header measures, hairline included */
  --bar-min: 3.5rem; /* what makes it at least that tall */

  /* 8 / 12 / 18 / 28, and a pill. Nothing in the mushy middle. */
  --r1: 8px;  --r2: 12px;  --r3: 18px;  --r4: 28px;  --pill: 980px;

  --ease:  cubic-bezier(.32, .72, 0, 1);
  --state: .18s;   /* hover, colour */
  --open:  .28s;   /* disclosure */
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --bg:         #0a0a0b;
    --surface:    #171719;
    --surface-2:  #202024;
    --line:       #26262a;
    --line-2:     #35353b;
    --line-3:     #8e8e93;
    --ink:        #f5f5f7;
    --ink-2:      #a0a0a6;
    --ink-3:      #77777d;
    --accent:     #ff7a2f;
    --accent-ink: #ff9b5e;
    --accent-soft:#2a1508;
    /* dark "shadows" are mostly an inset rim light — drop shadows die on #0a0a0b */
    --card-line:  #2c2c31;
    --card-grad:  linear-gradient(180deg, #1c1c1f 0%, #171719 46%);
    --shadow-1:   inset 0 1px 0 rgba(255, 255, 255, .045), 0 10px 28px -16px rgba(0, 0, 0, .6);
    --shadow-2:   inset 0 1px 0 rgba(255, 255, 255, .06),  0 18px 40px -18px rgba(0, 0, 0, .7);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --bg:         #0a0a0b;
  --surface:    #171719;
  --surface-2:  #202024;
  --line:       #26262a;
  --line-2:     #35353b;
  --line-3:     #8e8e93;
  --ink:        #f5f5f7;
  --ink-2:      #a0a0a6;
  --ink-3:      #77777d;
  --accent:     #ff7a2f;
  --accent-ink: #ff9b5e;
  --accent-soft:#2a1508;
  --card-line:  #2c2c31;
  --card-grad:  linear-gradient(180deg, #1c1c1f 0%, #171719 46%);
  --shadow-1:   inset 0 1px 0 rgba(255, 255, 255, .045), 0 10px 28px -16px rgba(0, 0, 0, .6);
  --shadow-2:   inset 0 1px 0 rgba(255, 255, 255, .06),  0 18px 40px -18px rgba(0, 0, 0, .7);
}

/* --- 2. Reset ------------------------------------------------------------ */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* anchors and keyboard focus both clear the sticky chrome */
  scroll-padding-top: calc(var(--bar) + var(--s2));
}
@media (max-width: 1068.98px) {
  html { scroll-padding-top: calc(var(--bar) + 44px + var(--s2)); }
}
@supports (interpolate-size: allow-keywords) {
  :root { interpolate-size: allow-keywords; }
}

/* the ground lives on html so the wash below can sit between it and the page */
html { background: var(--bg); }

body {
  position: relative;
  color: var(--ink);
  font-family: var(--text-face);
  font-size: 1.0625rem;            /* 17px */
  line-height: 1.47059;            /* 25px, the way SF is leaded */
  letter-spacing: -.022em;
  font-optical-sizing: auto;
  font-synthesis-weight: none;
  overflow-wrap: break-word;
}

/* The first screen stands on a faint dot grid with one warm wash from the
   top right corner — texture, never noise under text: the dots are drawn in
   the quieter hairline colour and the whole layer fades out by half a screen.
   Screen only, so paper never sees it; both colours are tokens, so dark mode
   needs nothing. */
@media screen {
  body::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 560px;
    z-index: -1;
    pointer-events: none;
    background:
      radial-gradient(900px 420px at 82% -160px, var(--accent-soft) 0%, transparent 70%),
      radial-gradient(var(--line) 1px, transparent 1.4px) 0 0 / 24px 24px;
    -webkit-mask-image: linear-gradient(#000 45%, transparent);
    mask-image: linear-gradient(#000 45%, transparent);
  }
}
/* thinning the strokes helps on a dark ground and hurts on a light one */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) body {
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }
}
:root[data-theme="dark"] body {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

img { display: block; max-width: 100%; }
svg { max-width: 100%; }
ul, ol { list-style: none; padding: 0; }
a { color: inherit; }
button { font: inherit; color: inherit; background: none; border: 0; }
h1, h2, h3 { font-weight: 600; }

::selection { background: var(--accent-soft); color: var(--ink); }

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

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    transition-duration: .01ms !important;
  }
}

/* Glyphs sit on the text baseline unless a flex parent says otherwise. Every
   one is drawn so its stroke lands near 1.3px at its render size — the same
   optical weight as the text it stands beside. */
.i { display: inline-block; vertical-align: -.13em; flex: none; }

/* The one exception to the drawn set: a profile link stands for someone
   else's product, so it carries that product's own mark, drawn from the
   official path and filled rather than stroked. A mark fills its 24×24 box
   edge to edge where a drawn glyph keeps a margin, so it is always set one
   step smaller than the glyph it stands beside — 16 against 17 in the
   footer, 15 against 16 in a list, 14 against 15 in a pill — and the two
   read at the same weight. The lab publishes no mark, so its glyph stays a
   drawing; it is filled here so a row of marks reads as one family. */
.mark { display: inline-block; vertical-align: -.13em; flex: none; }
.out {
  vertical-align: baseline;
  translate: 0 -.09em;
  color: var(--ink-3);
  transition: translate var(--state) var(--ease), color var(--state) var(--ease);
}
a:hover .out { translate: 2px -.24em; color: var(--accent); }

/* --- 3. Shell ------------------------------------------------------------ */

/* The container steps between two considered widths. It does not breathe
   sideways: the gutter is 22px from a phone to a 27-inch display. */
.wrap {
  width: 100%;
  margin-inline: auto;
  padding-inline: 20px;
}
@media (min-width: 735px)  { .wrap { max-width: 736px;  padding-inline: 22px; } }
@media (min-width: 1069px) { .wrap { max-width: 1024px; } }
@media (min-width: 1440px) { .wrap { max-width: 1120px; } }

@media (min-width: 1440px) { :root { --measure: 42rem; } }

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 12px 18px;
  background: var(--bg);
  border: 1px solid var(--line-3);
  border-radius: var(--r2);
  font-weight: 600;
  text-decoration: none;
}
.skip:focus { left: 12px; top: 12px; }

/* --- 4. Top bar ---------------------------------------------------------- */

/* The only blurred surface on the site. */
.bar {
  position: sticky;
  top: 0;
  z-index: 50;
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  border-bottom: 1px solid var(--line);
}
@supports not (background: color-mix(in srgb, red 50%, transparent)) {
  .bar { background: var(--bg); }
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .bar {
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    backdrop-filter: saturate(180%) blur(20px);
  }
}

.bar-in {
  display: flex;
  align-items: center;
  gap: var(--s4);
  min-height: var(--bar-min);
}

/* There is no monogram. The bar carries the name as a word — in full ink and
   a step larger than the navigation beside it — and it is always there, on
   every page and at every scroll position. */
.brand {
  display: inline-flex;
  align-items: center;
  margin-inline-end: auto;
  padding-block: 6px;
  font-size: 1.125rem;
  font-weight: 600;
  letter-spacing: -.019em;
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--state) var(--ease);
}
.brand:hover { color: var(--accent-ink); }

.nav { display: flex; align-items: center; gap: var(--s5); }
.nav a {
  position: relative;
  display: inline-block;
  padding-block: 6px;
  font-size: .9375rem;
  letter-spacing: -.016em;
  color: var(--ink-2);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--state) var(--ease);
}
.nav a:hover { color: var(--ink); }
.nav a[aria-current="page"] { color: var(--ink); font-weight: 600; }
/* the active state of the one navigation control */
.nav a[aria-current="page"]::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
}

#appearance { display: flex; flex: none; }

/* Both appearances are named and both are on screen: the switch is a track
   with two segments, and the one you are in is lit by a knob that slides
   between them rather than by two states blinking on and off. The segments
   share the track equally, so the knob is always exactly half of it and the
   control never resizes as you use it. Injected by script — see site.js. */
.appearance {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 1fr;
  flex: none;
  padding: 2px;
  border: 1px solid var(--line-2);
  border-radius: var(--pill);
  background: var(--surface);
}

.appearance .knob {
  position: absolute;
  z-index: 0;
  top: 2px;
  bottom: 2px;
  left: 2px;
  width: calc(50% - 2px);
  border-radius: var(--pill);
  background: var(--bg);
  box-shadow: var(--shadow-1);
  transition: transform var(--open) var(--ease);
}
.appearance[data-on="dark"] .knob { transform: translateX(100%); }
/* on a dark page the lit segment has to be the lighter surface, not the page */
:root[data-theme="dark"] .appearance .knob { background: var(--surface-2); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .appearance .knob { background: var(--surface-2); }
}

/* Icon only: the button is a square around a 15px glyph, so the two segments
   stay equal and the track keeps the height of the rest of the bar. */
.mode {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 26px;
  padding: 0;
  border-radius: var(--pill);
  color: var(--ink-2);
  cursor: pointer;
  transition: color var(--state) var(--ease);
}
.mode:hover { color: var(--ink); }
.mode[aria-pressed="true"] { color: var(--ink); }


/* The name, the navigation and the appearance switch stop fitting on one row
   here, so the navigation takes a row of its own, and everything that
   measures itself against the header (the contents strip, the anchor
   offsets) has to know. */
@media (max-width: 779.98px) {
  :root { --bar: 85px; }
  .bar-in { flex-wrap: wrap; row-gap: 0; padding-bottom: 4px; }
  .nav a { padding-block: 10px; }
  .nav {
    order: 3;
    width: 100%;
    gap: var(--s5);
    padding-bottom: 2px;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .nav::-webkit-scrollbar { display: none; }
}

/* On a 320px screen the name alone fills a row. The switch steps down a size
   and moves beside the navigation — which already scrolls — so the header
   stays two rows deep instead of three. */
@media (max-width: 359px) {
  .mode { width: 27px; }
  .brand { width: 100%; }
  .nav { width: auto; flex: 1 1 0; min-width: 0; padding-inline-end: var(--s2); }
  #appearance { order: 4; }
}

/* --- 5. The spine -------------------------------------------------------- */

.stage, .blk, .doc, .pagehead, .foot-in {
  display: grid;
  grid-template-columns: var(--rail) minmax(0, 1fr);
  gap: 0 var(--gap);
}

:root { --blk-pad: clamp(36px, 5vw, 64px); }
.blk {
  padding-block: var(--blk-pad);
  border-top: 1px solid var(--line);
}
.doc {
  padding-block: var(--s6) clamp(48px, 6.5vw, 80px);
  border-top: 1px solid var(--line);
}

.rail { align-self: start; padding-top: .35rem; }
/* the labels are the site's overlines now — small caps tracked wide, set in
   the accent ink, so the one colour runs quietly through every page */
.rail h2, .lbl, .idx {
  font-size: .75rem;
  font-weight: 600;
  line-height: 1.5;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--accent-ink);
}
/* a label may carry a glyph; the glyph is quiet and the word does the work */
.rail .lbl, .rail .idx { display: flex; align-items: center; gap: var(--s2); }
.lbl .i, .idx .i { color: var(--accent); }

@media (max-width: 1068.98px) {
  .stage, .blk, .doc, .pagehead, .foot-in {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }
  :root { --blk-pad: clamp(32px, 6vw, 44px); }
  .doc { padding-block: var(--s4) var(--s8); }
  .rail { padding-top: 0; margin-bottom: var(--s4); }
}

/* --- 6. Type ------------------------------------------------------------- */

p { max-width: var(--measure); }
p + p { margin-top: 1.35em; }

.prose a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, currentColor 30%, transparent);
  text-decoration-thickness: 1px;
  text-underline-offset: .2em;
  transition: text-decoration-color var(--state) var(--ease);
}
@supports not (color: color-mix(in srgb, red 50%, transparent)) {
  .prose a { text-decoration-color: var(--line-2); }
}
.prose a:hover { text-decoration-color: var(--accent); }

.fine { font-size: .8125rem; line-height: 1.462; letter-spacing: -.006em; color: var(--ink-2); }

/* --- 7. Stage (the first screen) ----------------------------------------- */

.stage { padding-block: clamp(36px, 5vw, 64px) clamp(32px, 4.5vw, 56px); }
.stage .col { grid-column: 2; }
@media (max-width: 1068.98px) { .stage .col { grid-column: 1; } }

/* A name is not a headline. It is set near the sentence that follows it,
   and nowhere near the 84px the page used to give it. */
.stage h1 {
  font-family: var(--display-face);
  font-size: clamp(1.875rem, 3.4vw, 2.625rem);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -.021em;
}

/* every page opens under the same short orange kicker — the site's one
   colour, used as a mark of arrival rather than decoration */
.stage h1::before, .pagehead h1::before {
  content: "";
  display: block;
  width: 44px;
  height: 3px;
  border-radius: 2px;
  background: var(--accent);
  margin-bottom: var(--s4);
}

.stage .role {
  margin-top: clamp(20px, 3vw, 32px);
  max-width: var(--wide);
  font-family: var(--display-face);
  font-size: clamp(1.25rem, 2.2vw, 1.625rem);
  font-weight: 400;
  line-height: 1.34;
  letter-spacing: -.012em;
  color: var(--ink);
  text-wrap: pretty;
}
.stage .role b { font-weight: 600; color: var(--accent-ink); }

.stage .prev {
  margin-top: clamp(20px, 2.6vw, 32px);
  max-width: var(--measure);
  color: var(--ink-2);
}

/* --- 8. Page header (list pages) ----------------------------------------- */

.pagehead { padding-block: clamp(32px, 5vw, 56px) clamp(20px, 3vw, 32px); }
.pagehead h1 {
  font-family: var(--display-face);
  font-size: clamp(1.875rem, 3.4vw, 2.625rem);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -.021em;
}
.pagehead .dek {
  margin-top: var(--s4);
  max-width: var(--measure);
  color: var(--ink-2);
}
.pagehead .btns { margin-top: var(--s5); }
.pagehead .updated { margin-top: var(--s4); }
@media (min-width: 1069px) { .pagehead .rail { padding-top: .8rem; } }

/* --- 9. Buttons and reference links -------------------------------------- */

.btns { display: flex; flex-wrap: wrap; gap: var(--s2); }
.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 36px;
  padding-inline: 16px;
  border: 1px solid var(--line-3);
  border-radius: var(--pill);
  font-size: .9375rem;
  letter-spacing: -.016em;
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
  transition: background-color var(--state) var(--ease);
}
.btn:hover {
  background: var(--accent-soft);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}
@supports not (color: color-mix(in srgb, red 50%, transparent)) {
  .btn:hover { border-color: var(--line-3); }
}

/* a DOI or a repository, set as quiet type — ninety-five of them must not shout */
.ref {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  color: var(--ink-2);
  font-size: .8125rem;
  letter-spacing: -.006em;
  transition: color var(--state) var(--ease);
  /* it sits inside a line of grey metadata, so it has to say it is a link
     without relying on colour to do it */
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, currentColor 34%, transparent);
  text-decoration-thickness: 1px;
  text-underline-offset: .22em;
}
@supports not (color: color-mix(in srgb, red 50%, transparent)) {
  .ref { text-decoration-color: var(--line-2); }
}
.ref:hover { color: var(--accent-ink); text-decoration-color: currentColor; }
.refs .ref { padding-block: 6px; margin-block: -6px; }
.refs { display: inline-flex; flex-wrap: wrap; gap: 0 var(--s4); }

/* --- 10. The two questions ----------------------------------------------- */

.lead {
  max-width: var(--wide);
  font-family: var(--display-face);
  font-size: clamp(1.1875rem, 2.2vw, 1.5rem);
  font-weight: 400;
  line-height: 1.32;
  letter-spacing: -.012em;
}

.qlead { margin-top: var(--s5); font-size: .9375rem; letter-spacing: -.016em; color: var(--ink-2); }

.qs { display: grid; gap: var(--s6); margin-top: var(--s5); }
/* once there is room, the two questions sit side by side — full size, no
   boxes; the pairing is the layout's way of saying they are one pair */
@media (min-width: 1069px) {
  .qs { grid-template-columns: 1fr 1fr; gap: var(--s6) var(--s7); }
  .q p { max-width: none; }
}
.q { display: grid; grid-template-columns: 2.5rem 1fr; align-items: baseline; }
.q .n {
  font-size: .8125rem;
  font-weight: 600;
  letter-spacing: 0;
  color: var(--accent-ink);
  font-variant-numeric: tabular-nums;
}
/* They are the point of the page, so they are the largest thing below the
   opener — made by size and by the mark under the phrase, never by a
   heavier weight. */
.q p {
  max-width: 20em;
  font-family: var(--display-face);
  font-size: clamp(1.3125rem, 2.8vw, 1.75rem);
  font-weight: 600;
  line-height: 1.22;
  letter-spacing: -.021em;
}
.q .mk {
  background-image: linear-gradient(var(--accent), var(--accent));
  background-repeat: no-repeat;
  background-size: 100% .055em;
  background-position: 0 1.06em;
  padding-bottom: .06em;
  /* in the narrower two-up column the marked phrases wrap; without this the
     underline would draw on one fragment only */
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

@media (max-width: 519px) {
  .q { grid-template-columns: 1.9rem 1fr; }
  .q p { max-width: none; }
}

/* --- 11. Interests ------------------------------------------------------- */

/* the interests live inside the Research block now, under their own quiet
   sub-heading — one section fewer, no words lost */
.sublbl {
  margin-top: var(--s7);
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--accent-ink);
}
.sublbl + .qlead { margin-top: var(--s2); }

/* the interests are warm chips — the accent as a soft fill, the words in ink,
   each opened by one drawn glyph in the accent ink */
.tags { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s4); }
.tags li {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  padding: 8px 15px;
  background: var(--accent-soft);
  border: 1px solid color-mix(in srgb, var(--accent) 26%, transparent);
  border-radius: var(--pill);
  font-size: .9375rem;
  letter-spacing: -.016em;
}
.tags .i { color: var(--accent-ink); }
@supports not (color: color-mix(in srgb, red 50%, transparent)) {
  .tags li { border-color: var(--line-2); }
}

/* --- 12. Project cards --------------------------------------------------- */

.cards {
  display: grid;
  gap: var(--s3);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
}
.card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s2);
  padding: clamp(20px, 3vw, 28px);
  background: var(--card-grad);
  border: 1px solid var(--card-line);
  border-radius: var(--r4);
  box-shadow: var(--shadow-1);
  transition: box-shadow var(--state) var(--ease),
              border-color var(--state) var(--ease),
              transform var(--state) var(--ease);
}
@supports (corner-shape: squircle) {
  .card { border-radius: 33px; corner-shape: squircle; }
}
.card:hover { box-shadow: var(--shadow-2); border-color: var(--line-2); }
@media (prefers-reduced-motion: no-preference) {
  .card:hover { transform: translateY(-2px); }
}

/* one icon tile per card — warm at rest, warmer under the pointer */
.tile {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: var(--r2);
  background: var(--accent-soft);
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
  color: var(--accent-ink);
  margin-bottom: var(--s2);
  transition: border-color var(--state) var(--ease);
}
@supports (corner-shape: squircle) {
  .tile { border-radius: 14px; corner-shape: squircle; }
}
.card:has(a:hover) .tile, .card:has(a:focus-visible) .tile {
  border-color: color-mix(in srgb, var(--accent) 48%, transparent);
}
@supports not (color: color-mix(in srgb, red 50%, transparent)) {
  .tile, .card:has(a:hover) .tile, .card:has(a:focus-visible) .tile { border-color: var(--card-line); }
}
.card h3 {
  font-size: 1.1875rem;
  font-weight: 600;
  letter-spacing: -.021em;
}
.card h3 a { display: inline-flex; align-items: baseline; gap: 6px; text-decoration: none; }
.card p { font-size: 1rem; letter-spacing: -.02em; line-height: 1.5; color: var(--ink-2); max-width: 34ch; }

/* --- 13. Index list (Elsewhere) ------------------------------------------ */

.index-list { border-top: 1px solid var(--line); }
.index-list li { border-bottom: 1px solid var(--line); }
.index-list a {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 3px var(--s4);
  padding-block: var(--s4);
  text-decoration: none;
}
.index-list .t {
  grid-column: 1;
  grid-row: 1;
  font-weight: 600;
  letter-spacing: -.022em;
  transition: color var(--state) var(--ease);
}
.index-list .go, .index-list .out { grid-column: 2; grid-row: 1 / 3; }
.index-list .d {
  grid-column: 1;
  grid-row: 2;
  font-size: .9375rem;
  letter-spacing: -.016em;
  line-height: 1.4667;
  color: var(--ink-2);
  max-width: 46ch;
}

/* Once there is room, the index is a ledger: a column of glyphs, a fixed
   column of names, a column of notes starting on one edge, and the arrows
   on a fourth. Below 735px the glyph column would eat into a 360px measure,
   so there it is simply not shown. */
.index-list .lead { color: var(--ink-3); transition: color var(--state) var(--ease); }
@media (max-width: 734.98px) { .index-list .lead { display: none; } }
@media screen and (min-width: 735px) {
  .index-list a {
    grid-template-columns: 1.5rem 11rem minmax(0, 1fr) auto;
    align-items: baseline;
    gap: 0 var(--s5);
    padding-block: var(--s3);
  }
  .index-list .lead { grid-column: 1; grid-row: 1; align-self: center; }
  .index-list .t { grid-column: 2; }
  .index-list .d { grid-column: 3; grid-row: 1; max-width: none; }
  .index-list .go, .index-list .out { grid-column: 4; grid-row: 1; }
}
.index-list a:hover .lead { color: var(--ink-2); }
.index-list a:hover .t { color: var(--ink); }
.index-list .go { color: var(--ink-3); transition: transform var(--state) var(--ease); }
.index-list a:hover .go { transform: translateX(3px); }

/* --- 14. Questions and answers ------------------------------------------- */

.faq { border-top: 1px solid var(--line); }
.faq details { border-bottom: 1px solid var(--line); }
.faq summary {
  display: flex;
  align-items: baseline;
  gap: var(--s4);
  padding-block: var(--s4);
  cursor: pointer;
  font-weight: 600;
  letter-spacing: -.022em;
  list-style: none;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary .chev {
  flex: none;
  margin-left: auto;
  color: var(--ink-3);
  transition: transform var(--open) var(--ease), color var(--state) var(--ease);
}
.faq summary:hover .chev { color: var(--ink-2); }
.faq details[open] summary .chev { transform: rotate(90deg); color: var(--accent); }
.faq .a { padding-bottom: var(--s4); }
.faq .a p { color: var(--ink-2); max-width: var(--measure); }
/* a real disclosure animation, with no script and no layout dependency */
@supports selector(::details-content) {
  .faq details::details-content {
    block-size: 0;
    overflow: hidden;
    transition: block-size var(--open) var(--ease), content-visibility var(--open) allow-discrete;
  }
  .faq details[open]::details-content { block-size: auto; }
}

/* --- 15. Table of contents and filters (sticky rail) ---------------------- */

/* one system, two uses: on teaching a list of anchor links, on publications a
   list of filter buttons. A button is styled exactly like a link; the pressed
   filter wears the same orange mark as the section being read. */

/* the toc/filters live in the rail column even when the rail is otherwise
   empty (media), or when the filters are hidden until the script arrives */
.doc > .col { grid-column: 2; }
@media (max-width: 1068.98px) { .doc > .col { grid-column: 1; } }

.toc { align-self: start; grid-column: 1; grid-row: 1; }
.toc ol, .toc .chips { margin-top: var(--s3); display: grid; }
.toc a, .toc .chip {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: var(--s2);
  padding-block: 7px;
  font-size: .8125rem;
  line-height: 1.462;
  letter-spacing: -.006em;
  color: var(--ink-2);
  text-decoration: none;
  text-align: left;
  cursor: pointer;
  transition: color var(--state) var(--ease);
}
.toc a:hover, .toc .chip:hover { color: var(--ink); }
.toc a[aria-current="true"], .toc .chip[aria-pressed="true"] { color: var(--ink); font-weight: 600; }
/* orange marks where you are — and, on publications, what you are looking at */
.toc a, .toc .chip { position: relative; }
.toc a[aria-current="true"]::before, .toc .chip[aria-pressed="true"]::before {
  content: "";
  position: absolute;
  border-radius: 2px;
  background: var(--accent);
}

@media (min-width: 1069px) {
  .toc { position: sticky; top: calc(var(--bar) + var(--s5)); }
  .toc a[aria-current="true"]::before, .toc .chip[aria-pressed="true"]::before { left: -14px; top: 8px; bottom: 8px; width: 2px; }
}
@media (max-width: 1068.98px) {
  .toc {
    position: sticky;
    top: var(--bar);
    z-index: 40;
    margin-inline: -20px;
    padding-inline: 20px;
    background: var(--bg);
    border-bottom: 1px solid var(--line);
    margin-bottom: var(--s7);
  }
  .toc .lbl {
    position: absolute;
    width: 1px; height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }
  .toc ol, .toc .chips {
    margin-top: 0;
    display: flex;
    gap: var(--s5);
    overflow-x: auto;
    scrollbar-width: none;
  }
  .toc ol::-webkit-scrollbar, .toc .chips::-webkit-scrollbar { display: none; }
  /* the last label fades rather than being cut off; the fade is the affordance */
  @supports (mask-image: linear-gradient(90deg, #000, transparent)) {
    .toc ol, .toc .chips {
      -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 32px), transparent);
      mask-image: linear-gradient(90deg, #000 calc(100% - 32px), transparent);
      padding-inline-end: 32px;
    }
  }
  /* the strip scrolls; the labels do not compress */
  .toc li, .toc .chip { flex: none; }
  .toc a, .toc .chip { padding-block: 12px; white-space: nowrap; }
  .toc a[aria-current="true"]::before, .toc .chip[aria-pressed="true"]::before { left: 0; right: 0; bottom: 0; height: 2px; }
}
@media (min-width: 735px) and (max-width: 1068.98px) {
  .toc { margin-inline: -22px; padding-inline: 22px; }
}

/* --- 16. Groups and rows ------------------------------------------------- */

.group { scroll-margin-top: var(--s4); }
.group + .group { margin-top: clamp(32px, 4vw, 52px); }

.group-head {
  position: relative;
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  padding-bottom: var(--s3);
  border-bottom: 1px solid var(--line-2);
}
/* the first stretch of the rule under a group is drawn in the accent */
.group-head::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -1px;
  width: 44px;
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
}
/* the glyph names the kind of work; the flex parent aligns text on the
   baseline, so the glyph asks to be centred instead */
.group-head .i { align-self: center; flex: none; color: var(--ink-3); }
.group-head h2 {
  font-family: var(--display-face);
  font-size: 1.3125rem;
  font-weight: 600;
  letter-spacing: -.019em;
}
.group-note { margin-top: var(--s4); font-size: .9375rem; letter-spacing: -.016em; color: var(--ink-2); max-width: var(--measure); }

/* the one state marker on the site */
.now {
  padding: 3px 10px;
  border-radius: var(--pill);
  background: var(--accent-soft);
  font-size: .8125rem;
  font-weight: 600;
  letter-spacing: -.012em;
  color: var(--accent-ink);
}

.rows > li { border-bottom: 1px solid var(--line); }
.rows > li:last-child { border-bottom: 0; }

/* a year of courses two abreast, once there is room — the ledger keeps its
   hairlines on every row, including the last, because rows now end mid-air */
@media (min-width: 735px) {
  .rows--cols {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0 var(--s7);
    align-items: start;
  }
  .rows--cols > li, .rows--cols > li:last-child { border-bottom: 1px solid var(--line); }
}

/* rows that are one-liners do not need the rhythm of a citation */
.rows--tight .row { padding-block: var(--s3); }

.row {
  display: grid;
  grid-template-columns: 3.25rem minmax(0, 1fr);
  align-items: baseline;
  gap: 0 var(--s4);
  padding-block: var(--s4);
}
.yr {
  font-size: .8125rem;
  letter-spacing: 0;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
/* a year is printed once per run — the repeats stay in the document for
   screen readers, for selection and for print, and leave the page quiet */
.row--same .yr { opacity: 0; }

.row h3 {
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.38;
  letter-spacing: -.021em;
}
.row h3 a { text-decoration: none; }
.row h3 .out { margin-left: 5px; }
.row h3 a:hover { color: var(--accent-ink); }
.row .au { margin-top: 4px; font-size: .9375rem; letter-spacing: -.016em; color: var(--ink-2); max-width: none; }
.row .au .me { color: var(--ink); font-weight: 600; }
.row .src {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px var(--s4);
  margin-top: 4px;
  font-size: .8125rem;
  letter-spacing: -.006em;
  color: var(--ink-2);
  max-width: none;
}
.row .lang { color: var(--ink-2); }

.row--flush { grid-template-columns: minmax(0, 1fr); }

@media (max-width: 599px) {
  .row { grid-template-columns: minmax(0, 1fr); }
  .yr { line-height: 1.4; margin-bottom: 4px; }
  /* collapsed, not removed: still announced, still selectable, still printed */
  .row--same .yr {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
  }
}

/* --- 17. Courses --------------------------------------------------------- */

.course { padding-block: var(--s4); }
.course .code {
  margin-bottom: 4px;
  font-size: .8125rem;
  letter-spacing: 0;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
.course h3 {
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.38;
  letter-spacing: -.021em;
}
.course .meta { margin-top: 5px; font-size: .9375rem; letter-spacing: -.016em; color: var(--ink-2); max-width: none; }
.course .inst { margin-top: 4px; font-size: .8125rem; line-height: 1.462; letter-spacing: -.006em; color: var(--ink-2); max-width: none; }
.course .n-students { font-variant-numeric: tabular-nums; letter-spacing: 0; }
.course .note,
.course .offline {
  margin-top: var(--s2);
  font-size: .8125rem;
  letter-spacing: -.006em;
  color: var(--ink-2);
  max-width: none;
}
.course .refs { margin-top: var(--s3); }

/* --- 18. Footer ---------------------------------------------------------- */

/* the last block already ends on its own padding; the footer only has to
   clear it, not open a second gap the size of the first */
.foot {
  margin-top: var(--s5);
  padding-block: clamp(32px, 4vw, 48px);
  border-top: 1px solid var(--line);
  font-size: .8125rem;
  letter-spacing: -.006em;
  color: var(--ink-2);
}
.foot-in { gap: var(--s5) var(--gap); align-items: start; }
.foot .sig { grid-column: 2; }
.foot .who { color: var(--ink); font-weight: 600; }

/* the profiles sign off as a row of drawn glyphs — each link carries its name
   in aria-label and title, so nothing is icon-only to a screen reader */
.foot .social { grid-column: 2; display: flex; flex-wrap: wrap; gap: var(--s3); }
.foot .social a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border: 1px solid var(--line-2);
  border-radius: var(--pill);
  color: var(--ink-2);
  transition: color var(--state) var(--ease),
              border-color var(--state) var(--ease),
              background-color var(--state) var(--ease);
}
.foot .social a:hover {
  color: var(--accent-ink);
  background: var(--accent-soft);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}
@supports not (color: color-mix(in srgb, red 50%, transparent)) {
  .foot .social a:hover { border-color: var(--line-3); }
}

@media (max-width: 1068.98px) {
  .foot .sig, .foot .social { grid-column: 1; }
}

/* --- 19. Utilities ------------------------------------------------------- */

/* a grid item that must be allowed to shrink below its longest DOI */
.col { min-width: 0; }

/* --- 20. Scroll ---------------------------------------------------------- */

/* Seven behaviours. None of them is load-bearing: with this section deleted
   every page is still complete, still legible and still navigable, which is
   why none of them is written in JavaScript either. Two are plain sticky
   positioning, one runs once on load, three are driven by the scroll
   position itself — so they run off the main thread and cannot stutter a
   list of ninety-five entries — and one runs only between pages.

   The whole section is inside `@media screen`, so nothing here can hide a
   word on paper, and everything that moves is additionally inside
   `prefers-reduced-motion: no-preference`. */

@media screen {

  @media (min-width: 1069px) {
    /* 1. The label keeps pace with the section it names, instead of
          scrolling away from the work it is supposed to be labelling. */
    .blk .rail { position: sticky; top: calc(var(--bar) + var(--s5)); }

    /* 2. On a list page, the group you are inside stays named at the top of
          the screen — the answer to "which of the seven is this?" */
    .group-head {
      position: sticky;
      top: var(--bar);
      z-index: 5;
      padding-top: var(--s3);
      background: var(--bg);
    }
  }

  @media (prefers-reduced-motion: no-preference) {

    /* 3. The opener arrives once, in under a second, three lines in step.
          On a list page the contents and the list itself follow, two more
          steps in the same walk, so the whole page arrives as one gesture. */
    .stage .col > *, .pagehead .col > *, .pagehead .idx {
      animation: rise-in .7s var(--ease) both;
    }
    .stage .col > :nth-child(2), .pagehead .col > :nth-child(2) { animation-delay: .06s; }
    .stage .col > :nth-child(3), .pagehead .col > :nth-child(3) { animation-delay: .12s; }
    .stage .col > :nth-child(4), .pagehead .col > :nth-child(4) { animation-delay: .18s; }
    .doc .toc   { animation: rise-in .7s var(--ease) both; animation-delay: .18s; }
    .doc > .col { animation: rise-in .7s var(--ease) both; animation-delay: .24s; }

    @supports (animation-timeline: scroll()) {
      /* 4. How far you have read, drawn on the hairline the header already
            has. Nothing is added to the page: the line was there, and now
            the part of it you have passed is orange. */
      .bar::after {
        content: "";
        position: absolute;
        left: 0;
        right: 0;
        bottom: -1px;
        height: 1px;
        background: var(--accent);
        transform: scaleX(0);
        transform-origin: 0 50%;
        animation: progress linear both;
        animation-timeline: scroll(root block);
      }
    }

    @supports (animation-timeline: view()) {
      /* 5. Blocks arrive as they are reached: twelve pixels and a fade,
            over the `entry` range exactly — so a block has finished
            arriving at the moment you can see all of it, and no text is
            ever sitting at part strength where it can be read. A tall
            block takes longer to arrive than a short one, which is the
            right way round. The list pages arrive the same way — one
            publication, course or article at a time — and the footer
            signs off with the same step. */
      .blk .lbl, .q, .tags li, .card, .index-list li, .faq details,
      .rows > li, .group-note, .foot-in > *, .sublbl {
        animation: rise linear both;
        animation-timeline: view();
        animation-range: entry;
      }
      /* a sticky heading is moved by the scroller and by nothing else */
      .group-head {
        animation: fade linear both;
        animation-timeline: view();
        animation-range: entry;
      }

      /* 6. The two questions draw their own underline as they are read.
            It is the only orange on the site that moves under its own
            steam, and it does so twice, on the two phrases that matter. */
      .q .mk {
        animation: mark linear both;
        animation-timeline: view();
        animation-range: cover 20% cover 50%;
      }
    }

    /* 7. One page hands over to the next. Where the browser can (Chromium,
          Safari 18.2+), following a link within the site is a quiet
          cross-fade rather than a flash of blank page — and the bar is
          named, so the one piece of chrome every page shares holds still
          while the page changes underneath it. Elsewhere this rule is
          simply ignored and navigation is what it always was. */
    @view-transition { navigation: auto; }
    .bar { view-transition-name: bar; }
  }
}

@keyframes rise-in   { from { opacity: 0; transform: translateY(10px); } }
@keyframes rise      { from { opacity: 0; transform: translateY(12px); } }
@keyframes fade      { from { opacity: 0; } }
@keyframes progress  { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes mark      { from { background-size: 0 .055em; } }

/* --- 21. Forced colours -------------------------------------------------- */

/* Windows High Contrast replaces every background, which would take the
   current-page rule and the panels with it. Keep the marks that carry
   meaning; let the decorative ones go. */
@media (forced-colors: active) {
  .nav a[aria-current="page"]::after,
  .toc a[aria-current="true"]::before,
  .toc .chip[aria-pressed="true"]::before,
  .group-head::after,
  .stage h1::before, .pagehead h1::before,
  .bar::after { background: CanvasText; }
  .card, .now, .tags li { border: 1px solid CanvasText; }
  .tile { border-color: CanvasText; }
  body::before { display: none; }
}

/* --- 22. Print ----------------------------------------------------------- */

@page { margin: 18mm 16mm; }

@media print {
  /* the canvas is painted by html's background now; without this a reader who
     saved the dark toggle would print onto a #0a0a0b ground */
  html { background: #fff; }
  .bar, .toc, .skip, .foot ul, .index-list .go { display: none !important; }
  /* the glyphs are decorative on screen and clutter on paper */
  .lbl .i, .idx .i, .group-head .i,
  .index-list .lead, .btn .i, .btn .mark { display: none !important; }
  .group-head { position: static; padding-top: 0; }
  .tile { box-shadow: none; background: none; border-color: #ccc; }
  body { font-size: 10pt; line-height: 1.4; color: #000; background: #fff; }
  .stage, .blk, .doc, .pagehead, .foot-in { display: block; }
  .wrap { max-width: none; padding-inline: 0; }
  .col, .rail { max-width: 132mm; }
  .rail { margin-bottom: 6pt; }
  h1, h2, h3 { break-after: avoid; }
  .row, .course, .card, .faq details { break-inside: avoid; }
  .row--same .yr {
    position: static;
    width: auto; height: auto;
    margin: 0;
    overflow: visible;
    clip-path: none;
    opacity: 1;
  }
  /* nowrap would inherit into ::after and forbid the URL from ever breaking */
  .ref, .btn { white-space: normal; }
  .ref::after, .btn::after { overflow-wrap: anywhere; }
  /* a closed <details> would otherwise print as a missing answer */
  .faq details > .a { display: block !important; }
  @supports selector(::details-content) {
    .faq details::details-content {
      block-size: auto !important;
      content-visibility: visible !important;
    }
  }
  .card { background: none; border: 1px solid #ccc; box-shadow: none; }
  a { text-decoration: none; }
  .ref::after, .btn::after { content: " " attr(href); font-size: 8pt; }
}
