/* ==========================================================================
   Post-AI design system
   Adapted from Pasta-Lab.github.io's css/site.css (the PASTA lab site) --
   same design tokens, layout primitives, and component classes, retargeted
   at this site's data-driven pages.

   Dropped from the source file, and why:
     - .pasta, .pasta__row, .pasta__icon, .pasta__letter, .pasta__word
       (the "PASTA" backronym list) -- this site has no such list.
     - .hero__loop and its @keyframes pasta-loop / dark-theme / reduced-motion
       overrides -- the animated sprite-sheet hero graphic is Pasta-Lab-
       specific brand art (assets/brand/hero-loop-sheet.webp) that was not
       copied here.
     - .faculty (the multi-PI grid container) -- this site has one PI, not a
       faculty grid. .pi/.pi__frame/.pi__photo/etc. are kept since a single
       PI card still uses them.
     - .student-grid (the roster grid container) -- kept .student/.student__*
       since they may still style individual entries.
     - .openings (the job-openings list container) -- kept .opening/.opening__*
       for the same reason.
     - .partner, .partner-grid, .partner__logo, .partner__mark, .partner__name,
       .partner__where, and .logo--perplexity/.logo--vals/.logo--ethereum/
       .logo--amazon (the "Collaborators" section and its per-partner logo
       masks) -- no data/ file describes collaborators or partners, and the
       four .logo--* rules pointed at ../assets/logos/*.svg, a directory that
       was deliberately not copied into this repo's assets/ tree. If a
       collaborators section is ever added here, assets/logos/ (with its own
       SVGs) needs to be added too -- it does not exist today.
   Every design token (:root and :root[data-theme='dark'] custom properties),
   the spacing/type scale, and every other component class are UNCHANGED --
   the palette and scales are the whole point of borrowing this stylesheet.

   IMPORTANT: this stylesheet requires STANDARDS mode -- the inverse of
   css/style.css, which requires quirks mode (see scripts/check-pages.mjs).
   Any page that links css/post-ai.css MUST start with <!DOCTYPE html> and
   MUST NOT also link css/style.css; scripts/check-pages.mjs enforces both
   halves of that rule for every page in POST_AI_PAGES. */

/* --- Fonts ---------------------------------------------------------------- */

@font-face {
  font-family: 'Space Grotesk';
  src: url('../assets/fonts/space-grotesk-latin.woff2') format('woff2');
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
    U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'JetBrains Mono';
  src: url('../assets/fonts/jetbrains-mono-latin.woff2') format('woff2');
  font-weight: 400 600;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
    U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* --- Tokens --------------------------------------------------------------- */

:root {
  /* surfaces and ink */
  --canvas: #f4f4f2;
  --surface: #ffffff;
  --surface-2: #ecece9;
  --surface-inset: #e6e6e2;
  --ink: #17171a;
  --ink-2: #4a4a50;
  --ink-3: #5f5f67;
  --line: #e0e0dc;
  --line-strong: #cfcfc9;

  /* one accent, tonal per theme */
  --accent: #b84a1e;
  --accent-hover: #a8431b;
  --accent-on: #ffffff;
  /* For small text sitting on an accent-wash background, where the plain
     accent drops below 4.5:1. */
  --accent-strong: #9a3c17;
  --accent-wash: rgba(184, 74, 30, 0.09);
  --accent-line: rgba(184, 74, 30, 0.28);

  /* brand illustration colours, decoration only */
  --semolina: #fdcf6b;
  --semolina-deep: #dca440;
  --basil: #8cb581;
  --sauce: #f38b5d;

  /* shape: the brand is voxels, so every corner is square */
  --r-sm: 0;
  --r-md: 0;
  --r-lg: 0;
  --r-pill: 0;

  /* depth, tinted with ink rather than black */
  --shadow-sm: 0 1px 2px rgba(23, 23, 26, 0.05);
  --shadow-md: 0 2px 4px rgba(23, 23, 26, 0.04), 0 8px 20px rgba(23, 23, 26, 0.06);
  --shadow-lg: 0 4px 8px rgba(23, 23, 26, 0.05), 0 18px 44px rgba(23, 23, 26, 0.1);

  /* type */
  --font-sans: 'Space Grotesk', ui-sans-serif, system-ui, -apple-system, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;

  /* layout */
  --shell: 1200px;
  --gutter: clamp(1.25rem, 4vw, 3rem);
  --nav-h: 68px;

  /* motion */
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --dur: 260ms;

  color-scheme: light;
}

:root[data-theme='dark'] {
  --canvas: #131315;
  --surface: #1b1b1e;
  --surface-2: #232327;
  --surface-inset: #101012;
  --ink: #edede9;
  --ink-2: #b4b4ba;
  --ink-3: #9a9aa2;
  --line: #2c2c31;
  --line-strong: #3a3a41;

  --accent: #f38b5d;
  --accent-hover: #ffa77c;
  --accent-on: #131315;
  --accent-strong: #f79a70;
  --accent-wash: rgba(243, 139, 93, 0.12);
  --accent-line: rgba(243, 139, 93, 0.32);

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.3), 0 8px 20px rgba(0, 0, 0, 0.4);
  --shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.3), 0 18px 44px rgba(0, 0, 0, 0.5);

  color-scheme: dark;
}

/* --- Base ----------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--nav-h) + 1.5rem);
}

body {
  margin: 0;
  /* A long unbroken token has no break opportunity, so on a phone it pushes
     the whole page sideways rather than wrapping. The longest on the site is a
     71-character Etherscan URL in challenges/level_4_82457693012849571639.html,
     which overflows a 375px screen on its own. break-word only acts when the
     alternative is overflow, so ordinary text is untouched.
     Everything else that could scroll horizontally is already contained: each
     grid has a narrow-screen override, pre and table scroll inside themselves,
     and main's overflow-x: clip holds the full-bleed rules. */
  overflow-wrap: break-word;
  background: var(--canvas);
  color: var(--ink-2);
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.65;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4 {
  margin: 0;
  color: var(--ink);
  font-weight: 600;
  letter-spacing: -0.022em;
  line-height: 1.12;
  text-wrap: balance;
}

p { margin: 0 0 1rem; max-width: 68ch; }
p:last-child { margin-bottom: 0; }

a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 3px; }
a:hover { color: var(--accent-hover); }

img { max-width: 100%; display: block; }

ul, ol { margin: 0; padding: 0; }

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

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

main:focus { outline: none; }

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

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

.skip-link {
  position: absolute;
  left: 0.5rem;
  top: -4rem;
  z-index: 200;
  padding: 0.7rem 1.1rem;
  color: var(--accent-on);
  background: var(--accent);
  font-size: 0.9rem;
  font-weight: 600;
  text-decoration: none;
  transition: top 160ms var(--ease);
}

.skip-link:focus { top: 0.5rem; color: var(--accent-on); }

/* --- Navigation ----------------------------------------------------------- */

.nav {
  position: sticky;
  top: 0;
  z-index: 100;
  height: var(--nav-h);
  background: color-mix(in srgb, var(--canvas) 86%, transparent);
  backdrop-filter: saturate(1.6) blur(14px);
  -webkit-backdrop-filter: saturate(1.6) blur(14px);
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}

.nav[data-stuck='true'] {
  border-bottom-color: var(--line);
  background: color-mix(in srgb, var(--canvas) 94%, transparent);
}

.nav__inner {
  height: 100%;
  display: flex;
  align-items: center;
  gap: 1.5rem;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  color: var(--ink);
  text-decoration: none;
  font-weight: 600;
  letter-spacing: -0.02em;
  flex: none;
}

.brand__mark { width: 38px; height: auto; }
.brand__name { font-size: 1.0625rem; }

.brand:hover { color: var(--ink); }
.brand:hover .brand__mark { transform: rotate(-6deg) scale(1.06); }
.brand__mark { transition: transform var(--dur) var(--ease); }

.nav__menu { margin-left: auto; }

.nav__links {
  display: flex;
  align-items: center;
  gap: clamp(0.35rem, 1.4vw, 1rem);
  list-style: none;
}

.nav__links a {
  display: block;
  padding: 0.4rem 0.55rem;
  color: var(--ink-2);
  font-size: 0.9375rem;
  font-weight: 500;
  text-decoration: none;
  border-radius: var(--r-sm);
  white-space: nowrap;
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease);
}

.nav__links a:hover { color: var(--ink); background: var(--surface-2); }
.nav__links a[aria-current='page'] { color: var(--accent); }

.nav__tools { display: flex; align-items: center; gap: 0.35rem; flex: none; }

.icon-btn {
  width: 36px; height: 36px;
  display: grid; place-items: center;
  padding: 0;
  color: var(--ink-2);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

.icon-btn:hover { color: var(--ink); background: var(--surface-2); border-color: var(--line); }
.icon-btn svg { width: 19px; height: 19px; fill: currentColor; }

.theme-toggle .theme-toggle__moon { display: none; }
:root[data-theme='dark'] .theme-toggle .theme-toggle__moon { display: block; }
:root[data-theme='dark'] .theme-toggle .theme-toggle__sun { display: none; }

.nav__burger { display: none; }

@media (max-width: 860px) {
  .nav__burger { display: grid; }
  /* Burger beside the theme toggle at the right edge, rather than stranded
     next to the brand.
     The auto margin moves from .nav__menu to .brand, so the empty space opens
     AFTER the brand instead of before the menu -- everything downstream then
     collects at the right. Done this way rather than with flex `order` on the
     burger: order changes paint position only, so the tab sequence would still
     run brand, burger, theme while the eye sees brand, theme, burger, and
     keyboard focus would jump right then back left. Here visual order and DOM
     order stay identical.
     .nav__menu is a zero-width flex item at this size (.nav__links is
     position: fixed), so it contributes nothing but two container gaps between
     the burger and the toggle -- hence the tighter gap below, which is what
     makes the two icons read as one cluster instead of two controls. */
  .brand { margin-right: auto; }
  .nav__menu { margin-left: 0; }
  .nav__inner { gap: 0.35rem; }
  .nav__links {
    position: fixed;
    inset: var(--nav-h) 0 auto 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 0.5rem var(--gutter) 1rem;
    background: var(--canvas);
    border-bottom: 1px solid var(--line);
    box-shadow: var(--shadow-md);
    transform: translateY(-8px);
    opacity: 0;
    visibility: hidden;
    transition: opacity 180ms var(--ease), transform 180ms var(--ease),
      visibility 180ms;
  }
  .nav__links[data-open='true'] { opacity: 1; transform: none; visibility: visible; }
  .nav__links a { padding: 0.75rem 0.5rem; font-size: 1rem; }
}

/* --- Sections and headings ------------------------------------------------ */

.section { padding-block: clamp(3.5rem, 8vw, 6.5rem); }
.section--tight { padding-block: clamp(2.5rem, 5vw, 4rem); }
.section--edge { border-top: 1px solid var(--line); }

.section__head { margin-bottom: clamp(1.75rem, 4vw, 2.75rem); }

.section__head--split {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.section__head h2 { font-size: clamp(1.65rem, 3.2vw, 2.35rem); }
.section__head p { margin-top: 0.85rem; max-width: none; line-height: 1.7; color: var(--ink-2); }

.page-head { padding-block: clamp(2.5rem, 6vw, 4.25rem) clamp(1.5rem, 3vw, 2.25rem); }
.page-head h1 { font-size: clamp(2.1rem, 5vw, 3.35rem); }
/* Intro copy spans its container so the text edges line up with the cards and
   section blocks below, rather than stopping at the default reading measure. */
.page-head p { margin-top: 1rem; max-width: none; font-size: 1.0625rem; line-height: 1.7; color: var(--ink-2); }

/* --- Buttons -------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.7rem 1.25rem;
  font-family: inherit;
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.2;
  white-space: nowrap;
  text-decoration: none;
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  cursor: pointer;
  transition: transform var(--dur) var(--ease), background var(--dur) var(--ease),
    border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}

.btn:hover { transform: translateY(-1px); }
.btn:active { transform: none; }

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

.btn--ghost { background: transparent; color: var(--ink); border-color: var(--line-strong); }
.btn--ghost:hover { background: var(--surface); border-color: var(--ink-3); color: var(--ink); }

.btn svg { width: 16px; height: 16px; fill: currentColor; flex: none; }

/* --- Hero ----------------------------------------------------------------- */

.hero {
  padding-top: clamp(2rem, 5vw, 4rem);
  padding-bottom: clamp(3rem, 6vw, 4.5rem);
  overflow: hidden;
}

/* Portrait left, identity right, bio spanning both (see .hero__bio further
   down, which is where the span is set). `auto` for the portrait column so it
   takes the width the image asks for rather than a share of the shell, and
   `start` so a short identity block does not float in the middle of a tall
   portrait.
 *
 * This rule and the 900px query below it are the ONLY .hero__grid
 * declarations in the file, and they are meant to stay that way: a second
 * copy appended near the bottom is how grid-template-columns here and in the
 * query below both went dead -- overridden unconditionally from 550 lines
 * away, so editing either one did nothing and every check stayed green. */
.hero__grid {
  display: grid;
  /* Identity column first, portrait second. */
  grid-template-columns: minmax(0, 1fr) minmax(0, auto);
  gap: clamp(1.5rem, 5vw, 4rem);
  /* stretch, not start: the art column then takes the identity block's full
     height and .pi__frame grows into it, which is what ties the portrait's
     height to the text beside it. Shrink the <h1> and the image follows. */
  align-items: stretch;
}

.hero__title {
  font-size: clamp(2.4rem, 6.5vw, 4.2rem);
  font-weight: 700;
  letter-spacing: -0.045em;
  line-height: 0.98;
}

.hero__lede {
  margin-top: 1.4rem;
  max-width: 46ch;
  font-size: clamp(1.02rem, 1.5vw, 1.15rem);
  color: var(--ink-2);
}

.hero__lede b { color: var(--ink); font-weight: 600; }

.hero__art {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 190px;
  /* The cap lives HERE, on the container, not on the portrait alone.
     This is a column flex container, so align-items: stretch gives every child
     the column's width. The portrait carried its own 340px cap while the style
     chips below it stretched, which matched only by coincidence on a phone --
     there the content column IS about 340px. Once .hero__grid collapses to one
     column at 900px the column goes full width, the portrait stayed 340 and the
     chips ran to 740 on an iPad, wider than the image they control.
     Capping the container bounds both together, and gives .pi__frame its width
     by stretch -- which is what it needs, having no intrinsic width of its own
     (its only child is absolutely positioned, so it measured 0x280 before any
     width was supplied). */
  width: min(100%, 340px);
}

/* .pi__frame ships a fixed 280px height for its use in a card elsewhere. Here
   it fills whatever height the identity column leaves after the style chips,
   so a 300x340 portrait stops letterboxing against the frame's gradient --
   that grey down both sides was the frame being wider than the image needed. */
.hero__art .pi__frame { height: auto; flex: 1 1 auto; min-height: 190px; }

@media (max-width: 900px) {
  .hero__grid { grid-template-columns: minmax(0, 1fr); }
}

/* --- Statement ------------------------------------------------------------ */

/* Closes the hero rather than sitting in its own section. */
.statement {
  margin-top: clamp(2.5rem, 6vw, 4.5rem);
  max-width: min(100%, 52ch);
  font-size: clamp(1.3rem, 2.6vw, 1.95rem);
  font-weight: 500;
  line-height: 1.34;
  letter-spacing: -0.02em;
  color: var(--ink);
  text-wrap: pretty;
}

/* --- Tags and meta chips -------------------------------------------------- */

.tags { display: flex; flex-wrap: wrap; gap: 0.4rem; list-style: none; }

.tag {
  padding: 0.24rem 0.62rem;
  color: var(--ink-3);
  background: var(--surface-2);
  border-radius: var(--r-pill);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.02em;
}

.chip-row { display: flex; flex-wrap: wrap; gap: 0.45rem; margin-top: 1.15rem; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.32rem 0.75rem;
  color: var(--ink-2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  font-size: 0.82rem;
  font-weight: 500;
  text-decoration: none;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease),
    background var(--dur) var(--ease);
}

.chip:hover { color: var(--accent); border-color: var(--accent-line); background: var(--accent-wash); }

/* --- Filter buttons ------------------------------------------------------- */

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
  margin-bottom: 2rem;
}

.filter {
  padding: 0.42rem 0.95rem;
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--ink-2);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  cursor: pointer;
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

.filter:hover { color: var(--ink); border-color: var(--line-strong); }

.filter[aria-pressed='true'] {
  color: var(--accent-on);
  background: var(--accent);
  border-color: var(--accent);
}

.filter__count {
  margin-left: 0.5rem;
  font-family: var(--font-mono);
  font-size: 0.76rem;
  opacity: 0.65;
}

/* --- Publications --------------------------------------------------------- */

.result-count {
  margin: -1rem 0 0;
  color: var(--ink-3);
  font-size: 0.9rem;
}

.pub-year {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin: 2.5rem 0 1.1rem;
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 0.92rem;
  font-weight: 600;
  letter-spacing: 0.04em;
}

.pub-year::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--line);
}

.pub-list { list-style: none; display: grid; gap: 0.5rem; }

.pub {
  display: grid;
  grid-template-columns: 7.5rem minmax(0, 1fr);
  gap: 1.15rem;
  padding: 1.05rem 1.15rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}

.pub:hover { border-color: var(--line-strong); box-shadow: var(--shadow-sm); }

.pub__venue {
  align-self: start;
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 0.755rem;
  font-weight: 600;
  line-height: 1.45;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  overflow-wrap: break-word;
}

.pub__title {
  margin: 0;
  font-size: 1.03rem;
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: -0.012em;
}

.pub__title a { color: var(--ink); text-decoration: none; }
.pub__title a:hover { color: var(--accent); }

.pub__authors {
  margin: 0.4rem 0 0;
  max-width: none;
  font-size: 0.9rem;
  color: var(--ink-3);
  line-height: 1.55;
}

.pub__authors .is-pi { color: var(--ink); font-weight: 600; }

.pub__domains { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.6rem; }

.domain {
  padding: 0.12rem 0.5rem;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--ink-2);
  border: 1px solid var(--line-strong);
}

/* All four read the same. One accent runs the page, and the label text already
   says which domain it is. */

.pub__award {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-top: 0.55rem;
  padding: 0.2rem 0.6rem;
  color: var(--ink);
  background: color-mix(in srgb, var(--semolina) 34%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--semolina-deep) 40%, transparent);
  border-radius: var(--r-pill);
  font-size: 0.775rem;
  font-weight: 600;
}

.pub__links { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 0.7rem; }

.pub__link {
  padding: 0.18rem 0.55rem;
  color: var(--ink-2);
  background: var(--surface-2);
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: 0.735rem;
  text-decoration: none;
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease);
}

.pub__link:hover { color: var(--accent-on); background: var(--accent); }

@media (max-width: 700px) {
  .pub { grid-template-columns: 1fr; gap: 0.5rem; }
  .pub__venue { order: -1; }
}

/* --- People --------------------------------------------------------------- */

.style-switch {
  display: inline-flex;
  gap: 0.2rem;
  padding: 0.25rem;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
}

.style-switch button {
  padding: 0.38rem 0.9rem;
  font-family: inherit;
  font-size: 0.83rem;
  font-weight: 600;
  color: var(--ink-3);
  background: transparent;
  border: 0;
  border-radius: var(--r-pill);
  cursor: pointer;
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease);
}

.style-switch button:hover { color: var(--ink); }

.style-switch button[aria-pressed='true'] {
  color: var(--ink);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
}

.pi {
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}

.pi:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }

/* The three portrait styles have different framing (a bust versus a full
   figure), so the photo is given a definite box and contained inside it.
   Percentages on a grid item would resolve to auto here and overflow. */
.pi__frame {
  position: relative;
  height: 280px;
  background:
    radial-gradient(120% 78% at 50% 100%, var(--accent-wash), transparent 70%),
    var(--surface-inset);
  overflow: hidden;
}

.pi__photo {
  position: absolute;
  top: 1.1rem;
  left: 1.1rem;
  width: calc(100% - 2.2rem);
  height: calc(100% - 1.1rem);
  object-fit: contain;
  object-position: center bottom;
  transition: opacity 220ms var(--ease), transform 320ms var(--ease);
}

.pi__photo.is-swapping { opacity: 0; transform: translateY(6px) scale(0.98); }

.pi__body { padding: 1.15rem 1.25rem 1.35rem; }
.pi__name { font-size: 1.18rem; }
.pi__role { margin: 0.3rem 0 0; font-size: 0.875rem; color: var(--ink-3); }

.pi__affil {
  margin: 0.55rem 0 0;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--accent);
}

.pi__interests {
  margin: 0.75rem 0 0;
  font-size: 0.885rem;
  color: var(--ink-2);
  line-height: 1.55;
}

.pi__links { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 1rem; }

.student {
  padding: 0.85rem 1rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  transition: border-color var(--dur) var(--ease);
}

.student:hover { border-color: var(--accent-line); }
.student__name { display: block; color: var(--ink); font-weight: 600; font-size: 0.97rem; }
.student__name a { color: inherit; text-decoration: none; border-bottom: 1px solid var(--accent-line); }
.student:hover .student__name a { color: var(--accent); border-bottom-color: var(--accent); }
.student__affil { display: block; margin-top: 0.15rem; color: var(--ink-3); font-size: 0.82rem; }
.student__note { color: var(--ink-3); font-size: 0.78rem; font-style: italic; }

/* --- Projects ------------------------------------------------------------- */

/* This was `display: grid` with NO grid-template-columns, which makes a
   single column -- every project card spanned the full page width. The cards
   were never badly tuned; the column rule was missing. */
.projects {
  display: grid;
  gap: clamp(0.9rem, 2vw, 1.35rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
}

.project {
  padding: clamp(1rem, 2vw, 1.35rem);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
}

.project__title { font-size: clamp(1.4rem, 2.4vw, 1.8rem); }
.project__tagline { margin-top: 0.55rem; max-width: none; font-size: 1.05rem; color: var(--ink); font-weight: 500; }
.project__body { margin-top: 1rem; }
/* These sit inside a full-width card, so the text tracks the card rather
   than stopping at the default reading measure. */
.project__body p { max-width: none; font-size: 0.975rem; line-height: 1.7; }

.project__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.85rem;
}

.status {
  display: inline-flex;
  align-items: center;
  padding: 0.2rem 0.62rem;
  color: var(--accent-strong);
  background: var(--accent-wash);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-pill);
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.project__people {
  margin-top: 1.1rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--line);
  font-size: 0.875rem;
  color: var(--ink-3);
  max-width: none;
}

.project__people b { color: var(--ink-2); font-weight: 600; }

/* --- Join page ------------------------------------------------------------ */

.opening {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0.5rem;
  padding: clamp(1.25rem, 2.6vw, 1.75rem) 0;
  border-bottom: 1px solid var(--line);
}

.opening__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 1rem;
}

.opening__role { font-size: clamp(1.1rem, 2vw, 1.35rem); }

.opening__where {
  padding: 0.18rem 0.6rem;
  color: var(--accent-strong);
  background: var(--accent-wash);
  border: 1px solid var(--accent-line);
  font-family: var(--font-mono);
  font-size: 0.74rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  white-space: nowrap;
}

.opening__about { margin: 0; max-width: none; color: var(--ink-2); font-size: 0.965rem; line-height: 1.7; }

.contact-note {
  margin-top: clamp(2rem, 4vw, 3rem);
  padding: clamp(1.25rem, 2.5vw, 1.75rem);
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
}

.contact-note p { max-width: none; line-height: 1.7; }

/* --- Footer --------------------------------------------------------------- */

.footer {
  margin-top: clamp(3rem, 7vw, 5rem);
  padding-block: clamp(2.25rem, 4vw, 3.25rem);
  border-top: 1px solid var(--line);
  background: var(--surface);
}

.footer__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) repeat(2, minmax(0, 1fr));
  gap: clamp(1.5rem, 4vw, 3rem);
}

.footer__slogan {
  margin-top: 0.85rem;
  max-width: 34ch;
  font-size: 0.925rem;
  color: var(--ink-3);
}

.footer h2 {
  margin-bottom: 0.75rem;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.footer ul { list-style: none; display: grid; gap: 0.45rem; }
.footer ul a { color: var(--ink-2); font-size: 0.92rem; text-decoration: none; }
.footer ul a:hover { color: var(--accent); }

.footer__base {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 1rem;
  margin-top: clamp(2rem, 4vw, 3rem);
  padding-top: 1.35rem;
  border-top: 1px solid var(--line);
  font-size: 0.85rem;
  color: var(--ink-3);
}

@media (max-width: 700px) {
  .footer__grid { grid-template-columns: 1fr 1fr; }
  .footer__grid > :first-child { grid-column: 1 / -1; }
}

/* --- States --------------------------------------------------------------- */

.state {
  padding: 2.5rem 1.5rem;
  text-align: center;
  color: var(--ink-3);
  background: var(--surface);
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-lg);
}

.state p { margin-inline: auto; }
.state--error { border-color: var(--accent-line); color: var(--ink-2); }

/* --- Reveal on scroll ----------------------------------------------------- */

/* Scoped to .js, set synchronously in the document head, so the page is fully
   readable when scripting is off instead of stuck at zero opacity. */
@media (prefers-reduced-motion: no-preference) {
  .js [data-reveal] {
    opacity: 0;
    transform: translateY(16px);
    transition: opacity 620ms var(--ease), transform 620ms var(--ease);
    transition-delay: calc(var(--i, 0) * 70ms);
  }
  .js [data-reveal].is-visible { opacity: 1; transform: none; }

  .js .hero [data-reveal] { transition-duration: 760ms; }
}

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

/* --- Print ---------------------------------------------------------------- */

@media print {
  .nav, .footer, .filters, .hero__art { display: none; }
  body { background: #fff; color: #000; }
}

/* The Chinese name rides beside the Latin one at a smaller size, so the pair
   reads as one signature rather than two competing names. */
.brand__cn {
  margin-left: 0.4rem;
  color: var(--ink-3);
  font-size: 0.9375rem;
  font-weight: 500;
}

/* The header row collapses in stages, dropping whatever costs least first.
   At full width it holds brand + five links + four icons + theme toggle +
   version switch, which needs about 1100px. The burger only arrives at 860px,
   so every width in between rendered a nav wider than its own viewport and
   put a horizontal scrollbar on the whole page -- measured at 861px it hung
   196px past the edge, with the version switch clipped mid-word.

   Measured overflow, and what removing each part recovers:

     width   as-was   -social   -switch   -both
      861     196       39        32        0
      900     161        4         0        0
      960     107        0         0        0
     1050      25        0         0        0

   Neither alone clears 861px, hence two stages. Social goes first because it
   costs nothing: the footer's "Elsewhere" column carries all four of these
   links (check #14 enforces that in both directions), so hiding the header
   row cannot strand a profile. The version switch goes second, and only below
   960px where dropping the icons is not enough -- it is the one control with
   no duplicate anywhere, so it stays visible as long as the row can hold it.
   The nav links are never dropped here; they collapse into the burger at
   860px, and losing primary navigation would cost more than either. */
.nav__social { display: flex; align-items: center; gap: 0.15rem; }
@media (max-width: 1100px) { .nav__social { display: none; } }
@media (max-width: 960px) { .nav__tools .mode-switch { display: none; } }

/* The bio spans both hero columns. It is prose meant to be read; beside the
   portrait it inherited hero__lede's 46ch cap and wrapped into a narrow
   ribbon, which cost vertical space AND readability.
 *
 * The columns and align-items this pairs with live on .hero__grid up in the
 * Hero section, NOT here: a copy of them used to sit on this line, silently
 * beating the originals from 550 lines away. */
.hero__bio {
  grid-column: 1 / -1;
  margin-top: 1.5rem;
  color: var(--ink-2);
  font-size: clamp(1.02rem, 1.5vw, 1.12rem);
  line-height: 1.65;
}
.hero__bio p + p { margin-top: 0.85rem; }
/* The user asked in plain words that this bio use the entire linewidth.
   Without this, emitting real <p> tags (see renderBio in js/home.js) pulls
   in the global `p { max-width: 68ch }` rule, which would cap the bio at
   roughly two-thirds of the shell -- narrower than intended, not wider. */
.hero__bio p { max-width: none; }
.hero__bio strong { color: var(--ink); font-weight: 600; }
.hero__cn { color: var(--ink-3); font-size: 0.62em; font-weight: 500; }
.hero__art .style-switch { margin-top: 1.1rem; justify-content: center; }

/* 900px, the same breakpoint .hero__grid stacks at up in the Hero section,
   not a nearby-but-different one: two breakpoints for the same layout is how
   the 833-900px silent-two-column gap got introduced in the first place.
   .hero__grid's own stacking declaration stays with the rest of that rule and
   is deliberately not repeated here; this block exists solely because
   .hero__bio's `grid-column: 1 / -1` is set just above and has to be undone
   after it. */
@media (max-width: 900px) {
  .hero__bio { grid-column: auto; }
}

/* A slim accent band, not a box: this sits directly under the hero and must
   not out-shout it. */
.recruit {
  padding: clamp(1.1rem, 2.5vw, 1.6rem) 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  background: var(--accent-wash);
}
/* max-width: none on both -- the global `p { max-width: 68ch }` capped these
   two blocks while every neighbour spanned the full shell, so the recruiting
   callout and the teaching prose read as indented for no stated reason. Same
   fix the hero bio already carries. */
.recruit p { margin: 0 0 0.6rem; max-width: none; color: var(--ink-2); }
.recruit p:last-child { margin-bottom: 0; }
.recruit ul { margin: 0.3rem 0 0.7rem 1.1rem; color: var(--ink-2); }

/* .callout-list's own styling (list-style:none plus a bullet via ::before)
   lives only in css/style.css, which the Post-AI pages never load (quirks
   mode, out of scope here) -- so without this, the recruiting callout's
   bullets fall back to default browser discs. Mirrors css/style.css:730-738,
   adapted to sit as a li::before marker (that file's version puts the bullet
   in margin-right on the li itself, which this page's list has no room for
   once list-style:none removes the default marker box). */
.recruit .callout-list { list-style: none; margin-left: 0; padding-left: 1.6em; }
.recruit .callout-list li { position: relative; }
.recruit .callout-list li::before { content: "•"; position: absolute; left: -1.1em; color: var(--accent); }

.news-rows { list-style: none; margin: 0; padding: 0; }
.news-row {
  display: grid;
  grid-template-columns: 7.5rem minmax(0, 1fr);
  gap: 0 1rem;
  padding: 0.55rem 0 0.55rem 0.85rem;
  border-bottom: 1px solid var(--line);
  box-shadow: inset 2px 0 0 transparent;
}
.news-row__date {
  color: var(--ink-3);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  letter-spacing: 0.02em;
}
.news-row__body { color: var(--ink-2); }
/* An inset shadow rather than a border: a real border would widen the box and
   push pinned rows out of alignment with the rest. Every row reserves the same
   left padding, so only the colour changes between marked and unmarked. */
.news-row--pinned { box-shadow: inset 2px 0 0 var(--accent); }
.news-row--pinned .news-row__body { color: var(--ink); }
/* column-reverse paints the <ul> above the <summary> while leaving the source
   order the HTML parser requires (summary must be the first child). */
/* No margin-top: the revealed rows continue the list above them, and any gap
   here reads as a section break rather than a continuation. The <summary>
   carries its own spacing instead, so the control still stands clear of the
   last row whether the list is open or closed. */
.news-more { display: flex; flex-direction: column-reverse; }
.news-more__open { display: none; }
.news-more[open] .news-more__closed { display: none; }
.news-more[open] .news-more__open { display: inline; }
.news-more > summary {
  margin-top: 0.55rem;
  cursor: pointer;
  color: var(--ink-3);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
}
.news-more > summary:hover { color: var(--accent); }

@media (max-width: 40rem) {
  .news-row { grid-template-columns: minmax(0, 1fr); gap: 0.15rem; }
}

/* --- Activity --------------------------------------------------------------
   A label rail on the left, content on the right, hairlines between groups.
   Cards cost padding and a border per item; a rail costs one column. */
.activity { display: grid; }
.activity__row {
  display: grid;
  grid-template-columns: 8.5rem minmax(0, 1fr);
  gap: 0 1.5rem;
  padding: 1.1rem 0;
  border-top: 1px solid var(--line);
}
.activity__row:last-child { border-bottom: 1px solid var(--line); }
/* The element is an <h3> (renderActivity in js/home.js -- these are the only
   names the four groups have in the document outline). Every declaration here
   already beat the `h1, h2, h3, h4` block at the top of this file on
   specificity, but that block ALSO sets font-weight and line-height, which
   this rule did not, so the switch from <div> would have quietly made these
   labels semibold and tightened their leading from 1.65 to 1.12 -- shifting
   them off the first line of the body beside them. `inherit` reproduces
   exactly what the <div> computed to, so the change is outline-only. */
.activity__label {
  color: var(--ink-3);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  font-weight: inherit;
  line-height: inherit;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.activity__body { color: var(--ink-2); }
/* Real markers. These lists were list-style: none, which left teaching,
   talks and service as undifferentiated runs of lines -- fine for a two-item
   card, poor for a fourteen-entry service list. */
.activity__body ul { list-style: disc; margin: 0; padding-left: 1.15rem; }
.activity__body li::marker { color: var(--ink-3); }
.activity__body li { padding: 0.15rem 0; }
.activity__body p { margin: 0.5rem 0 0; max-width: none; }

/* renderAwards emits an <h3> per group ("Academic Awards", "Selected
   Capture-The-Flag"). Unstyled inside this rail they render at full heading
   size and out-shout the section's own <h2> -- the same complaint recorded
   against the old .feature card layout. Style them down to a quiet
   sub-label, and this closes that carried-over item rather than moving it. */
.activity__body h3 {
  margin: 0.7rem 0 0.15rem;
  color: var(--ink-3);
  font-size: 0.875rem;
  font-weight: 600;
  letter-spacing: 0.01em;
}
.activity__body h3:first-child { margin-top: 0; }
/* Two layers: the group name on its own line, its entries bulleted beneath.
   This was a two-column grid holding a label against one comma-joined run of
   text -- compact, but a reader hunting for one programme committee had to
   read a sentence to find it. */
.activity__group + .activity__group { margin-top: 0.85rem; }
.activity__group-label {
  margin: 0 0 0.3rem;
  color: var(--ink);
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: 0;
}
.activity__group-items { margin: 0; padding-left: 1.15rem; list-style: disc; color: var(--ink-2); }

/* Below the rail's breakpoint the label becomes a heading above its content
   rather than a cramped gutter. */
@media (max-width: 46rem) {
  .activity__row { grid-template-columns: minmax(0, 1fr); gap: 0.4rem; }
}

/* Awards, now its own section rather than an Activity row. renderAwards emits
   an <h3> per group with a <ul> beneath, so this mirrors .activity__body's
   treatment instead of inventing a second look for the same shape. */
.awards { display: grid; gap: 1.25rem; }
/* Matched to .activity__group-label: both name a group inside a section, and
   they read as one system. Uppercase is dropped along with the size increase
   -- it works as a 13px label but shouts at 15px. */
.awards h3 {
  margin: 0 0 0.3rem;
  color: var(--ink);
  font-size: 0.9375rem;
  font-weight: 600;
}
.awards ul { margin: 0; padding-left: 1.15rem; list-style: disc; color: var(--ink-2); }
.awards li { padding: 0.15rem 0; }
.awards li::marker { color: var(--ink-3); }

/* An `{.award}` marker in data/*.md renders as class="award" -- one entry uses
   it today, the ASE 2024 service line, to say that year carried a
   Distinguished Reviewer award. The classic page marks it maroon; this page
   styled it not at all, so the data said "award" and the reader saw an
   ordinary year. The star matters more than the colour: colour alone is not a
   hint if you cannot see it, and this is the only place on the page where one
   item in a list means something the others do not. */
.award,
a.award {
  color: var(--accent);
  font-weight: 600;
}

.award::after {
  content: ' \2605';
  font-size: 0.82em;
  /* Not part of the link text: a screen reader announcing "2024 black star"
     helps nobody, and the title below carries the meaning in words. */
  speak: never;
}

/* The email puzzle, opened from any "drop me an email" link. <dialog> gives
   Escape, focus trapping and an inert backdrop for free -- all three are easy
   to get wrong by hand, and none of them is what this feature is about. */
.email-dialog {
  width: min(46rem, calc(100vw - 2rem));
  padding: clamp(1.25rem, 3vw, 2rem);
  color: var(--ink-2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg, 0 24px 60px rgba(0, 0, 0, 0.22));
}

.email-dialog::backdrop { background: rgba(20, 20, 18, 0.45); }

.email-dialog p { max-width: none; }
.email-dialog__title { margin: 0 0 0.4rem; font-size: 1.15rem; }
.email-dialog__lede { margin-bottom: 1rem; color: var(--ink-2); }
.email-dialog__lede code { font-size: 0.9em; }

/* The ciphertext is one unbroken 900-character hex string. Without this it
   either forces the dialog wider than the viewport or is clipped; wrapping
   mid-string is correct here because the reader is going to copy it, not read
   it. */
.email-dialog pre {
  max-height: 40vh;
  overflow: auto;
  padding: 0.8rem;
  background: var(--surface-2);
  border: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: 0.78rem;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-all;
}

.email-dialog form { margin-top: 1.1rem; text-align: right; }
.email-dialog button { cursor: pointer; font: inherit; }

/* --- side-page chrome ----------------------------------------------------- */

/* The pages beyond the homepage -- errors, leaderboards, the course page, the
   puzzle chambers -- carry a slim bar rather than the full nav.
   The homepage's nav exists to move between its own sections and to offer the
   site's outbound links; a 404 has no sections and a puzzle chamber should not
   be advertising anything. Name, a way home, and out of the way. */
.slim-nav {
  position: sticky;
  top: 0;
  z-index: 100;
  background: color-mix(in srgb, var(--canvas) 88%, transparent);
  backdrop-filter: saturate(1.6) blur(14px);
  -webkit-backdrop-filter: saturate(1.6) blur(14px);
  border-bottom: 1px solid var(--line);
}

.slim-nav__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  height: var(--nav-h);
}

main { overflow-x: clip; }

.slim-nav__tools {
  display: flex;
  align-items: center;
  gap: 0.9rem;
}

.slim-nav__back {
  color: var(--ink-3);
  font-size: 0.85rem;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--dur) var(--ease);
}

.slim-nav__back:hover { color: var(--accent); }

.slim-foot {
  margin-top: clamp(3rem, 8vw, 5rem);
  padding: clamp(1.5rem, 4vw, 2.25rem) 0;
  border-top: 1px solid var(--line);
  color: var(--ink-3);
  font-size: 0.85rem;
}

.slim-foot__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6rem 1.5rem;
}

.slim-foot p { margin: 0; max-width: none; }
.slim-foot a { color: var(--ink-2); }
.slim-foot a:hover { color: var(--accent); }

/* The whole reason these pages have no Original/AI-built switch: there is no
   hand-written version of them to switch to. Stating that is more honest than
   offering a toggle that would take the reader somewhere unrelated. */
.slim-foot__note { color: var(--ink-3); }

/* The course page sets its intro beside a "Course at a glance" card. That was
   a Bootstrap col-md-7 / col-md-5 pair, and bootstrap.css is not loaded here,
   so the two had silently stacked -- content intact, but not the layout the
   page was written for. Rewriting the grid to the new primitive is what the
   conversion is supposed to do with a grid, rather than leaving dead classes
   behind. */
.course-split {
  display: grid;
  gap: clamp(1.25rem, 3vw, 2.25rem);
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
  align-items: start;
}

@media (max-width: 52rem) {
  .course-split { grid-template-columns: minmax(0, 1fr); }
}

/* --- long-form content ---------------------------------------------------- */

/* The seventeen converted side pages are prose, not components. This
   stylesheet was written for the homepage -- .project cards, .news-row, the
   activity rail -- and styled almost no raw markup: no h2, no ul, no table, no
   code. The side pages hold 248 <td>, 203 <li>, 139 <p> and 69 <h2> between
   them, every one rendering at browser defaults inside a design that never
   expected them. That is why they read as a wall.
 *
 * DESCENDANT selectors, not child ones. The first version of this used
 * `.prose > *` throughout, which was elegant and reached nothing: the
 * converted pages keep their own wrapper divs, so not one heading or paragraph
 * on any of the seventeen is a direct child of .prose. Every rule was inert.
 *
 * TEXT gets a measure; TABLES and FIGURES do not. Capping everything makes a
 * course schedule unreadable; capping nothing lets paragraphs run to 1200px.
 * Only what is read line by line is constrained. */
.prose {
  max-width: none;
  color: var(--ink-2);
  font-size: 1.02rem;
  line-height: 1.7;
}

/* No measure. The Post-AI homepage runs its own prose at the full shell width
   (.hero__bio p is max-width: none), and a side page reading narrower than the
   page it links back to looks like a different site rather than the same one.
   Consistency with the homepage was chosen over the ~74ch reading measure
   typography would prefer -- a deliberate trade, not an oversight.
   The measure still has to be absent from the ELEMENTS too: capping p and ul
   individually was what left bare text nodes running wide while paragraphs
   beside them stopped short, which is the inconsistency this replaces. */
.prose p, .prose ul, .prose ol, .prose dl, .prose blockquote,
.prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 { max-width: none; }

/* Full width, and scrollable on narrow screens rather than crushed. */

@media (max-width: 62rem) {
  .prose table { display: block; overflow-x: auto; }
}

/* Explicit margins rather than an owl selector, for the same reason: an owl
   depends on siblings sharing a parent, and here they often do not. */
.prose p,
.prose ul,
.prose ol,
.prose dl,
.prose pre,
.prose table,
.prose blockquote,
.prose figure { margin: 0 0 1.15em; }

.prose h1, .prose h2, .prose h3, .prose h4 {
  color: var(--ink);
  line-height: 1.25;
  letter-spacing: -0.01em;
}

/* Generous space ABOVE a heading, little below: that is what makes a heading
   belong to the text it introduces instead of floating between blocks. The
   converted pages had headings sitting flush on the paragraph above them. */
.prose h3 { margin: 2em 0 0.42em; font-size: 1.2rem; }
.prose h4 { margin: 1.7em 0 0.38em; font-size: 1.02rem; }
.prose h5, .prose h6 { margin: 1.5em 0 0.35em; font-size: 0.95rem; }

.prose > :first-child,
.prose > div:first-child > :first-child { margin-top: 0; }

.prose h2 + *, .prose h2 + * > :first-child { margin-top: 0; }

.prose a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.prose a:hover { color: var(--ink); }

.prose ul, .prose ol { padding-left: 1.4rem; }
.prose ul { list-style: disc; }
.prose ol { list-style: decimal; }
.prose li::marker { color: var(--ink-3); }
.prose li { margin: 0.35em 0; }
.prose li > ul, .prose li > ol { margin: 0.35em 0 0; }

.prose b, .prose strong { color: var(--ink); font-weight: 650; }

.prose code {
  padding: 0.12em 0.36em;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 3px;
  font-family: var(--font-mono);
  font-size: 0.86em;
}

.prose pre {
  max-width: none;
  overflow-x: auto;
  padding: 0.9rem 1.1rem;
  background: var(--surface-2);
  border: 1px solid var(--line);
  line-height: 1.55;
}

.prose pre code { padding: 0; background: none; border: 0; font-size: 0.84rem; }

/* Full width and scrollable rather than crushed: a course schedule or a
   leaderboard has more columns than a text measure can hold. */
.prose table {
  width: 100%;
  max-width: none;
  border-collapse: collapse;
  font-size: 0.92rem;
}

.prose th {
  text-align: left;
  font-weight: 650;
  color: var(--ink);
  border-bottom: 2px solid var(--line);
}

.prose th, .prose td {
  padding: 0.55rem 0.7rem;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}

.prose tbody tr:hover { background: var(--surface-2); }

.prose img { max-width: 100%; height: auto; }

.prose blockquote { padding-left: 1rem; border-left: 2px solid var(--line); color: var(--ink-3); }

.prose hr { margin: 2.4em 0; border: 0; border-top: 1px solid var(--line); }

/* The course page's intro-beside-a-card block. */
.prose .course-split { margin-bottom: 1.15em; }
.prose .course-card {
  padding: 1.1rem 1.25rem;
  background: var(--surface-2);
  border: 1px solid var(--line);
}
.prose .course-card > :first-child { margin-top: 0; }
.prose .course-card > :last-child { margin-bottom: 0; }

/* --- the content's own vocabulary ---------------------------------------- */

/* These class names come from the pages themselves -- callout, tag-paper,
   tag-deadline, date, notes, page-header -- and were carrying meaning that
   nothing rendered. A warning block read as an ordinary paragraph; a deadline
   read the same as a lecture. Styling what the content already says is better
   than inventing a new vocabulary and rewriting seventeen pages to use it. */

/* Sections separated the way the homepage separates its own.
 *
 * There, a <section class="section section--edge"> is a block at PAGE width
 * with a top border, so the rule runs edge to edge while the content inside
 * stays in the 1200px shell. Side pages are one flat run of prose with no
 * section elements to hang that on, so the rule is drawn by the heading
 * itself and stretched back out to the viewport.
 *
 * The heading size is the homepage's own clamp, so a section title reads the
 * same on both, and the space around it is proportioned to match .section's
 * padding-block without matching it exactly -- prose sections come more often
 * than the homepage's five, and its full 6.5rem between each would leave the
 * page mostly gaps. */
/* Sections separated exactly as the homepage separates its own, because the
   two should look like one site.

   The homepage does it almost entirely with SPACE: .section carries
   padding-block clamp(3.5rem, 8vw, 6.5rem), .section__head a matching
   margin-bottom, and .section--edge a 1px var(--line) hairline. That line is
   nearly invisible alone -- the air around it is what reads as a break.

   Earlier revisions here compressed the space "because prose sections come
   more often than the homepage's five" and compensated with a darker, thicker
   rule and an accent marker. It separated sections perfectly well and looked
   like a different site. Same numbers as the homepage now, marker gone. */
.prose h2 {
  position: relative;
  /* padding-BLOCK, not padding-top. .section carries the same value on both
     edges, so on the homepage a boundary reads as: previous content, space,
     rule, space, heading. Setting only padding-top put all of it below the
     rule, so content ran straight into the next rule with nothing above it --
     the space was there, just all on one side.
     margin-top opens the space ABOVE the rule; the rule is drawn at the top of
     this box by ::after, and padding-top opens the space below it. */
  margin-top: clamp(3.5rem, 8vw, 6.5rem);
  padding-top: clamp(3.5rem, 8vw, 6.5rem);
  margin-bottom: clamp(1.75rem, 4vw, 2.75rem);
  font-size: clamp(1.65rem, 3.2vw, 2.35rem);
  line-height: 1.2;
}

/* The full-bleed rule. left: 50% then a -50vw margin escapes the centred
   shell without needing a full-width ancestor to hang off. */
/* Drawn at 32% of --ink, not var(--line).
   The geometry here was right for two revisions and invisible anyway: --line
   is #e0e0dc against a #f4f4f2 canvas, a contrast ratio of 1.20:1, where 1.0
   is literally invisible. Three rounds of "the separation is too subtle" were
   answered by making the RULE bigger while leaving it painted in the faintest
   grey the design owns. Measured: 32% lands at 2.06:1 -- clearly a line,
   without becoming a border.
   color-mix against --ink rather than a literal, so the dark theme inverts it
   along with everything else. */
/* The .section--edge hairline, stretched back to the viewport: the homepage
   gets full-bleed free because .section is a page-width block, and these pages
   have no section elements to hang it on. */
.prose h2::after {
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  margin-left: -50vw;
  width: 100vw;
  height: 1px;
  background: var(--line);
}

/* The first heading opens the page; it is not separating anything from what
   came before, so it gets none of the separation. The margin and the bar have
   to be cleared too -- clearing only the padding and border left the first
   section pushed down with an accent bar floating above nothing. */
/* NOT .page-header. That class marks SECTION headings on these pages -- the
   course page has fourteen of them -- and the page title sits outside it in
   every case, verified across all seventeen. Resetting on .page-header killed
   the separator on precisely the headings that needed one, which is why three
   rounds of making the rule louder changed nothing: it was being switched off
   a hundred lines further down. */
/* .prose-title, an explicit marker written onto the opening heading of each
   page, because neither of the obvious selectors means what it looks like:
   :first-child matches an h2 that is the first child OF ITS PARENT, and every
   section heading here is the only child of its own .page-header wrapper -- so
   `h2:first-child` matched all fourteen and suppressed every separator on the
   page. CSS cannot say "the first h2 in document order", so the markup says it
   instead. */
.prose .prose-title,
.prose .course-hero h2 {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

/* Both pseudo-elements, not just the marker: the rule is drawn by ::after now,
   and clearing only ::before would leave a full-bleed line across the top of
   the page separating the heading from the nav. */
.prose .prose-title::before,
.prose .prose-title::after,
.prose .course-hero h2::before,
.prose .course-hero h2::after { content: none; }

/* Callouts: a box, because the thing they are doing is interrupting. */
.prose .callout {
  max-width: none;
  margin: 1.6em 0;
  padding: 1rem 1.15rem;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
}

.prose .callout > :first-child { margin-top: 0; }
.prose .callout > :last-child { margin-bottom: 0; }

/* A highlight band, the homepage's .recruit construct rebuilt for prose.
   There, a <section class="recruit"> is a full-width block carrying the tint
   with a .shell inside holding the content to 1200px. Prose has no section
   elements to hang that on -- it is one flat run inside a single .shell -- so
   the band bleeds itself back out to the viewport and re-establishes the shell
   width on an inner wrapper. Same left:50%/-50vw/100vw technique .prose h2's
   separator already uses, and main's overflow-x: clip contains it.

   The band's own top and bottom borders ARE its section separators, so the
   heading inside drops the rule and the spacing it would otherwise carry --
   the one construct besides the page title allowed to do that, and only
   because it replaces what it cancels. */
.prose .prose-band {
  position: relative;
  left: 50%;
  width: 100vw;
  margin-left: -50vw;
  margin-top: clamp(3.5rem, 8vw, 6.5rem);
  margin-bottom: clamp(3.5rem, 8vw, 6.5rem);
  padding-block: clamp(2rem, 4.5vw, 3rem);
  background: var(--accent-wash);
  border-top: 1px solid var(--accent-line);
  border-bottom: 1px solid var(--accent-line);
}

.prose .prose-band__inner {
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.prose .prose-band h2 {
  margin-top: 0;
  padding-top: 0;
  margin-bottom: clamp(0.9rem, 2vw, 1.3rem);
}
.prose .prose-band h2::after { content: none; }

/* The heading AFTER a band must not separate itself from it. The band already
   closed the section with its bottom border, so a following h2 that draws its
   own rule and its own space on both sides produces the doubled boundary:
   space, band edge, space, a second hairline floating in the middle of
   nothing, space, heading. The band's margin-bottom is the whole gap. */
.prose .prose-band + h2,
.prose .prose-band + .page-header h2 {
  margin-top: 0;
  padding-top: 0;
}
.prose .prose-band + h2::after,
.prose .prose-band + .page-header h2::after { content: none; }

.prose .prose-band > :last-child,
.prose .prose-band__inner > :last-child { margin-bottom: 0; }

/* Tags carry a type in their class name; give each one a colour so the type is
   visible rather than merely encoded. */
.prose .tag {
  display: inline-block;
  /* Adjacent tags carry no whitespace between them in the markup
     (</span><span class="tag">), so inline-block puts them in contact and
     "Lecture" and "Q&A" read as one run-on chip.
     margin-RIGHT on every tag, not margin-left on `.tag + .tag`: these wrap
     inside the cell, and a left margin also applies to the first tag on a
     wrapped line, indenting it past the tags above it. A right margin leaves
     wrapped lines flush. The cost is a trailing margin on the last tag, which
     is invisible -- Tags is the final column and the space falls inside the
     cell's own padding.
     margin-bottom keeps wrapped rows from touching vertically.
     rem, not em: a tag's font-size is 0.86em of an already-reduced table font,
     so an em gap shrinks to about 4px and reads as no gap. 0.4rem is what
     .tags and .chip-row already use for the same job.
     Both live in this one rule because a duplicated selector is how a later
     definition silently wins over an earlier one -- the long-form block has a
     test against exactly that. */
  margin-right: 0.4rem;
  margin-bottom: 0.3rem;
  padding: 0.12em 0.55em;
  border-radius: 3px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  color: var(--ink-2);
  font-family: var(--font-mono);
  font-size: 0.74rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  white-space: nowrap;
  vertical-align: middle;
}

.prose .tag-deadline { color: #a2401f; background: color-mix(in srgb, #a2401f 10%, transparent); border-color: color-mix(in srgb, #a2401f 28%, transparent); }
.prose .tag-paper    { color: #2b5f8a; background: color-mix(in srgb, #2b5f8a 10%, transparent); border-color: color-mix(in srgb, #2b5f8a 28%, transparent); }
.prose .tag-lecture  { color: #3f6b47; background: color-mix(in srgb, #3f6b47 10%, transparent); border-color: color-mix(in srgb, #3f6b47 28%, transparent); }
.prose .tag-project  { color: #6a4a8f; background: color-mix(in srgb, #6a4a8f 10%, transparent); border-color: color-mix(in srgb, #6a4a8f 28%, transparent); }
.prose .tag-academic { color: #7a5a1f; background: color-mix(in srgb, #7a5a1f 10%, transparent); border-color: color-mix(in srgb, #7a5a1f 28%, transparent); }
.prose .tag-qa       { color: #1f6b6b; background: color-mix(in srgb, #1f6b6b 10%, transparent); border-color: color-mix(in srgb, #1f6b6b 28%, transparent); }
.prose .tag-flex     { color: var(--ink-3); }

.prose .date {
  color: var(--ink-3);
  font-family: var(--font-mono);
  font-size: 0.82rem;
  white-space: nowrap;
}

.prose .notes { color: var(--ink-3); font-size: 0.92rem; }

/* .page-header wraps a SECTION heading on these pages -- fourteen of them on
   the course page -- not the page's own title. An earlier version of this
   block zeroed padding-top, border-top and margin-top on every .page-header
   h2, which deleted the 6.5rem of section spacing from every heading while
   the hairline above it still drew. The result was a rule sitting almost on
   top of its heading: the "everything is tight" that no amount of tuning the
   h2 rule could fix, because the h2 rule was being overridden here.
   The wrapper now contributes nothing; .prose h2 owns the section spacing. */
.prose .page-header { margin: 0; padding: 0; border: 0; }

/* The striped tables these pages already ask for. */
.prose .table-striped tbody tr:nth-child(odd) { background: color-mix(in srgb, var(--ink) 3%, transparent); }

/* The rest of the content's vocabulary. Each of these was carrying meaning
   with nothing rendering it -- the course page's own header, four kinds of
   table it distinguishes between, a guest-speaker tag, a code language hook,
   and an embedded timeline. */
.prose .course-hero { margin-bottom: 1.8em; }
.prose .course-hero h1,

.prose .course-subtitle {
  margin-top: 0.3em;
  color: var(--ink-3);
  font-size: 1.05rem;
}

/* Four table variants, one treatment: the page distinguishes them by purpose,
   not by appearance, and giving each a different look would say otherwise. */
.prose .course-table,
.prose .key-dates-table,
.prose .schedule-table,
.prose .midterm-table { font-size: 0.9rem; }

.prose .table-responsive { max-width: none; overflow-x: auto; }
.prose .table-responsive table { width: 100%; margin-left: 0; }

.prose .tag-guest { color: #6a4a8f; background: color-mix(in srgb, #6a4a8f 10%, transparent); border-color: color-mix(in srgb, #6a4a8f 28%, transparent); }

/* A language hint on a code block. No highlighting engine here, so it reads as
   a label rather than pretending to colour the syntax. */
.prose .language-solidity { font-family: var(--font-mono); }

/* An embedded timeline, which this design cannot style from the inside -- it
   is an iframe. Constrain it and let it be. */
.prose .twitter,
.prose .twitter-timeline { max-width: 100%; margin: 1.2em 0; }
