/* JEDIS Lab — site styles.
   Theme: light theme values at rest, "quantum" palette (accent hue 200 / 255).
   The page body stays a dark night sky; content bands below each hero use
   backdrop-filter:var(--sky-flip) to invert the sky into a light background. */

* { box-sizing: border-box; }

:root {
  --bg: oklch(0.9750 0.0040 255);
  --bg2: oklch(0.9550 0.0060 255);
  --card: oklch(1 0 0);
  --fg: oklch(0.2100 0.0150 255);
  --muted: oklch(0.4600 0.0120 255);
  --line: oklch(0.9000 0.0060 255);
  --accent: oklch(0.56 0.13 200);
  --accent2: oklch(0.56 0.13 255);
  --ambient: 0.42;
  --page: oklch(0.8 0.13 200); /* hue shifted per page by JS */
  --font-display: 'Zilla Slab', Georgia, serif;
  /* Width is a no-op on Zilla Slab, which has no width axis — the vars stay so
     the font lab can drive them for faces that do (Anybody, Archivo, Mona). */
  --w-hero: 100%;
  --w-sec: 100%;
  --font-body: 'Inter', system-ui, sans-serif;
  /* Labels (nav, eyebrows, buttons, dates, badges) and code (the BibTeX block)
     used to share one mono token, but they want different things: labels are
     now set in a plain sans so the chrome stops shouting "technical", while
     code stays genuinely monospaced so BibTeX still aligns. */
  --font-label: 'IBM Plex Sans', system-ui, sans-serif;
  --font-mono: 'IBM Plex Mono', monospace;
  /* Korean headings on the join page — Zilla Slab has no Hangul */
  --font-ko: 'IBM Plex Sans KR', 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
  --band-a: color-mix(in oklch, oklch(0.9750 0.0040 255) 14%, transparent);
  --band-b: color-mix(in oklch, oklch(0.9550 0.0060 255) 20%, transparent);
  --band-c: color-mix(in oklch, color-mix(in oklch, oklch(0.56 0.13 200) 22%, oklch(0.9750 0.0040 255)) 18%, transparent);
  --band-dark: oklch(0.1600 0.0100 255);
  --band-hero: transparent;
  --glass: color-mix(in oklch, var(--card) 20%, transparent); /* lighter frost so the hover aurora reads brighter through it */
  --glass-brd: rgba(255, 255, 255, 0.85);
  --glass-blur: blur(24px) saturate(1.8);
  --card-blur: blur(40px) saturate(1.8); /* heavier than the header — cards diffuse strongly */
  --glass-bg: linear-gradient(135deg, rgba(255,255,255,0.32), transparent 46%),
              radial-gradient(120% 80% at 0% 0%, rgba(255,255,255,0.32), transparent 55%),
              var(--glass);
  --glass-sh: 0 16px 48px -16px rgba(15,23,42,0.16),
              inset 0 1px 0 rgba(255,255,255,0.95),
              inset 0 0 0 1px rgba(255,255,255,0.05),
              inset 0 -14px 30px -20px rgba(15,23,42,0.16);
  --sky-flip: invert(0.94) hue-rotate(180deg) sepia(0.1) saturate(1.1) brightness(1.03) contrast(0.96);
  --pad-x: max(clamp(16px, 4vw, 40px), calc(50vw - 540px));
}

html { overflow-y: scroll; }
html, body { margin: 0; padding: 0; background: var(--band-dark); color: var(--fg); }
body { font-family: var(--font-body); -webkit-font-smoothing: antialiased; }

a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent2); }
::selection { background: var(--accent); color: var(--bg); }
input::placeholder { color: var(--muted); }

@media (pointer: coarse) { .jedis-cursor { display: none !important; } }
@media (pointer: fine) { a, button, input, .clickable { cursor: none !important; } }

/* The opacity rule for entrance animations: an element with an opacity
   keyframe animation becomes a backdrop root in Chromium while the animation
   applies (running OR filling) — glass descendants then can't sample the sky
   behind it. So opacity may animate only on elements that ARE the glass or
   contain none (self-opacity doesn't block an element's own backdrop-filter);
   any wrapper that CONTAINS sky-glass must stay transform-only. This is why
   pageFade on .hero (an ancestor of the prof hero card) was replaced by
   fadeIn on the hero's children. */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes riseUp { from { transform: translateY(26px); } to { transform: translateY(0); } }
@keyframes drift { 0% { transform: translate3d(0,0,0) scale(1); } 100% { transform: translate3d(-4%,3%,0) scale(1.12); } }

main section > * { animation: riseUp .9s cubic-bezier(.22,1,.36,1) both; }
/* heroes fade + rise; the hero container itself must never animate */
main .hero > * { animation: riseUp .9s cubic-bezier(.22,1,.36,1) both, fadeIn .7s ease both; }
main section > *:nth-child(2) { animation-delay: .09s; }
main section > *:nth-child(3) { animation-delay: .18s; }
main section > *:nth-child(n+4) { animation-delay: .27s; }

@media (prefers-reduced-motion: reduce) {
  main section > * , .ambient, .hero { animation: none !important; }
}

/* ---------- fixed backdrop layers ---------- */

.root {
  min-height: 100vh;
  background: linear-gradient(172deg,
      color-mix(in oklch, var(--accent2) 7%, var(--band-dark)),
      var(--band-dark) 34%,
      color-mix(in oklch, var(--accent) 8%, var(--band-dark)) 62%,
      color-mix(in oklch, var(--accent2) 12%, var(--band-dark)) 100%) fixed,
    var(--band-dark);
  color: var(--fg);
  font-family: var(--font-body);
  position: relative;
  overflow-x: clip;
  cursor: none;
}

.ambient {
  position: fixed;
  inset: -12%;
  z-index: 0;
  pointer-events: none;
  opacity: var(--ambient);
  background: radial-gradient(52% 42% at 72% 106%, color-mix(in oklch, var(--accent2) 40%, transparent), transparent 68%),
              radial-gradient(46% 38% at 10% 92%, color-mix(in oklch, var(--accent) 34%, transparent), transparent 68%),
              radial-gradient(70% 55% at 45% 34%, color-mix(in oklch, var(--accent) 12%, transparent), transparent 72%);
  animation: drift 26s ease-in-out infinite alternate;
}
/* while the sky loop is parked (html.sky-still, set by sky.js's idle gate),
   still the aurora too — its full-screen drift otherwise keeps the
   backdrop-filter stack re-compositing every frame for nobody */
html.sky-still .ambient { animation-play-state: paused; }

#sky {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  pointer-events: none;
  z-index: 0;
}

.grid-overlay {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  color: var(--line);
  opacity: 0.24;
  -webkit-mask-image: radial-gradient(circle at 50% 25%, black, transparent 78%);
  mask-image: radial-gradient(circle at 50% 25%, black, transparent 78%);
}
.grid-overlay svg { position: absolute; inset: 0; }

/* ---------- custom cursor ---------- */

/* The cursor draws above the sky-flip, so it is never inverted along with the
   content beneath it — a white core and a mid-tone accent that read on the
   night sky vanish over the light bands. Every cursor colour therefore goes
   through these two, and app.js adds .cur-lit while the pointer is inside an
   .invert-wrap zone. The elements transition their own colours, since custom
   properties can't be transitioned directly. */
:root { --cur-ink: #fff; --cur-acc: var(--accent); }
html.cur-lit { --cur-ink: oklch(0.19 0.02 255); --cur-acc: oklch(0.44 0.15 200); }
.jedis-cursor, .jedis-cursor * {
  transition: background-color .22s ease, border-color .22s ease,
              box-shadow .22s ease, color .22s ease, fill .22s ease, stroke .22s ease;
}

/* Topmost layer by design: the dev panel stack lives at z-index ~2147483400
   (devtools.css) and the global `cursor: none` stays active over its buttons,
   so anything lower leaves no visible cursor there. */
.cursor-ring {
  position: fixed; top: 0; left: 0;
  width: 34px; height: 34px;
  border: 1.5px solid var(--cur-acc);
  border-radius: 50%;
  pointer-events: none;
  z-index: 2147483646;
  opacity: 0;
  transition: opacity .3s ease, background .2s ease;
  will-change: transform;
}
.cursor-dot {
  position: fixed; top: 0; left: 0;
  width: 6px; height: 6px;
  background: var(--cur-acc);
  border-radius: 50%;
  pointer-events: none;
  z-index: 2147483646;
  opacity: 0;
  transition: opacity .3s ease;
  will-change: transform;
}

/* ---------- cursor variants (?cursor) ----------
   Four alternates to the dot+ring above, each borrowed from a different part
   of the site's own language: the sky (nova), constellations, lab instruments
   (reticle), and the quantized zone's halftone grid (pix). app.js owns all
   geometry — spike length, node positions, dot radius — because it changes
   every frame; this file owns only material: colour, taper, weight, glow. */
.cur-layer {
  position: fixed; inset: 0;
  pointer-events: none;
  z-index: 2147483646;
  opacity: 0;
  transition: opacity .3s ease;
}

/* nova — a star at the pointer: white-hot core, a 4-point diffraction glint
   that rotates into the direction of travel and stretches with speed, and a
   bloom that swells over anything clickable. */
.cur-nova { position: fixed; top: 0; left: 0; width: 0; height: 0; will-change: transform; }
.cur-nova i { position: absolute; display: block; }
.cn-bloom {
  width: 56px; height: 56px; margin: -28px 0 0 -28px;
  border-radius: 50%;
  background: radial-gradient(circle, color-mix(in oklch, var(--cur-acc) 55%, transparent), transparent 66%);
}
.cn-sp {
  height: 1.5px; margin-top: -0.75px;
  background: linear-gradient(90deg, transparent, var(--cur-acc) 40%, var(--cur-ink) 50%, var(--cur-acc) 60%, transparent);
}
.cn-core {
  width: 5px; height: 5px; margin: -2.5px 0 0 -2.5px;
  border-radius: 50%; background: var(--cur-ink);
  box-shadow: 0 0 7px color-mix(in oklch, var(--cur-acc) 85%, transparent);
}

/* constellation — four nodes trailing on a rope, linked by hairlines. Over an
   interactive element they leave the rope and take its four corners, so the
   figure closes around whatever you're about to click. */
.cur-const { position: fixed; inset: 0; width: 100%; height: 100%; overflow: visible; }
.cur-const polyline { fill: none; stroke: var(--cur-acc); stroke-width: 1; stroke-linejoin: round; }
.cur-const circle { fill: var(--cur-acc); }
.cur-const .cc-head { fill: var(--cur-ink); filter: drop-shadow(0 0 4px var(--cur-acc)); }

/* reticle — corner brackets and a mono readout. At rest a small square around
   the pointer reading its coordinates; over a target the brackets fly out to
   frame it and the readout switches to the element's size. */
.cur-ret, .cur-cal, .cur-ch, .cur-lock, .cur-grid { position: fixed; inset: 0; }
.cur-ret i, .cur-ret b,
.cur-cal i, .cur-cal b,
.cur-ch i, .cur-ch b,
.cur-lock i, .cur-lock b,
.cur-grid i, .cur-grid b {
  position: fixed; top: 0; left: 0; display: block; will-change: transform;
}
.cr-c { width: 9px; height: 9px; border: 1.5px solid var(--cur-acc); }
.cr-c--tl { border-right: 0; border-bottom: 0; }
.cr-c--tr { border-left: 0; border-bottom: 0; }
.cr-c--bl { border-right: 0; border-top: 0; }
.cr-c--br { border-left: 0; border-top: 0; }
.cr-dot { width: 3px; height: 3px; border-radius: 50%; background: var(--cur-acc); }
.cr-out {
  font: 500 10px/1 var(--font-mono);
  letter-spacing: 0.1em;
  color: var(--cur-acc);
  opacity: 0.62;
  white-space: nowrap;
}

/* pix — the pointer quantized onto the halftone lattice: dot radius falls off
   from the centre cell and the whole cluster snaps to the cell pitch, so it
   steps rather than glides. */
.cur-pix { position: fixed; inset: 0; }
.cur-pix i {
  position: fixed; top: 0; left: 0; display: block;
  border-radius: 50%;
  background: var(--cur-acc);
  will-change: transform;
}

/* ---------- the reticle family ----------
   Five further readings of the same idea: a cursor that measures what it is
   pointing at, in the visual language of a different instrument each time.
   They share the mono readout above (.cr-out) and the same eased-frame
   scaffolding in app.js. */

/* caliper — an engineering dimension annotation. Extension lines drop from
   the target's bottom corners to a dimension line with outward arrowheads,
   labelled with the measured width. */
.cl-ext { width: 1px; background: color-mix(in oklch, var(--cur-acc) 72%, transparent); }
.cl-dim { height: 1px; background: var(--cur-acc); }
.cl-ar { width: 0; height: 0; border: 3.5px solid transparent; }
.cl-ar--l { border-right-color: var(--cur-acc); }
.cl-ar--r { border-left-color: var(--cur-acc); }

/* crosshair — hairlines across the whole viewport, with the coordinate read
   off at the top and left edges. Over a target the pair splits and settles on
   its four edges, so the cursor becomes an alignment guide. */
.ch-v { width: 1px; height: 100vh; background: var(--cur-acc); }
.ch-h { height: 1px; width: 100vw; background: var(--cur-acc); }
.ch-lab {
  font: 500 9.5px/1 var(--font-mono);
  letter-spacing: 0.12em;
  color: var(--cur-acc);
  opacity: 0.7;
  white-space: nowrap;
}

/* scope — an optical reticle: four graduated arms, a ranging ring that
   tightens on lock, and a flare that expands out of it as the lock takes. */
.cur-scope { position: fixed; inset: 0; }
.cur-scope b { position: fixed; top: 0; left: 0; display: block; will-change: transform; }
.sc-core { position: fixed; top: 0; left: 0; width: 0; height: 0; will-change: transform; }
.sc-core i { position: absolute; display: block; }
.sc-hub { width: 0; height: 0; }
.sc-arm {
  width: 21px; height: 1.5px; margin-top: -0.75px; margin-left: 9px;
  transform-origin: -9px 50%;
  background: repeating-linear-gradient(90deg, var(--cur-acc) 0 1.5px, transparent 1.5px 6px);
  opacity: 0.85;
}
.sc-arm--e { transform: rotate(0deg); }
.sc-arm--s { transform: rotate(90deg); }
.sc-arm--w { transform: rotate(180deg); }
.sc-arm--n { transform: rotate(270deg); }
.sc-ring {
  width: 30px; height: 30px; margin: -15px 0 0 -15px;
  border: 1px solid var(--cur-acc); border-radius: 50%;
}
.sc-flare {
  width: 30px; height: 30px; margin: -15px 0 0 -15px;
  border: 1px solid var(--cur-acc); border-radius: 50%;
}
.sc-dot { width: 3px; height: 3px; margin: -1.5px 0 0 -1.5px; border-radius: 50%; background: var(--cur-ink); }

/* lock — target acquisition. The brackets idle at 45° and rotate square as
   they take the corners, a faint wash fills the target, and the readout names
   what was acquired rather than only measuring it. */
.lk-c { width: 11px; height: 11px; border: 1.5px solid var(--cur-acc); }
.lk-c--tl { border-right: 0; border-bottom: 0; }
.lk-c--tr { border-left: 0; border-bottom: 0; }
.lk-c--bl { border-right: 0; border-top: 0; }
.lk-c--br { border-left: 0; border-top: 0; }
.lk-fill {
  border-radius: 7px;
  background: color-mix(in oklch, var(--cur-acc) 12%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--cur-acc) 22%, transparent);
}
.lk-dot { width: 3px; height: 3px; border-radius: 50%; background: var(--cur-acc); }

/* grid — the reticle resolved onto a lattice: a patch of the lattice lights up
   under the pointer, the brackets snap to it (so they step between cells), and
   the readout counts in cells instead of pixels. */
.cur-grid .gd-hub { position: fixed; top: 0; left: 0; width: 0; height: 0; will-change: transform; }
.cur-grid .gd-hub i { position: absolute; width: 1.5px; height: 1.5px; border-radius: 50%; background: var(--cur-acc); }
.gd-c { width: 8px; height: 8px; border: 1.5px solid var(--cur-acc); }
.gd-c--tl { border-right: 0; border-bottom: 0; }
.gd-c--tr { border-left: 0; border-bottom: 0; }
.gd-c--bl { border-right: 0; border-top: 0; }
.gd-c--br { border-left: 0; border-top: 0; }

/* ---------- hybrids: constellation × reticle × grid ----------
   The same three ingredients in different proportions — a rope of stars
   trailing the pointer, four corner brackets that take the target, and a
   lattice the whole thing can snap to. What varies between them is how much
   of the motion is continuous and how much is quantized.

   The centre mark is deliberately heavier here than in the earlier variants:
   a white core inside an accent halo, so the pointer is never the thing you
   have to hunt for on a dark sky. */
.cur-mix { position: fixed; inset: 0; }
.cur-mix i, .cur-mix b { position: fixed; top: 0; left: 0; display: block; will-change: transform; }
.cur-mix svg { position: fixed; inset: 0; width: 100%; height: 100%; overflow: visible; }
.cur-mix svg polyline { fill: none; stroke: var(--cur-acc); stroke-width: 1; stroke-linejoin: round; }
.cur-mix svg circle { fill: var(--cur-acc); }
.cur-mix .cx-hub { position: fixed; top: 0; left: 0; width: 0; height: 0; will-change: transform; }
.cur-mix .cx-hub i {
  position: absolute;
  width: 1.5px; height: 1.5px;
  border-radius: 50%;
  background: var(--cur-acc);
}
.cx-core {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--cur-ink);
  box-shadow: 0 0 10px color-mix(in oklch, var(--cur-acc) 80%, transparent);
}
.cx-halo { width: 15px; height: 15px; border-radius: 50%; border: 1px solid var(--cur-acc); }
.cx-c { width: 10px; height: 10px; border: 1.5px solid var(--cur-acc); }
.cx-c--tl { border-right: 0; border-bottom: 0; }
.cx-c--tr { border-left: 0; border-bottom: 0; }
.cx-c--bl { border-right: 0; border-top: 0; }
.cx-c--br { border-left: 0; border-top: 0; }

/* The centre mark's lock gestures (?node — see makeNode in app.js). Each is
   one extra element that lives at scale 0-ish at rest and is driven entirely
   by transform + opacity from the loop, so nothing here transitions on its
   own. Sizes are the rest size; the loop does the growing. */
.cx-ring {                                   /* hollow: the core's aperture */
  width: 11px; height: 11px;
  border-radius: 50%;
  border: 1.5px solid var(--cur-ink);
  box-shadow: 0 0 10px color-mix(in oklch, var(--cur-acc) 70%, transparent);
}
.cx-spark {                                  /* star: four spikes off the dot */
  width: 22px; height: 22px;
  background: var(--cur-ink);
  filter: drop-shadow(0 0 6px color-mix(in oklch, var(--cur-acc) 75%, transparent));
  clip-path: polygon(50% 0%, 57% 43%, 100% 50%, 57% 57%, 50% 100%, 43% 57%, 0% 50%, 43% 43%);
}
.cx-pip {                                    /* pip: hairline crosshair */
  width: 18px; height: 18px;
  background:
    linear-gradient(to right, var(--cur-acc) 0 5px, transparent 5px calc(100% - 5px), var(--cur-acc) calc(100% - 5px) 100%) 50% 50% / 100% 1px no-repeat,
    linear-gradient(to bottom, var(--cur-acc) 0 5px, transparent 5px calc(100% - 5px), var(--cur-acc) calc(100% - 5px) 100%) 50% 50% / 1px 100% no-repeat;
}

/* switcher: fifteen names don't fit the two-per-row tune-foot rhythm */
.cur-pick { display: flex; flex-wrap: wrap; gap: 6px; }
.cur-pick .tune-btn { flex: 0 0 auto; padding: 5px 9px; }
.cur-grp {
  margin: 11px 0 6px;
  font-size: 9.5px; letter-spacing: 0.14em; text-transform: uppercase;
  opacity: 0.45;
}
.cur-grp:first-child { margin-top: 0; }
.cur-note { margin-top: 9px; opacity: 0.5; letter-spacing: 0.04em; }

.content { position: relative; z-index: 1; }

/* ---------- glass over the night sky ----------
   .glass is built for the light inverted bands — over the starfield its frost
   washes the sky pale and swallows the type. Anything glassy that sits on the
   dark sky uses this instead: the header bar's material, factored out so the
   two can't drift. The header's JS scroll blend overrides these inline as it
   goes light, so this block is only the at-rest (over-dark) state. */
.glass-sky {
  --fg: #ffffff;
  --muted: oklch(0.8 0.014 255);
  --line: rgba(255, 255, 255, 0.18);
  --glass-brd: rgba(255, 255, 255, 0.16);
  border-radius: 20px;
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  /* No background, matching what the header bar actually renders. Its scroll
     blend writes none (the old inline value was invalid at computed-value time,
     so the intended 62% --band-dark tint never applied). Adding one here would
     make these surfaces darker than the bar. Verify with scripts/glass-parity. */
  border: 1px solid rgba(255, 255, 255, 0.16);
  box-shadow: 0 16px 48px -18px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.16);
}

/* ---------- header ---------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 30;
  padding: 14px clamp(10px, 3vw, 28px) 0;
  /* flow height locked to the at-rest slot (measured by setHdrSlot): the bar
     condensing on scroll must not change document layout — with mandatory
     scroll-snap, a mid-scroll layout shift moves every snap position and the
     browser re-snaps against the gesture (feels like bouncing). The bar
     shrinks *inside* this fixed slot instead. */
  height: var(--hdr-slot, auto);
  /* the locked slot can extend below the condensed bar — don't let that
     invisible strip eat clicks meant for content beneath it */
  pointer-events: none;
}
.site-header .header-bar { pointer-events: auto; }
/* material comes from .glass-sky (in the markup); JS blends it light on scroll */
.header-bar {
  max-width: 1200px;
  margin: 0 auto;
  /* --hdr-t (0..1, set by the same JS scroll blend as the colours) condenses the
     bar as it goes light: padding, mark, wordmark and subtitle all ride one t */
  padding: calc(15px - 5px * var(--hdr-t, 0)) clamp(18px, 2.8vw, 28px);
  display: flex;
  align-items: center;
  gap: 22px;
  flex-wrap: wrap;
}

.brand { display: flex; align-items: center; gap: 14px; cursor: pointer; color: var(--fg); }
.brand:hover { color: var(--fg); }
.brand-mark {
  position: relative;
  width: calc(40px - 10px * var(--hdr-t, 0));
  height: calc(40px - 10px * var(--hdr-t, 0));
  flex: none;
}
/* v3 logo is a white glyph on transparent — same invert blend as the wordmark */
.brand-mark img { display: block; width: 100%; height: 100%; filter: invert(var(--wm-flip, 0)); }
.brand-mark--sm { width: 30px; height: 30px; }
/* white wordmark PNG; --wm-flip (0..1, set by the JS header blend) inverts it
   toward dark when the header goes light. An <img>+filter (not a CSS mask)
   on purpose: mask-image is fetched CORS-mode and silently fails on file:// */
/* wordmark + subtitle stack as one column beside the mark; both images share
   the same white-PNG + invert(--wm-flip) blend so they flip together on scroll */
.brand-lockup { display: flex; flex-direction: column; align-items: flex-start; }
.brand-word {
  display: block;
  /* 140px → 120px keeps the footer's wordmark:mark ratio once the mark hits 30px */
  width: calc(140px - 20px * var(--hdr-t, 0));
  height: auto;
  /* glyphs live in the PNG's bottom 69% (star headroom above); with the subtitle
     collapsed nothing balances that headroom, so scrub in the same -12% optical
     nudge the footer wordmark uses */
  transform: translateY(calc(-12% * var(--hdr-t, 0)));
  filter: invert(var(--wm-flip, 0));
}
/* full lab name, aligned to the wordmark's width; the PNG's own star headroom
   supplies the optical gap, so the subtitle tucks right under the glyphs */
.brand-sub {
  display: block;
  width: 140px;
  /* 13.2px is the PNG's natural height at 140px wide (690×65); collapsing the
     height (not display) lets the subtitle scrub away with scroll. Opacity is
     fully out by t≈0.54 so the brief vertical squash is never visible. */
  height: calc(13.2px * (1 - var(--hdr-t, 0)));
  margin-top: calc(7px * (1 - var(--hdr-t, 0)));
  opacity: clamp(0, 0.92 - 1.7 * var(--hdr-t, 0), 0.92);
  filter: invert(var(--wm-flip, 0));
  /* subtitle art is centered, but the wordmark's letters sit left-of-center
     (the star flourish pads the right), so nudge left to align optical centers */
  transform: translateX(-5px);
}
/* footer wordmark is a single row beside the mark (no subtitle); it keeps the
   original optical-centering nudge the header lockup no longer needs, and its
   height matches the header's wordmark:mark ratio (0.90 × 30px mark = 27px) */
.brand-word--sm { width: auto; height: 35px; transform: translateY(-12%); }
.accent { color: var(--accent); }

.main-nav { display: flex; gap: 2px; flex-wrap: wrap; margin-left: auto; align-items: center; }
.nav-link {
  position: relative;
  isolation: isolate;
  display: inline-grid;
  justify-items: center;
  font-family: var(--font-label);
  font-size: 13px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 10px 14px;
  border-radius: 9px;
  cursor: pointer;
  transition: color .25s ease;
  font-weight: 400;
  color: var(--muted);
  background: transparent;
}
/* hover pill scales in behind the label (transform+opacity only — compositor-smooth,
   and immune to the JS scroll blend restyling --fg/--muted on the header) */
.nav-link::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: color-mix(in oklch, var(--fg) 9%, transparent);
  opacity: 0;
  transform: scale(0.6);
  transition: opacity .2s ease, transform .34s cubic-bezier(.22, 1, .36, 1);
}
.nav-link:hover { color: var(--fg); }
.nav-link:hover::before { opacity: 1; transform: scale(1); }
.nav-link:active::before { transform: scale(0.94); transition-duration: .1s, .1s; }
.nav-link.active { font-weight: 600; color: var(--fg); background: color-mix(in oklch, var(--accent) 16%, transparent); }
.nav-link.active::before { display: none; }
.nav-label { grid-area: 1 / 1; transition: transform .34s cubic-bezier(.22, 1, .36, 1); }
.nav-link:hover .nav-label { transform: translateY(-1px); }
/* ---------- hero sections (transparent over the night sky, dark-local vars) ---------- */

.hero {
  /* No animation on the hero container, ever. Any opacity animation here makes
     the hero a backdrop root in Chromium for as long as it applies, so glass
     inside (the prof hero card) can't frost the sky behind it — unfilled, that
     still meant no frost during the whole arrival. The children fade + rise
     instead (see the entrance-animation note at the keyframes). */
  background: var(--band-hero);
  --fg: #ffffff;
  --muted: oklch(0.82 0.014 255);
  --line: oklch(0.4 0.012 255);
  --bg: oklch(0.17 0.01 255);
  --bg2: oklch(0.21 0.014 255);
  --card: oklch(0.24 0.014 255);
  --glass-brd: rgba(255, 255, 255, 0.16);
  color: var(--fg);
  padding: clamp(64px, 12vh, 144px) var(--pad-x) clamp(48px, 7vh, 76px);
}
.hero--home { overflow: clip; padding: clamp(72px, 15vh, 172px) var(--pad-x) clamp(56px, 8vh, 88px); }
.hero--prof { overflow: clip; padding-bottom: clamp(56px, 8vh, 88px); }
.hero--blog { overflow: clip; }
/* news hero runs shallow so the featured row lands above the fold */
.hero--news { padding: clamp(48px, 8vh, 92px) var(--pad-x) clamp(24px, 3.5vh, 40px); }
.hero--news .page-title--sub { margin-bottom: 12px; }
.hero--news .seg { margin-top: 18px; }

.eyebrow {
  font-family: var(--font-label);
  font-size: 12px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--page);
  margin-bottom: 22px;
}
.eyebrow--tight { margin-bottom: 18px; }

.hero-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-stretch: var(--w-hero);
  font-size: clamp(42px, 8.5vw, 108px);
  line-height: 0.98;
  letter-spacing: -0.005em;
  margin: 0;
  text-wrap: balance;
}
.hero-sub {
  max-width: 640px;
  margin: 28px 0 0;
  font-size: clamp(16px, 2vw, 20px);
  line-height: 1.6;
  color: var(--muted);
  text-wrap: pretty;
}
.hero--home .hero-sub { max-width: 760px; }
.em-fg { color: var(--fg); font-style: normal; }
.hero-cta { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 34px; }

.page-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-stretch: var(--w-hero);
  font-size: clamp(30px, 5vw, 56px);
  letter-spacing: -0.004em;
  margin: 0;
}
.page-title--sub { margin-bottom: 20px; }
.page-sub { max-width: 620px; margin: 0; font-size: 16px; line-height: 1.6; color: var(--muted); }

/* ---------- inverted-sky content wrapper + section bands ---------- */

/* The sky inversion lives on a ::before layer painted *behind* the content
   rather than as a backdrop-filter on the wrapper itself: an ancestor
   backdrop-filter would become the backdrop root and stop the glass cards
   inside from blurring the sky/content behind them. */
.invert-wrap { position: relative; }

.invert-wrap::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  backdrop-filter: var(--sky-flip);
  -webkit-backdrop-filter: var(--sky-flip);
}

.band-a { background: var(--band-a); }
.band-b { background: var(--band-b); }
.band-c { background: var(--band-c); }

/* ---------- section scroll-snap ----------
   Proximity snap on the root: the page scrolls freely and only glides into
   alignment when a gesture ends near an opted-in section — long lists (news,
   publications, blog) have no .snap targets and never trap. .snap-fill turns
   a section into a full-viewport "chapter" (svh so mobile URL bars don't
   jump); flex-centering keeps its content composed at any screen height.
   ?snap=off (html.no-snap, set in app.js) disables the whole system. */
/* Mandatory snap: every section is a snap area, so a gesture can only rest
   at a section top (or anywhere inside a section taller than the viewport —
   the CSS spec allows free scroll while a snap area covers the snapport,
   which is what keeps the long news/publication/blog lists scrollable).
   The footer's end-alignment makes the page bottom a valid rest too.
   Gated behind .snap-ready (added by app.js after the first render): before
   the SPA fills <main>, the page is just header + footer, and mandatory snap
   would latch onto the footer's end-alignment and ride it to the bottom as
   the content grows. */
/* Scroll-feel candidates (compare panel bottom-left, ?feel=free|assist|inertia|deck):
   free    — Apple school: pure native scroll, no snapping, every rest shows content
   assist  — hybrid engine: responsive lerped wheel scroll (lighter than
             inertia); as the gesture goes quiet the same engine's target
             drifts onto the nearest section top, so there is no pause and
             then a slide — the coast just lands  [default]
   inertia — Lenis school: wheel drives a lerped virtual scroll with soft
             magnetic gravity toward section tops
   deck    — CSS mandatory snap + slide-wheel advance (the round-4 build)
   Only deck uses CSS snap; the others settle in JS or not at all. */
html.snap-ready[data-feel="deck"] { scroll-snap-type: y mandatory; }
/* ?snap=off / people-page opt-out beat every feel */
html.no-snap, html.snap-none { scroll-snap-type: none !important; }
/* outside deck, content bands revert to natural height so any free rest
   shows real content (no seas of padding); heroes + lead slides keep their
   viewport fill in every feel — they're composed views, not constraints */
html:not([data-feel="deck"]) .snap-fill:not(.hero) {
  min-height: 0;
  display: block;
}
.snap { scroll-snap-align: start; }
/* snapped bands sit flush under the sticky header (the inverted zone starts
   at the top of the screen) — push their CONTENT below the glass bar so
   section titles aren't hidden behind it. Keyed to .snap on purpose: only a
   band the page can COME TO REST on needs clearing, and a band you merely
   scroll past keeps the plain .section padding. */
.invert-wrap > .snap { padding-top: max(clamp(48px, 7vh, 82px), calc(var(--hdr-slot, 0px) + 16px)); }
/* Heroes alone offset by --hdr-slot (the sticky header's in-flow height, set
   by app.js on load/resize): their snap position becomes the true page top,
   so the page rests at 0 instead of re-snapping past the header on load.
   Content bands snap flush to the viewport top — the inverted zone starts
   exactly at the top of the screen, with the glass header floating over it. */
.hero.snap { scroll-margin-top: var(--hdr-slot, 0px); }
.snap-fill {
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* hero chapter = header slot + hero = one viewport, so the next section
   (the inverted zone) begins exactly at the fold */
.hero.snap-fill {
  min-height: calc(100vh - var(--hdr-slot, 0px));
  min-height: calc(100svh - var(--hdr-slot, 0px));
}
/* lead slide (publications, news): hero + featured block share one
   viewport-filling chapter whose snap position is the page top; the hero
   flexes to centre its title in whatever space the featured block leaves */
.snap-lead {
  scroll-margin-top: var(--hdr-slot, 0px);
  min-height: calc(100vh - var(--hdr-slot, 0px));
  min-height: calc(100svh - var(--hdr-slot, 0px));
  display: flex;
  flex-direction: column;
}
.snap-lead .hero {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* News/gallery share one hero (title + seg toggle) that must sit in the
   identical spot in both modes, so the toggle you click never jumps. Gallery
   has no featured block to centre against, so its hero is top-anchored at its
   natural height; --flush pins the news hero the same way and hands the
   leftover viewport of the lead slide to the featured block instead. */
.snap-lead--flush .hero { flex: 0 0 auto; display: block; }
.snap-lead--flush > .invert-wrap {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.site-footer { scroll-snap-align: end; }

.section { padding: clamp(48px, 7vh, 82px) var(--pad-x); }
.section--sm { padding: clamp(44px, 6vh, 76px) var(--pad-x); }
.section--cta { padding: clamp(44px, 6vh, 72px) var(--pad-x) clamp(64px, 10vh, 108px); }
.pb-lg { padding-bottom: clamp(64px, 10vh, 108px); }
.pb-xl { padding-bottom: clamp(56px, 9vh, 96px); }
.pb-sm { padding-bottom: clamp(32px, 5vh, 48px); }
.pb-xs { padding-bottom: clamp(20px, 3vh, 32px); }
.pt-sm { padding-top: clamp(26px, 4vh, 40px); }
.pt-xs { padding-top: clamp(16px, 2.5vh, 26px); }

.sec-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 26px;
}
.sec-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-stretch: var(--w-sec);
  font-size: clamp(24px, 3.4vw, 38px);
  letter-spacing: -0.01em;
  margin: 0;
}
.sec-title--mb { margin-bottom: 26px; }
.link-mono {
  font-family: var(--font-label);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: pointer;
  color: var(--fg);
}
.link-mono:hover { color: var(--accent); }
button.link-mono { background: none; border: 0; padding: 0; }
/* a sec-head can carry two mono links (e.g. list toggle + all-updates) */
.sec-head-actions { display: flex; gap: 22px; align-items: baseline; }

.mono-label {
  font-family: var(--font-label);
  font-size: 12px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 20px;
}
.mono-label--sm { letter-spacing: 0.14em; margin-bottom: 14px; }
/* section head that names the axis its cards are ordered along */
.sec-axis { display: flex; align-items: baseline; justify-content: space-between; gap: 16px 24px; flex-wrap: wrap; margin-bottom: 22px; }
.sec-axis > .mono-label, .sec-axis > .sec-title { margin-bottom: 0; }
.axis-note { font-family: var(--font-label); font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); white-space: nowrap; }
.mono-label--muted { letter-spacing: 0.16em; color: var(--muted); margin-bottom: 18px; }

/* ---------- glass cards & shared bits ---------- */

.glass {
  position: relative; /* hosts the hover-treatment overlays */
  color: var(--fg);
  background: var(--glass-bg);
  backdrop-filter: var(--card-blur);
  -webkit-backdrop-filter: var(--card-blur);
  border: 1px solid var(--glass-brd);
  box-shadow: var(--glass-sh);
  border-radius: 16px;
}

/* ---------- card hover treatment: nebula bloom + halo through glass ---------- */

/* On hover the card lifts and lights up from behind the frost (z-index:-1,
   so the glow reads as light inside the glass and text always paints over
   it). The light is five soft pools in three cool hues — two blooms rising
   from the bottom edge, a ribbon sliding along the top, and a faint pool on
   each side. Which hue lands on which edge is a per-card shuffle (--eh1..3,
   dealt by js/hover.js). Three kinds of motion, all slow and non-repeating:
     • drift — each pool slides along its edge on its own coprime timer;
     • lean  — the blooms and ribbon carry a WEAK share of --cbias (the
       smoothed cursor offset fed by app.js): felt, not noticed;
     • pulse — each pool's brightness swells and sinks independently
       (--bi1..5), kept honest by a soft-knee compressor so aligned pulses
       can never make the whole card too bright. */

@property --mx1 { syntax: '<percentage>'; inherits: false; initial-value: 25%; }
@property --mx2 { syntax: '<percentage>'; inherits: false; initial-value: 52%; }
@property --mx4 { syntax: '<percentage>'; inherits: false; initial-value: 50%; }
@property --my1 { syntax: '<percentage>'; inherits: false; initial-value: 40%; }
@property --my2 { syntax: '<percentage>'; inherits: false; initial-value: 60%; }
@property --bi1 { syntax: '<number>'; inherits: false; initial-value: 1; }
@property --bi2 { syntax: '<number>'; inherits: false; initial-value: 1; }
@property --bi3 { syntax: '<number>'; inherits: false; initial-value: 1; }
@property --bi4 { syntax: '<number>'; inherits: false; initial-value: 1; }
@property --bi5 { syntax: '<number>'; inherits: false; initial-value: 1; }

/* matched shadow structure at rest and on hover (extra slots transparent here)
   so the box-shadow transition interpolates cleanly — no first-hover pop */
.glass {
  transition: transform .5s cubic-bezier(.22, 1, .36, 1),
              box-shadow .5s cubic-bezier(.22, 1, .36, 1);
  box-shadow:
    0 30px 62px -24px transparent,
    inset 0 0 0 1px rgba(255,255,255,0.05),
    inset 0 20px 44px -30px transparent,
    var(--glass-sh);
}

/* the --hvp-* vars are the treatment's tuning constants (settled through the
   hover-lab rounds; defaults inline so no JS is needed):
     --hvp-int   glow intensity ×      --hvp-size  bloom size ×
     --hvp-edge  ribbon thickness      --hvp-lean  cursor-lean ×
     --hvp-speed motion speed ×        --hvp-breathe pulse amplitude
     --hvp-ceil  net-glow ceiling ×    --hvp-fade  fade-in duration */
.glass::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--hvp-fade, .8s) ease;
  /* soft-knee compressor: --tr is the pulses' weighted raw total (nominal
     78 when all --bi = 1). Excess above nominal is squashed through
     x·H/(x+H) — linear at first, flattening toward the headroom H — and
     --gain rescales every pool by compressed/raw. min() in the numerator
     lets dips below nominal pass through untouched. */
  --tr: calc(18 * max(0, var(--bi1, 1)) + 16 * max(0, var(--bi2, 1))
           + 18 * max(0, var(--bi3, 1)) + 13 * max(0, var(--bi4, 1))
           + 13 * max(0, var(--bi5, 1)));
  --texc: max(0, var(--tr) - 78);
  --thead: max(0.01, calc(78 * (var(--hvp-ceil, 1.25) - 1)));
  --gain: calc((min(78, var(--tr)) + var(--texc) * var(--thead) / (var(--texc) + var(--thead)))
               / max(1, var(--tr)));
  background:
    radial-gradient(calc(95% * var(--hvp-size, 1)) calc(80% * var(--hvp-size, 1))
        at calc(var(--mx1) + var(--cbias, 0%) * 2 * var(--hvp-lean, 1)) 112%,
        color-mix(in oklch, oklch(0.62 0.12 var(--eh1, 200)) calc(18% * var(--hvp-int, 1) * max(0, var(--bi1, 1)) * var(--gain, 1)), transparent), transparent 70%),
    radial-gradient(calc(90% * var(--hvp-size, 1)) calc(88% * var(--hvp-size, 1))
        at calc(var(--mx2) + var(--cbias, 0%) * 2.5 * var(--hvp-lean, 1)) 115%,
        color-mix(in oklch, oklch(0.63 0.12 var(--eh2, 265)) calc(16% * var(--hvp-int, 1) * max(0, var(--bi2, 1)) * var(--gain, 1)), transparent), transparent 70%),
    radial-gradient(120% var(--hvp-edge, 34px)
        at calc(var(--mx4) + var(--cbias, 0%) * 1.5 * var(--hvp-lean, 1)) -8px,
        color-mix(in oklch, oklch(0.63 0.115 var(--eh3, 315)) calc(18% * var(--hvp-int, 1) * max(0, var(--bi3, 1)) * var(--gain, 1)), transparent), transparent 74%),
    radial-gradient(var(--hvp-edge, 34px) 110% at -8px var(--my1),
        color-mix(in oklch, oklch(0.63 0.12 var(--eh2, 265)) calc(13% * var(--hvp-int, 1) * max(0, var(--bi4, 1)) * var(--gain, 1)), transparent), transparent 74%),
    radial-gradient(var(--hvp-edge, 34px) 110% at calc(100% + 8px) var(--my2),
        color-mix(in oklch, oklch(0.62 0.12 var(--eh1, 200)) calc(13% * var(--hvp-int, 1) * max(0, var(--bi5, 1)) * var(--gain, 1)), transparent), transparent 74%);
  animation:
    hvmX1 calc(13s / var(--hvp-speed, 1)) ease-in-out infinite alternate,
    hvmX2 calc(17s / var(--hvp-speed, 1)) ease-in-out infinite alternate,
    hvpX4 calc(16s / var(--hvp-speed, 1)) ease-in-out infinite alternate,
    hvoY1 calc(15s / var(--hvp-speed, 1)) ease-in-out infinite alternate,
    hvoY2 calc(19s / var(--hvp-speed, 1)) ease-in-out infinite alternate,
    hvB1 calc(5.3s / var(--hvp-speed, 1)) ease-in-out infinite alternate,
    hvB2 calc(7.1s / var(--hvp-speed, 1)) ease-in-out infinite alternate,
    hvB3 calc(8.7s / var(--hvp-speed, 1)) ease-in-out infinite alternate,
    hvB4 calc(6.1s / var(--hvp-speed, 1)) ease-in-out infinite alternate,
    hvB5 calc(9.7s / var(--hvp-speed, 1)) ease-in-out infinite alternate;
  animation-delay: var(--ad, 0s);
  animation-play-state: paused;
}
/* --aur (set per card by JS) dials the whole layer down on wide/short cards
   like publication rows; narrow-tall cards keep --aur = 1 */
.glass:hover::after { opacity: var(--aur, 1); animation-play-state: running; }

/* pool drift along the edges — coprime timers, alternate, never in step */
@keyframes hvmX1 { from { --mx1: 16%; } to { --mx1: 35%; } }
@keyframes hvmX2 { from { --mx2: 61%; } to { --mx2: 43%; } }
@keyframes hvpX4 { from { --mx4: 25%; } to { --mx4: 75%; } }
@keyframes hvoY1 { from { --my1: 22%; } to { --my1: 74%; } }
@keyframes hvoY2 { from { --my2: 78%; } to { --my2: 26%; } }
/* each pool swells and sinks around 1 by ±(amplitude × --hvp-breathe) on
   its own coprime timer; the compressor above handles any alignment */
@keyframes hvB1 { from { --bi1: calc(1 - 0.38 * var(--hvp-breathe, 1)); } to { --bi1: calc(1 + 0.30 * var(--hvp-breathe, 1)); } }
@keyframes hvB2 { from { --bi2: calc(1 + 0.32 * var(--hvp-breathe, 1)); } to { --bi2: calc(1 - 0.40 * var(--hvp-breathe, 1)); } }
@keyframes hvB3 { from { --bi3: calc(1 - 0.42 * var(--hvp-breathe, 1)); } to { --bi3: calc(1 + 0.28 * var(--hvp-breathe, 1)); } }
@keyframes hvB4 { from { --bi4: calc(1 + 0.28 * var(--hvp-breathe, 1)); } to { --bi4: calc(1 - 0.44 * var(--hvp-breathe, 1)); } }
@keyframes hvB5 { from { --bi5: calc(1 - 0.36 * var(--hvp-breathe, 1)); } to { --bi5: calc(1 + 0.34 * var(--hvp-breathe, 1)); } }

/* depth + a rim-light that lets the frosted edge catch colour, no drawn border.
   same 3-slot + glass-sh structure as the rest state above, so it fades in */
.glass:hover {
  box-shadow:
    /* fixed --accent tint (NOT the per-page --page hue) so the shadow is the
       same cool colour on every page; sRGB mix avoids an oklch red detour */
    0 30px 62px -24px color-mix(in srgb, var(--accent) 26%, rgba(15,23,42,0.30)),
    inset 0 0 0 1px rgba(255,255,255,0.34),
    inset 0 20px 44px -30px transparent,
    var(--glass-sh);
}
/* opt out of the whole hover treatment, for glass that is a panel rather than
   a control: no nebula (the ::after never renders) and no rim-light — it holds
   the same shadow structure as the rest state so nothing shifts on hover.
   js/app.js keeps these out of the cursor's hover field too. */
.glass.hv-none::after { content: none; }
.glass.hv-none:hover {
  box-shadow:
    0 30px 62px -24px transparent,
    inset 0 0 0 1px rgba(255,255,255,0.05),
    inset 0 20px 44px -30px transparent,
    var(--glass-sh);
}
.hv-lift:hover { transform: translateY(-5px); }
/* gentler variant for wide full-width rows, where the full lift reads heavy */
.hv-lift-sm:hover { transform: translateY(-2px); }

@media (prefers-reduced-motion: reduce) {
  .glass::after { animation: none !important; }
  .hv-lift:hover, .hv-lift-sm:hover { transform: none; }
}

.btn-primary {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 14px 24px;
  border-radius: 11px;
  background: var(--accent);
  color: var(--bg);
  font-family: var(--font-label);
  font-size: 13px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-weight: 500;
  box-shadow: 0 6px 16px -12px var(--accent), inset 0 1px 0 rgba(255,255,255,0.28);
  transition: filter .18s ease, transform .18s ease, box-shadow .18s ease;
  cursor: pointer;
  border: none;
}
.btn-primary:hover {
  filter: brightness(1.05);
  box-shadow: 0 9px 22px -14px var(--accent);
  transform: translateY(-1px);
  color: var(--bg);
}
.btn-primary--lg { padding: 15px 26px; white-space: nowrap; }
.btn-ghost {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 14px 24px;
  border-radius: 11px;
  background: transparent;
  color: var(--fg);
  font-family: var(--font-label);
  font-size: 13px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
  border: 1px solid var(--line);
}
.btn-ghost:hover { border-color: var(--accent); }

.hatch {
  background: repeating-linear-gradient(135deg, var(--bg2), var(--bg2) 9px, var(--card) 9px, var(--card) 18px);
  display: flex;
  align-items: center;
  justify-content: center;
}
.hatch--fine { background: repeating-linear-gradient(135deg, var(--bg2), var(--bg2) 8px, var(--card) 8px, var(--card) 16px); }
.ph-label { font-family: var(--font-label); font-size: 11px; letter-spacing: 0.12em; color: var(--muted); }
.ph-label--wide { letter-spacing: 0.14em; }
.ph-label--xs { font-size: 10px; }
.ph-label--photo { font-size: 10.5px; }

/* ---------- home ---------- */

/* home "Selected publications": featured-paper carousel with the paper list
   as its navigation — main card (vertical: cover on top, caption below) with
   the list riding over it; the rotation walks the list */
/* the figure always owns the full card; the rail is a glass panel (the same
   .glass material, hv-none) that slides in over it from the right edge */
.feat-duo { display: grid; grid-template-columns: minmax(0, 1fr); align-items: stretch; overflow: hidden; }
.feat-duo .feat-list {
  position: absolute;
  top: 12px;
  right: -440px; /* parked past the clip; slides via `right`, never transform —
                    a transform on the glass kills its backdrop-filter (see the
                    glass notes; the panel must keep diffusing as it lands) */
  bottom: 12px;
  width: min(320px, 64%);
  z-index: 2;
  padding: 24px 22px;
  border-left: none;
  overflow-y: auto;
  visibility: hidden;
  transition: right .5s cubic-bezier(.22, 1, .36, 1), visibility 0s .5s;
}
.feat-duo--open .feat-list {
  right: 12px;
  visibility: visible;
  transition: right .5s cubic-bezier(.22, 1, .36, 1);
}
@media (prefers-reduced-motion: reduce) { .feat-duo .feat-list, .feat-duo--open .feat-list { transition: none; } }
/* the list toggle floats bare in the card's top-right corner — no frame,
   just the glyph; it rides above the sliding panel so it always closes it */
.feat-duo .feat-list-toggle {
  position: absolute; /* outranks .cv-ico's own position:relative (tooltip anchor) */
  top: 14px;
  right: 14px;
  z-index: 4;
  background: none;
  border: 0;
  color: var(--fg);
}
.feat-list-toggle.active { color: var(--accent-ink, var(--accent)); }
/* no hover tooltip pill on this one — it floats over the photo */
.feat-duo .feat-list-toggle::after { content: none; }
/* while folded the dots stand in as slide navigation, floating under the
   card */
.feat-dots--duo { padding: 14px 0 0; margin-bottom: 0; }
.feat-duo--open + .feat-dots--duo { display: none; }
/* vertical slide: the photo/cover claims the larger share of the card */
.feat-duo .pub-feat { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) auto; }
/* one cover height for both states, so toggling the rail never resizes the
   card — the rail column inherits the same height and centers its rows */
.feat-duo .pub-feat-cover { position: relative; border-right: none; border-bottom: 1px solid var(--line); min-height: clamp(320px, 44vh, 470px); }
/* slim caption strip: the title leads its row (venue · year beside it), and
   the authors sit under the title, one line with an ellipsis. Fixed height,
   so a long author list can never resize the card. */
.feat-duo .pub-feat-body {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: baseline;
  column-gap: 14px;
  row-gap: 2px;
  padding: 11px 22px 13px;
  height: 78px;
  overflow: hidden;
}
.feat-duo .pub-feat-body .featured-flag { grid-column: 1; }
.feat-duo .pub-feat-title { grid-column: 2; }
.feat-duo .pub-feat-abs {
  grid-column: 2;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 1;
  overflow: hidden;
  margin: 0;
  font-size: 12.5px;
}
.feat-duo .pub-feat-abs {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  font-size: 13px;
}
/* the paper's constellation, low in the cover's corner */
.feat-constellation { position: absolute; right: 18px; bottom: 14px; width: 120px; height: 80px; opacity: 0.5; }
/* every slide is one clamped title line in a baseline row, so the strip
   height is constant across slides without pinning it */
/* one-line title keeps the box shallow; the rail carries the full text */
.feat-duo .pub-feat-title {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 1;
  overflow: hidden;
}
/* the flag carries venue · year — the cover keeps only its wordmark */
.feat-duo .pub-feat-cover .pub-feat-yr { display: none; }
/* a graphical abstract is a diagram: contained on the topic nebula, never
   cropped. The height is fixed, not a floor — against an indefinite height
   the image's max-height:100% resolves to none, and a portrait figure would
   size the row to its own pixels and run the card off the screen. */
.feat-duo .pub-feat-cover--fig { min-height: 0; height: clamp(320px, 44vh, 470px); padding: 20px; }
.fig-img { display: block; width: auto; height: auto; max-width: 100%; max-height: 100%; object-fit: contain; }

/* the list is a constellation rail: one hairline threading a star per
   paper — the active star fills with its topic hue and lights its row */
.feat-list {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 16px 22px;
  border-left: 1px solid var(--line);
}
/* the duo is a structural panel, not an interactive card — mute the glass
   hover treatment (aurora + accent shadow); row/title hovers stay */
.feat-duo:hover {
  box-shadow:
    0 30px 62px -24px transparent,
    inset 0 0 0 1px rgba(255,255,255,0.05),
    inset 0 20px 44px -30px transparent,
    var(--glass-sh);
}
.feat-duo:hover::after { opacity: 0; animation-play-state: paused; }
.feat-row {
  position: relative;
  display: grid;
  grid-template-columns: 18px 1fr;
  grid-template-rows: auto auto;
  column-gap: 12px;
  row-gap: 2px;
  padding: 11px 8px 13px 0;
  background: none;
  border: none;
  text-align: left;
  cursor: pointer;
}
/* rows answer hover with the site's card gesture — the same lift + curve */
.feat-row { transition: transform .5s cubic-bezier(.22, 1, .36, 1); }
.feat-row:hover { transform: translateY(-2px); }
@media (prefers-reduced-motion: reduce) { .feat-row:hover { transform: none; } }
.feat-row-node { grid-column: 1; grid-row: 1 / span 2; position: relative; }
.feat-row-node::before {
  content: '';
  position: absolute;
  left: 8px;
  top: -9px;
  bottom: -9px;
  width: 1px;
  background: var(--line);
}
.feat-row:first-child .feat-row-node::before { top: 15px; }
.feat-row:last-child .feat-row-node::before { bottom: auto; height: 24px; }
.feat-row-node::after {
  content: '';
  position: absolute;
  left: 5px;
  top: 12px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  border: 1px solid var(--muted);
  background: var(--card);
  transition: background-color .25s ease, border-color .25s ease, box-shadow .25s ease;
}
.feat-row:hover .feat-row-node::after { border-color: var(--fc); }
.feat-row.active .feat-row-node::after {
  background: var(--fc);
  border-color: var(--fc);
  box-shadow: 0 0 9px color-mix(in oklch, var(--fc) 60%, transparent);
}
.feat-row-meta { grid-column: 2; display: flex; gap: 10px; align-items: baseline; }
/* the venue leads the row — it identifies a paper where the year, shared by
   half the list, does not */
.feat-row-venue {
  font-family: var(--font-label);
  font-size: 11px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
  transition: color .25s ease;
}
.feat-row-yr { font-family: var(--font-label); font-size: 10px; letter-spacing: 0.05em; color: var(--fc); opacity: 0.75; }
.feat-row-title { grid-column: 2; font-size: 13.5px; line-height: 1.4; color: var(--muted); transition: color .25s ease; }
.feat-row:hover .feat-row-title { color: var(--fg); }
.feat-row.active .feat-row-venue { color: var(--fc); }
.feat-row.active .feat-row-title { color: var(--fg); }
@media (max-width: 860px) {
  .feat-duo .feat-list { width: min(300px, 86%); }
}
.card-meta { display: flex; align-items: center; gap: 10px; }
.card-date { font-family: var(--font-label); font-size: 12px; }
.tag-pill {
  font-family: var(--font-label);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid;
}
.card-title { font-family: var(--font-display); font-weight: 500; font-size: 18px; line-height: 1.3; margin: 0; }
.card-body { margin: 0; font-size: 14px; line-height: 1.55; color: var(--muted); }

.grid-pillars { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 16px; }
.card-pillar { padding: 26px 22px; display: flex; flex-direction: column; gap: 10px; }
/* Each pillar is tagged with where it sits on the silicon→system stack, not with
   an ordinal — the four areas aren't a sequence, they're four altitudes. The
   pillar hue is bound to that same axis, so the color carries the meaning too. */
.pillar-lvl { font-family: var(--font-label); font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; }
.pillar-title { font-family: var(--font-display); font-weight: 500; font-size: 17px; line-height: 1.25; margin: 0; }
.pillar-body { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--muted); }

/* home PI card ("pihome" — .pi-card belongs to the People page): the prof
   hero restated on the light band — intro + appointments timeline left,
   full-column portrait and the contact row right, one glass card */
.pihome-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(220px, 24vw, 300px);
  gap: clamp(28px, 4vw, 52px);
  align-items: stretch;
  padding: clamp(24px, 2.8vw, 38px);
  color: var(--fg);
}
.pihome-main { min-width: 0; }
.pihome-side { min-width: 0; display: flex; flex-direction: column; }
/* the portrait fills the column: the text column sets the card's height and
   the photo stretches to meet it edge-to-edge (cover crop keeps the framing) */
.pihome-photo { display: flex; flex: 1; min-height: 0; }
.pihome-photo .portrait { flex: 1; height: 100%; aspect-ratio: auto; max-width: none; }
.pihome-name { font-family: var(--font-display); font-weight: 600; font-stretch: var(--w-sec); font-size: clamp(26px, 3vw, 38px); letter-spacing: -0.01em; margin: 0; }
.pihome-name a { color: inherit; }
.pihome-name a:hover { color: var(--accent); }
.pihome-role { font-family: var(--font-label); font-size: 12.5px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent); margin: 7px 0 14px; }
.pihome-bio { margin: 0; max-width: 760px; font-size: 14.5px; line-height: 1.65; color: var(--muted); }
/* identity and record are one object, hairline-separated (prof hero grammar) */
.pihome-record { margin-top: clamp(20px, 2.8vh, 30px); padding-top: clamp(18px, 2.4vh, 26px); border-top: 1px solid var(--line); }
@media (max-width: 820px) {
  .pihome-card { grid-template-columns: 1fr; }
  .pihome-side { max-width: 280px; }
  /* single column: no column height to fill — back to the fixed crop */
  .pihome-photo .portrait { aspect-ratio: 4 / 5; height: auto; }
}

/* home "Recent updates": slim rows, a tag-hued panel on the left carrying
   the date over its tag. The story line unfolds on hover/focus via a
   grid-template-rows 0fr→1fr transition — no transform or opacity on the
   glass, so the material keeps working. */
.selpub-list { display: flex; flex-direction: column; gap: 14px; }
.sel-pub { display: grid; grid-template-columns: clamp(130px, 15vw, 170px) minmax(0, 1fr); overflow: hidden; color: var(--fg); }
.sel-pub-side {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 14px 10px;
  border-right: 1px solid var(--line);
  background:
    radial-gradient(120% 70% at 50% -12%, color-mix(in oklch, var(--fc) 30%, transparent), transparent 70%),
    radial-gradient(90% 60% at 12% 110%, color-mix(in oklch, var(--fc) 14%, transparent), transparent 70%);
}
/* the date is the row's wordmark — it stands where the venue used to, in the
   same display voice; the tag speaks underneath it in the rail's label voice */
.sel-pub-date { font-family: var(--font-display); font-weight: 600; font-stretch: var(--w-sec); font-size: clamp(19px, 2vw, 24px); letter-spacing: -0.01em; color: var(--fc); }
.sel-pub-tag {
  font-family: var(--font-label);
  font-size: 9.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-align: center;
  color: var(--fc);
  opacity: 0.75;
}
.sel-pub-main { padding: 18px 22px; display: flex; flex-direction: column; justify-content: center; }
.sel-pub-title { font-family: var(--font-display); font-weight: 500; font-size: clamp(16px, 1.8vw, 19px); line-height: 1.35; margin: 0; text-wrap: pretty; }
.sel-pub-more { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .5s cubic-bezier(.22, 1, .36, 1); }
.sel-pub-more-in { overflow: hidden; min-height: 0; }
.sel-pub:hover .sel-pub-more, .sel-pub:focus-visible .sel-pub-more { grid-template-rows: 1fr; }
.sel-pub-abs { margin: 8px 0 0; font-size: 13px; line-height: 1.55; color: var(--muted); }
/* an update has no authors line above it, so the body carries the whole gap */
.sel-pub--upd .sel-pub-abs { margin-top: 10px; }
.sel-pub:hover .sel-pub-title { color: var(--accent); }
@media (prefers-reduced-motion: reduce) { .sel-pub-more { transition: none; } }
@media (max-width: 640px) {
  .sel-pub { grid-template-columns: 1fr; }
  .sel-pub-side { border-right: none; border-bottom: 1px solid var(--line); padding: 12px 10px; }
}

/* ---------- about ---------- */

.about-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-stretch: var(--w-hero);
  font-size: clamp(30px, 5vw, 58px);
  line-height: 1.08;
  letter-spacing: -0.004em;
  margin: 0;
  text-wrap: balance;
}
.about-mission {
  max-width: 720px;
  margin: 28px 0 0;
  font-size: clamp(16px, 1.8vw, 19px);
  line-height: 1.65;
  color: var(--muted);
  text-wrap: pretty;
}
/* silicon→system rail under the mission: three hue-dotted jump stops */
.about-rail { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 34px; }
.rail-stop {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--font-label); font-size: 12.5px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--muted);
  background: none; border: 1px solid var(--line); border-radius: 999px;
  padding: 8px 16px; transition: border-color .25s, color .25s;
}
.rail-stop:hover { border-color: color-mix(in oklch, var(--fg) 55%, transparent); color: var(--fg); }
.rail-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
.rail-sep { color: var(--line); font-size: 13px; }
.axis-note--rail { margin-left: 8px; }

/* thrust sections: copy and the live instrument share one frosted panel
   (js/about.js) so the text reads against glass, not the raw inverted sky */
.about-thrust { display: flex; align-items: center; }
.thrust-card { width: 100%; display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); gap: clamp(24px, 4vw, 56px); align-items: center; padding: clamp(24px, 3.2vw, 40px); }
.thrust-card--rev .thrust-copy { order: 2; }
.thrust-card--rev .thrust-inst { order: 1; }
@media (max-width: 940px) {
  .thrust-card { grid-template-columns: 1fr; gap: 24px; }
  .thrust-card--rev .thrust-copy { order: 1; }
  .thrust-card--rev .thrust-inst { order: 2; }
}
/* wide variant: copy on top, figure spans the full card (two machines side
   by side need the width) */
.thrust-card--wide { grid-template-columns: 1fr; align-items: start; }
/* per-card tuning — the wide cards each carry a different figure */
.thrust-card--wide[data-inst="imc"] { gap: 28px; }
.thrust-card--wide[data-inst="imc"] .thrust-copy { max-width: none; }
/* the wide card owns the full band, so the copy uses it: two text
   columns at desktop keep the measure readable while spanning the card */
.thrust-card--wide[data-inst="imc"] .thrust-body { columns: 2; column-gap: 48px; }
@media (max-width: 860px) { .thrust-card--wide[data-inst="imc"] .thrust-body { columns: 1; } }
.thrust-card--wide[data-inst="imc"] .inst-canvas { height: 320px; }
@media (max-width: 560px) { .thrust-card--wide[data-inst="imc"] .inst-canvas { height: 540px; } }
.thrust-card--wide[data-inst="adapt"] { gap: 28px; }
.thrust-card--wide[data-inst="adapt"] .inst-canvas { height: 340px; }
@media (max-width: 560px) {
  .thrust-card--wide[data-inst="adapt"] .inst-canvas { height: 520px; }
  /* two energy figures plus the verdict is too long for one nowrap line here */
  .thrust-card--wide[data-inst="adapt"] .inst-readout { white-space: normal; text-align: right; }
}
.thrust-title { font-family: var(--font-display); font-weight: 600; font-stretch: var(--w-sec); font-size: clamp(24px, 3vw, 34px); line-height: 1.15; letter-spacing: -0.004em; margin: 0 0 14px; text-wrap: balance; }
.thrust-body { margin: 0 0 18px; font-size: 15px; line-height: 1.7; color: var(--muted); text-wrap: pretty; }
.thrust-chips { display: flex; gap: 8px; flex-wrap: wrap; }
.int-chip--link { transition: color .2s, border-color .2s; }
.int-chip--link:hover { color: var(--accent); border-color: color-mix(in oklch, var(--accent) 55%, var(--line)); }

/* instrument chrome: mono readouts; one control */
.thrust-inst { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
.inst-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.inst-head .inst-name { margin-bottom: 0; }
.inst-readout { font-family: var(--font-mono); font-size: 12px; color: var(--fg); white-space: nowrap; }
.inst-canvas { display: block; width: 100%; height: 280px; }
@media (max-width: 560px) { .inst-canvas { height: 220px; } }
.inst-ctl { display: flex; align-items: center; }
.inst-hint { margin: 0; font-size: 12px; line-height: 1.5; color: var(--muted); }

.inst-seg { display: inline-flex; border: 1px solid var(--line); border-radius: 999px; padding: 3px; gap: 2px; }
.seg-btn {
  font-family: var(--font-label); font-size: 12px; letter-spacing: 0.04em;
  color: var(--muted); background: none; border: none; border-radius: 999px;
  padding: 6px 14px; transition: background .2s, color .2s;
}
.seg-btn:hover { color: var(--fg); }
.seg-btn.on { background: color-mix(in oklch, var(--seg-accent, var(--accent)) 16%, transparent); color: var(--fg); }

.inst-slider { display: flex; align-items: center; gap: 12px; width: 100%; }
.slider-lab { font-family: var(--font-label); font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); white-space: nowrap; }
.inst-slider input[type="range"] { flex: 1; min-width: 0; }
.slider-val { font-family: var(--font-mono); font-size: 12px; color: var(--fg); min-width: 44px; text-align: right; }

/* model-compression wide card: full-width copy, 3-panel figure, one
   control per panel aligned under its panel */
.thrust-card--wide[data-inst="quant"] .inst-canvas { height: 356px; }
.inst-ctl3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; align-items: center; justify-items: center; width: 100%; }
.inst-ctl3 .inst-slider { max-width: 260px; }
@media (max-width: 620px) {
  .thrust-card--wide[data-inst="quant"] .inst-canvas { height: 660px; }
  .inst-ctl3 { grid-template-columns: 1fr; justify-items: start; }
}


/* ---------- professor ---------- */


.prof-name {
  font-family: var(--font-display);
  font-weight: 600;
  font-stretch: var(--w-hero);
  font-size: clamp(32px, 5vw, 52px);
  letter-spacing: -0.004em;
  margin: 0 0 6px;
}
.prof-role { font-size: 17px; color: var(--muted); margin-bottom: 22px; }
.prof-links { display: flex; gap: 10px 24px; flex-wrap: wrap; margin-top: 30px; align-items: center; }
.portrait {
  aspect-ratio: 4 / 5;
  border-radius: 18px;
  border: 1px solid var(--glass-brd);
  box-shadow: 0 24px 60px -24px rgba(0,0,0,0.6);
  overflow: hidden;
}
.portrait-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 20%;
  display: block;
  border-radius: inherit;
}

.prof-p { font-size: 15px; line-height: 1.68; color: var(--fg); margin: 0 0 14px; text-wrap: pretty; }
.prof-p--muted { font-size: 14.5px; color: var(--muted); margin: 0; }

/* The hero card carries identity + the career record, so the hero drops the
   100vh fill — the card decides the height. Material is .glass-sky (the header
   bar's), so it stays dark against the starfield. Narrower than the site's
   default measure: the record only needs half the card, and the portrait takes
   the rest at a size worth looking at. */
.prof-hero-sec {
  padding-left: max(clamp(16px, 4vw, 40px), calc(50vw - 540px));
  padding-right: max(clamp(16px, 4vw, 40px), calc(50vw - 540px));
}
/* Deliberate divergence from the header bar's material: this card holds a lot
   of small text, so it takes a scrim to push the starfield and grid further
   back. .glass-sky--tint marks it as an intentional exception — everything else
   (blur, border, radius, shadow) still matches, and scripts/glass-parity checks
   that it does. */
.glass-sky--tint { background-color: color-mix(in oklch, var(--band-dark) 62%, transparent); }
.prof-hero-card {
  padding: clamp(24px, 2.8vw, 38px);
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(230px, 27vw, 330px);
  gap: clamp(28px, 4vw, 56px);
  align-items: start;
}
.prof-hero-main { min-width: 0; }
.prof-hero-side { min-width: 0; display: flex; flex-direction: column; gap: 14px; }
.prof-hero-card .portrait { max-width: none; }
/* identity block and record are one object; a hairline separates them */
.prof-hero-record {
  margin-top: clamp(22px, 3vh, 34px);
  padding-top: clamp(20px, 2.6vh, 30px);
  border-top: 1px solid var(--glass-brd);
}
@media (max-width: 820px) {
  .prof-hero-card { grid-template-columns: 1fr; }
  .prof-hero-side { max-width: 280px; }
}


/* ---- accent on the dark sky ----
   --accent at L 0.56 measures 3.57:1 against the hero card, under the 4.5:1
   WCAG AA floor for the 10-13px mono it gets used on. Fix: teal is reserved for
   what you can click, and lifted until it clears AA; labels and years drop to
   the neutral --muted, so colour reads as interaction rather than decoration.
   Scoped to the dark sky — on the light inverted bands 0.56-on-white already
   passes — and large display `.accent` text is untouched (3:1 suffices there). */
.hero, .glass-sky { --accent-ink: oklch(0.86 0.09 200); }
.hero .mono-label, .glass-sky .mono-label,
.hero .pc-y, .glass-sky .pc-y { color: var(--muted); }

/* ---- record cards ----
   Every record sits on glass so it stays legible over the halftone sky. Rows
   are one entry each: right-aligned mono year, title, muted org. */
/* the professor page's cards are read, not clicked, so they take no hover
   treatment — kill the aurora and the lift/rim the shared .glass adds */
.pc-card:hover::after, .cv-rail-card:hover::after { opacity: 0; animation-play-state: paused; }
.pc-card:hover, .cv-rail-card:hover { box-shadow: var(--glass-sh); }
.pc-card { padding: 24px 26px; }
.pc-head { display: flex; align-items: center; gap: 14px; margin-bottom: 14px; }
.pc-head .mono-label { margin-bottom: 0; flex: none; }
.pc-head::after { content: ''; flex: 1 1 auto; height: 1px; background: var(--line); }
.pc-prose { max-width: 78ch; }
.pc-list { display: flex; flex-direction: column; }
.pc-row { display: grid; grid-template-columns: 70px 1fr; gap: 12px; padding: 6px 0; border-top: 1px solid var(--line); }
.pc-row:first-child { border-top: 0; padding-top: 0; }
.pc-list--nb .pc-row { grid-template-columns: 1fr; gap: 0; }
.pc-y {
  font-family: var(--font-label);
  font-size: 10.5px;
  line-height: 1.5;
  color: var(--accent);
  text-align: right;
  padding-top: 3px;
  white-space: nowrap;
}
.pc-b { min-width: 0; }
.pc-t { font-weight: 500; font-size: 14px; line-height: 1.32; text-wrap: pretty; }
.pc-t--sp { margin-top: 10px; }
.pc-o { font-size: 12.5px; line-height: 1.4; color: var(--muted); margin-top: 1px; }
.pc-sub {
  font-family: var(--font-label);
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 22px 0 10px;
}
.pc-card--meet { padding-bottom: 26px; }
.meet-fold { display: flex; flex-wrap: wrap; align-items: center; gap: 14px 28px; }
.meet-fold .meet-intro { margin: 0; flex: 1 1 340px; }

/* everything below the hero sits at the hero card's measure — it was running
   1360 against the hero's 1080, which read as a misalignment */
.prof-band {
  padding-left: max(clamp(16px, 4vw, 40px), calc(50vw - 540px));
  padding-right: max(clamp(16px, 4vw, 40px), calc(50vw - 540px));
}
.pc-col { min-width: 0; }

/* one column at a readable measure: at 1080 the two-column version left a
   dead quarter under the short first paragraph */
.prof-band .pc-prose { max-width: 74ch; }

/* ---- CV surface (?cv=a|b|c) ----
   The whole CV in full, continuous, with a sticky index. Shared: undated lists
   (talks, service) tile in columns instead of running long; every section is a
   full glass card. */
.pc-cols { columns: 2; column-gap: clamp(32px, 4vw, 64px); }
.pc-row--nb { grid-template-columns: 1fr; gap: 0; break-inside: avoid; }
.svc-group { break-inside: avoid; }
.pc-sub--flush { margin-top: 0; }
.pc-cols .svc-group + .svc-group { margin-top: 18px; }
@media (max-width: 760px) { .pc-cols { columns: 1; } }

.cv-flow-main { display: flex; flex-direction: column; gap: clamp(16px, 1.6vw, 22px); min-width: 0; }

/* the index itself: a list of section jumps, active one marked */
.cv-index { display: flex; flex-direction: column; gap: 2px; }
.cv-index-btn {
  font-family: var(--font-label);
  font-size: 11.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-align: left;
  padding: 9px 12px;
  border: 0;
  border-left: 2px solid transparent;
  background: none;
  color: var(--muted);
  cursor: pointer;
  transition: color .18s ease, border-color .18s ease, background .18s ease;
}
.cv-index-btn:hover { color: var(--fg); }
.cv-index-btn.active { border-left-color: var(--accent); color: var(--fg); }

/* a + b — content left, a carded index rail on the right (mirroring the hero's
   name-left / portrait-right). The card gives the mono index a legible surface
   over the busy halftone band. */
.cv-flow--railright {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(210px, 24vw, 288px);
  gap: clamp(20px, 2.4vw, 34px);
  align-items: start;
}
.cv-rail-card {
  position: sticky;
  top: calc(var(--hdr-slot, 72px) + 24px);
  padding: 18px;
}

/* b — the rail leads with a profile block: a full-column portrait with the name
   below it, so the right column that used to empty out now carries the identity
   the way a CV header would */
.cv-mini-portrait {
  width: 100%;
  aspect-ratio: 4 / 5;
  border-radius: 12px;
  overflow: hidden;
  border: 1px solid var(--line);
}
.cv-mini-portrait .portrait-img { width: 100%; height: 100%; object-fit: cover; object-position: center 18%; }
.cv-mini-id { margin-top: 14px; }
.cv-mini-name { font-family: var(--font-display); font-weight: 600; font-size: 17px; letter-spacing: -0.01em; }
.cv-mini-role { font-size: 12.5px; color: var(--muted); margin-top: 3px; text-wrap: pretty; }
/* the rail's contact row: icon buttons, not a text list — email · scholar · CV ·
   office hours, each a monoline glyph with an accessible label/tooltip */
.cv-links {
  display: flex;
  gap: 8px;
  margin: 18px 0;
  padding: 16px 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.cv-links { position: relative; align-items: center; justify-content: space-between; }
/* email open: the three non-envelope icons step aside for the chip; the
   envelope holds its hover treatment as the active toggle */
.email-open .cv-ico:not([data-email]) { opacity: 0; transform: translateX(10px); pointer-events: none; }
.email-open .cv-ico[data-email] {
  color: var(--accent-ink, var(--accent));
  border-color: color-mix(in oklch, var(--accent-ink, var(--accent)) 55%, var(--line));
  background: color-mix(in oklch, var(--accent-ink, var(--accent)) 10%, transparent);
}
/* fixed 46px squares in every zone — flex-grow made them wide rectangles whose
   ratio changed with the column width */
.cv-ico {
  flex: 0 0 46px;
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: none;
  color: var(--muted);
  cursor: pointer;
  transition: color .16s ease, border-color .16s ease, background .16s ease, transform .22s ease, opacity .22s ease;
}
.cv-ico svg { width: 20px; height: 20px; }
.cv-ico:hover {
  color: var(--accent-ink, var(--accent));
  border-color: color-mix(in oklch, var(--accent-ink, var(--accent)) 55%, var(--line));
  background: color-mix(in oklch, var(--accent-ink, var(--accent)) 10%, transparent);
  transform: translateY(-2px);
}
.cv-ico:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .cv-ico:hover { transform: none; } }

/* hover/focus label — aria-label rendered as a small pill above the icon. Uses
   --fg/--bg so it inverts per zone: a white pill on the dark hero, a dark pill
   on the light rail. */
.cv-ico { position: relative; }
.cv-ico::after {
  content: attr(aria-label);
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  padding: 4px 8px;
  border-radius: 6px;
  background: var(--fg);
  color: var(--bg);
  font-family: var(--font-label);
  font-size: 10.5px;
  letter-spacing: 0.02em;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity .16s ease, transform .16s ease;
  z-index: 10;
}
.cv-ico:hover::after, .cv-ico:focus-visible::after { opacity: 1; transform: translateX(-50%) translateY(0); }
@media (prefers-reduced-motion: reduce) { .cv-ico::after { transition: opacity .16s ease; } }

/* hero: the icon row sits under the portrait, same squares as the rail.
   The hero column is the wider of the two, so space-between parked its four
   squares in the corners — here they group on the centreline instead.
   --cv-gap is the knob (?ico=<px> overrides it live; see app.js). This is
   scoped to the hero on purpose: the rail is narrower and keeps the shared
   space-between, which can never push the squares outside its card. */
.cv-links--hero {
  border: 0;
  padding: 0;
  margin: 0;
  justify-content: center;
  /* the min() caps the gap at what the column can hold: the hero's side column
     narrows with the viewport, and a fixed 40px pushes the squares outside the
     card below ~1100px wide. Centred + overflowing clips both ends. */
  gap: min(var(--cv-gap, 40px), calc((100% - 4 * 46px) / 3));
}

/* the primary action, in both copies of the identity block: full width under
   the icon row, so "request a meeting" reads as the one thing to press rather
   than a fifth glyph */
.prof-hero-cta, .cv-rail-cta { justify-content: center; width: 100%; }
/* the rail is the narrower, denser column — the button steps down to match it
   and keeps the icon row's rhythm above the section index */
.cv-rail-cta { padding: 11px 14px; font-size: 11.5px; margin-bottom: 16px; }

@media (max-width: 820px) {
  .cv-flow--railright { grid-template-columns: 1fr; }
  .cv-rail-card { position: static; order: -1; }
  .cv-rail-card .cv-index { flex-direction: row; flex-wrap: wrap; }
}

/* keep the light-band card material honest: on glass the hairlines that
   separate rows read too hot at full strength */
.pc-card .pc-row { border-color: color-mix(in oklch, var(--line) 72%, transparent); }


.interest-row { display: flex; gap: 8px; flex-wrap: wrap; }
.int-chip {
  font-family: var(--font-label);
  font-size: 12px;
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--muted);
}

/* narrow screens: the year gutter stops paying for itself — stack it above the
   entry, and let the fact strip run full width */
@media (max-width: 560px) {
  .pc-row { grid-template-columns: 1fr; gap: 0; }
  .pc-y { text-align: left; padding-top: 0; margin-bottom: 2px; }
}

/* ---------- people ---------- */

.pi-card {
  border-radius: 18px;
  padding: clamp(22px, 3vw, 32px);
  display: grid;
  /* portrait column hugs the photo so the text sits right beside it */
  grid-template-columns: clamp(160px, 16vw, 200px) minmax(0, 1fr);
  gap: clamp(20px, 3vw, 36px);
  align-items: center;
}
@media (max-width: 560px) { .pi-card { grid-template-columns: 1fr; } }
.pi-portrait { max-width: 200px; width: 100%; aspect-ratio: 1; border-radius: 14px; border: 1px solid var(--line); overflow: hidden; }
.pi-eyebrow {
  font-family: var(--font-label);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
}
.pi-name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(24px, 3.2vw, 34px);
  letter-spacing: -0.02em;
  margin: 8px 0 4px;
}
.pi-role { font-size: 15px; color: var(--muted); margin-bottom: 12px; }
.pi-desc { margin: 0; font-size: 14.5px; line-height: 1.6; color: var(--muted); max-width: 560px; }

/* one rhythm for the whole roster: the gap between groups is built from the
   exact same terms as a section boundary on this page — pb-xs plus the snap
   sections' header-clearance top pad (.invert-wrap > .snap) — so every gap
   on the People page computes to the identical pixel value */
.people-group { margin-bottom: calc(clamp(20px, 3vh, 32px) + max(clamp(48px, 7vh, 82px), var(--hdr-slot, 0px) + 16px)); }
.people-group:last-child { margin-bottom: 0; }
.group-glyph { color: var(--accent); margin-right: 2px; }
.grid-people { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; }
.card-member { padding: 18px; display: flex; flex-direction: column; gap: 14px; }
.member-portrait { aspect-ratio: 1; border-radius: 12px; border: 1px solid var(--line); overflow: hidden; }
.member-name { font-family: var(--font-display); font-weight: 500; font-size: 17px; margin: 0 0 4px; }
.member-role { font-size: 13px; color: var(--muted); }
.member-chip {
  display: inline-block;
  white-space: nowrap;
  font-family: var(--font-label);
  font-size: 11px;
  letter-spacing: 0.02em;
  padding: 4px 10px;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--muted);
}

.member-now { font-family: var(--font-label); font-size: 11.5px; color: var(--accent); margin-top: 5px; }

/* former cards: current affiliation leads (accent — it's the live fact),
   the lab-era role + start year sit under it as a quiet label line.
   Position and organisation each get their own line (see nowLines() in
   app.js) rather than wrapping wherever the card runs out of width. */
.member-aff { font-size: 13px; color: var(--accent); }
.member-aff-l { display: block; }
/* the identity block absorbs the card's slack so .member-foot — and with it the
   co-advised chip — sits on the card's floor. Grid rows stretch to equal
   height, so every badge in a row lands on the same baseline. */
.member-id { flex: 1 1 auto; min-height: 0; }
.member-was { font-family: var(--font-label); font-size: 11px; color: var(--muted); margin-top: 4px; }
.card-member--sm .member-aff { font-size: 12px; }

/* smaller cards for former members */
.grid-people--sm { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; }
.card-member--sm { padding: 14px; gap: 10px; }
.card-member--sm .member-name { font-size: 15px; margin-bottom: 3px; }
.card-member--sm .member-role { font-size: 12px; }

/* institution cards: school hue as a still aurora — a soft nebula pool
   hanging from the top edge (same grammar as the hover aurora, but static)
   with a hairline that fades at both ends instead of a drawn border, plus a
   large logo watermark sinking past the bottom-right corner. DGIST marks
   internal students; KAIST/SKKU mark co-advised ones. */
.card-member { position: relative; overflow: hidden; }
/* --inst-c paints the wash/hairline (kept bright so hues read apart at low
   alpha); --inst-ink is the darker sibling used for chip text. DGIST = bright
   cyan, KAIST = royal navy, SKKU = green — three clearly separated hues. */
.inst-dgist { --inst-c: #00b1e1; --inst-ink: #007ba6; }
.inst-kaist { --inst-c: #2a63c9; --inst-ink: #00427e; }
.inst-skku { --inst-c: #2e9c55; --inst-ink: #1f7a3f; }
.card-inst {
  background-image:
    radial-gradient(130% 52px at 50% -10px, color-mix(in srgb, var(--inst-c) 30%, transparent), transparent 80%),
    linear-gradient(90deg, transparent 3%, color-mix(in srgb, var(--inst-c) 52%, transparent) 28%, color-mix(in srgb, var(--inst-c) 52%, transparent) 72%, transparent 97%);
  background-repeat: no-repeat;
  background-size: 100% 100%, 100% 1.5px;
  background-position: top, top;
}
.card-inst .member-chip { color: var(--inst-ink); border-color: color-mix(in srgb, var(--inst-ink) 36%, transparent); }
/* z-index:-1 tucks the watermark behind the (opaque) portrait and card text —
   it only shows in the open glass around them, never on the photo */
.inst-mark {
  position: absolute;
  z-index: -1;
  bottom: -30px;
  right: -24px;
  width: 172px;
  opacity: 0.09;
  pointer-events: none;
}
/* optical size compensation: DGIST's thin ring reads small, SKKU's dense
   gingko reads large — nudge widths so all three seals look the same size */
.inst-dgist .inst-mark { width: 196px; bottom: -36px; right: -28px; }
.inst-skku .inst-mark { width: 156px; bottom: -26px; right: -20px; }

.former-name { font-weight: 500; font-size: 15.5px; }
.former-role { font-size: 14px; color: var(--muted); margin-top: 2px; }

.collab-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 0 40px; max-width: 900px; }
.collab-row { padding: 14px 0; border-top: 1px solid var(--line); }
.join-tile {
  border: 1px dashed var(--accent);
  border-radius: 16px;
  padding: 18px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 8px;
  cursor: pointer;
  min-height: 180px;
  /* standing alone (no student cards in the row to stretch against) the tile
     keeps a member card's footprint: 202x317 measured on the full roster.
     In a populated grid row the stretched height wins and this is ignored. */
  aspect-ratio: 202 / 317;
}
.join-tile:hover { background: color-mix(in oklch, var(--accent) 8%, transparent); }
.join-tile-label { font-family: var(--font-label); font-size: 13px; color: var(--accent); }
.join-tile-body { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--muted); }
/* the tile is .glass now (same material as the member cards around it); the
   dashed accent border stays as its "this one is an invitation" marker */
.join-tile.glass { border: 1px dashed color-mix(in srgb, var(--accent) 70%, transparent); }

/* PI contact pills (email + CV) */
.pi-links { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 16px; }
.plink {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-family: var(--font-label);
  font-size: 12.5px;
  padding: 7px 14px;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--fg);
  transition: border-color .25s ease, background .25s ease, color .25s ease;
}
.plink svg { width: 14px; height: 14px; }
.plink:hover { border-color: var(--accent); color: var(--accent); background: color-mix(in oklch, var(--accent) 8%, transparent); }

/* member cards are links to #/people/<slug> */
.card-member[data-person] { cursor: pointer; }
.card-member[data-person]:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
/* group tags + link icons pinned to the card foot */
.member-foot { margin-top: auto; display: flex; align-items: flex-end; justify-content: space-between; gap: 10px; }
.member-tags { display: flex; flex-wrap: wrap; gap: 6px; }
/* A topic reads as a coloured dot plus a mono label, not a filled pill: the
   same register as the site's other labels, and two topics on one card stay
   quiet. --tc (set inline per topic) is the only thing that varies between a
   card tag and its rail row, so the two can't drift apart. */
.member-tags { gap: 3px 13px; }
.member-tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-family: var(--font-label);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}
.member-tag::before {
  content: '';
  flex: none;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--tc);
}
.member-links { margin-top: 0; flex: none; }
.member-links .pub-ic { width: 26px; height: 26px; }

/* research-topic filter: a glass rail down the right of the roster grid. It
   sticks below the header while you scroll the student list, then releases
   with the section — the filter only governs current students, so it has no
   business following you into Former Members.
   (Chip colours live with the other .chip rules below, which come later in
   the file and would otherwise win on source order.) */
.people-cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(168px, 14vw, 202px);
  gap: clamp(18px, 2vw, 28px);
  align-items: start;
}
.people-cols > #peopleList { grid-column: 1; grid-row: 1; min-width: 0; }
/* the rail costs the grid a column at this width — buy it back by letting the
   cards run a little narrower, so a group of four still fills one row */
.people-cols .grid-people { grid-template-columns: repeat(auto-fill, minmax(178px, 1fr)); }
/* the track holds the column open; the panel travels inside it, eased by
   railFollow() in js/app.js (no sticky — we want damped motion, not a pin) */
.topic-rail { grid-column: 2; grid-row: 1; position: relative; align-self: stretch; }
.topic-filter {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  padding: 15px 14px 13px;
  border-radius: 14px;
}
/* stacked as an index: label on top, one row per topic */
.topic-filter .people-filter { margin-top: 0; flex-direction: column; align-items: stretch; gap: 1px; }
.topic-filter .filter-label {
  margin: 0 0 9px 8px;
  font-size: 10px;
  letter-spacing: 0.11em;
}
/* rows, not pills: a dot for the hue, a label, and a quiet head count */
.topic-filter .chip {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 6px 8px;
  border: none;
  background: none;
  border-radius: 8px;
  font-size: 11.5px;
  letter-spacing: 0.01em;
  color: var(--muted);
  text-align: left;
  transition: color .2s ease, background .2s ease;
}
.topic-filter .chip::before {
  content: '';
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--tc, transparent);
}
/* 'All' carries no topic colour — a hollow dot reads as "no filter" */
.topic-filter .chip:not(.chip--topic)::before { box-shadow: inset 0 0 0 1px currentColor; }
.topic-filter .chip-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.topic-filter .chip-n {
  flex: none;
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  opacity: 0.55;
}

/* narrow: no room for a rail — fall back to a full-width card above the list */
@media (max-width: 900px) {
  .people-cols { display: block; }
  .topic-rail { position: static; min-height: 0 !important; }
  .topic-filter { position: static; margin-bottom: 28px; padding: 13px 14px; }
  .topic-filter .people-filter { flex-direction: row; align-items: center; flex-wrap: wrap; gap: 2px 6px; }
  .topic-filter .filter-label { margin: 0 6px 0 4px; }
  .topic-filter .chip { width: auto; }
  .topic-filter .chip-label { flex: none; }
}

/* ---------- member page (#/people/<slug>) ---------- */

.person-bio { max-width: 640px; }
.person-bio + .person-bio { margin-top: 14px; }
.person-edu-label { margin-top: 30px; }
.edu-list { max-width: 640px; margin-top: 6px; }
.edu-item { padding: 11px 0; border-top: 1px solid var(--line); font-size: 14.5px; }
.edu-item:first-child { border-top: none; }
.person-side { overflow: hidden; /* clips the inst watermark like the roster cards */ }
.person-portrait { max-width: 220px; margin-bottom: 18px; }
.person-side .detail-v { text-align: right; }
.person-pubs { margin-top: 14px; }

/* ---------- publications ---------- */

/* The magnifier rides in the input's own background stack rather than a
   wrapper element: a wrapper would carry the section's riseUp transform and
   become the backdrop root, which would stop the input's own glass from
   sampling the sky behind it. Stroke is hard-coded to --muted's value because
   a data-URI SVG can't read currentColor, and this field only ever sits on
   the light inverted band. */
.search-input {
  width: 100%;
  padding: 15px 18px 15px 48px;
  border-radius: 12px;
  border: 1px solid var(--glass-brd);
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236B7385' stroke-width='1.9' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='M20 20l-3.8-3.8'/%3E%3C/svg%3E")
      no-repeat left 18px center / 17px 17px,
    var(--glass-bg);
  backdrop-filter: var(--card-blur);
  -webkit-backdrop-filter: var(--card-blur);
  box-shadow: var(--glass-sh);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: 15px;
  outline: none;
  transition: border-color .22s ease, box-shadow .4s cubic-bezier(.22, 1, .36, 1);
}
/* Focus borrows .glass:hover's vocabulary rather than drawing an outline: the
   card deepens its shadow, tints it toward the accent and lights an inner rim.
   Slot count matches --glass-sh exactly so the shadow interpolates instead of
   popping on the first focus. */
.search-input:focus {
  /* sRGB mix, not oklch: mixing the accent into a translucent white in oklch
     takes a saturated detour and the border reads as a drawn teal ring */
  border-color: color-mix(in srgb, var(--accent) 45%, var(--glass-brd));
  box-shadow:
    0 20px 46px -16px color-mix(in srgb, var(--accent) 26%, rgba(15,23,42,0.22)),
    inset 0 1px 0 rgba(255,255,255,0.95),
    inset 0 0 0 1px rgba(255,255,255,0.28),
    inset 0 -14px 30px -20px rgba(15,23,42,0.16);
}
/* The global input:focus-visible ring (see the focus-ring block below) fires on
   plain mouse clicks too — text inputs always match :focus-visible — and a hard
   2px outline offset outside the rounded card looked bolted on. This field
   carries its own focus state above, which stays plainly visible for keyboard
   users, so the shared ring stands down here only. */
.search-input:focus-visible { outline: none; }
/* Safari/Chrome decorate type=search; the field carries its own affordances */
.search-input::-webkit-search-decoration,
.search-input::-webkit-search-cancel-button { -webkit-appearance: none; }
@media (prefers-reduced-motion: reduce) { .search-input { transition: none; } }
/* Filters ride their own .glass card, stacked tight under the search field:
   bare on the band, the halftone shows through and the muted chip labels drop
   below legibility. Two cards 8px apart read as one control group while each
   keeps the site's card material intact. */
.filter-panel { margin-top: 8px; padding: 6px 20px 18px; }
/* It borrows the card material but not the card behaviour: this is a control
   surface you point INTO, so the nebula bloom and the hover shadow both stand
   down. Restating the rest shadow beats .glass:hover on source order. The news
   list card (.news-panel) is the same kind of container and shares this. */
.filter-panel::after, .news-panel::after { content: none; }
.filter-panel:hover, .news-panel:hover {
  box-shadow:
    0 30px 62px -24px transparent,
    inset 0 0 0 1px rgba(255,255,255,0.05),
    inset 0 20px 44px -30px transparent,
    var(--glass-sh);
}
.filter-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 16px; align-items: center; }
.filter-row + .filter-row { margin-top: 10px; }
.filter-label {
  font-family: var(--font-label);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  margin-right: 4px;
}
.chip {
  font-family: var(--font-label);
  font-size: 12px;
  letter-spacing: 0.03em;
  padding: 7px 13px;
  border-radius: 999px;
  cursor: pointer;
  transition: all .16s;
  border: 1px solid var(--line);
  color: var(--muted);
  background: transparent;
}
.chip.active { border-color: var(--accent); color: var(--bg); background: var(--accent); }
/* research-topic rows (People page rail). The dot carries the hue; selecting
   a row brings its ink up to the topic colour over a faint wash of the same.
   One gesture, no borders — the rail is an index, not a row of buttons. */
.people-filter .chip:hover { color: var(--fg); background: color-mix(in oklch, var(--fg) 5%, transparent); }
.people-filter .chip.active { background: color-mix(in oklch, var(--fg) 7%, transparent); color: var(--fg); }
.people-filter .chip.active .chip-n { opacity: 0.8; }
.chip--topic.active { color: var(--tc); background: color-mix(in oklch, var(--tc) 11%, transparent); }
.chip--topic.active:hover { background: color-mix(in oklch, var(--tc) 16%, transparent); color: var(--tc); }
.pub-count { font-family: var(--font-label); font-size: 12px; color: var(--muted); margin-top: 18px; }

.pub-list { display: flex; flex-direction: column; gap: 12px; }
.pub-item {
  display: flex;
  gap: clamp(12px, 2vw, 20px);
  padding: 22px;
  border-radius: 14px;
  align-items: flex-start;
}
.pub-main { flex: 1; min-width: 0; }
.pub-title {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(16px, 2vw, 19px);
  line-height: 1.32;
  margin: 0 0 7px;
  text-wrap: pretty;
}
.pub-authors { color: var(--muted); font-size: 14px; margin-bottom: 12px; }
.pub-tags { display: flex; gap: 8px; flex-wrap: wrap; }
.pub-pill {
  font-family: var(--font-label);
  font-size: 11px;
  padding: 4px 10px;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--muted);
}
.pub-pill--topic { border: 1px solid transparent; }
.no-results { text-align: center; padding: 60px 20px; color: var(--muted); font-family: var(--font-label); font-size: 14px; }

/* A band whose only content is a `.no-results` line (empty gallery, empty blog).
   Without a floor the light band collapses to one line of type and the whole
   page reads as a stub — hold it near a screenfull and centre the line in it. */
.band-empty { min-height: clamp(360px, 58vh, 620px); display: flex; align-items: center; justify-content: center; }
.band-empty .no-results { padding: 0 20px; }

/* author markers */
.pub-authors .au-me { color: var(--fg); font-weight: 700; text-decoration: underline; text-underline-offset: 2px; }
.pub-authors .au-lab { color: var(--fg); font-weight: 700; }
.au-mark { color: var(--accent); font-size: 0.72em; margin-left: 1px; }

/* count + legend row */
.pub-toprow { display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: center; justify-content: space-between; margin-top: 18px; }
.pub-toprow .pub-count { margin-top: 0; }
.pub-showrow { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; width: 100%; }
.per-select { display: inline-flex; align-items: center; gap: 8px; margin-left: auto; }
.per-select-label { font-family: var(--font-label); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }
.per-dd-wrap { position: relative; display: inline-flex; align-items: center; }
/* caret */
.per-dd-wrap::after {
  content: '';
  position: absolute; right: 11px; top: 50%;
  width: 6px; height: 6px;
  border-right: 1.5px solid var(--muted);
  border-bottom: 1.5px solid var(--muted);
  transform: translateY(-70%) rotate(45deg);
  pointer-events: none;
}
.per-dd {
  -webkit-appearance: none; -moz-appearance: none; appearance: none;
  font-family: var(--font-label);
  font-size: 12px;
  color: var(--fg);
  padding: 6px 28px 6px 12px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--glass-bg);
  backdrop-filter: var(--card-blur);
  -webkit-backdrop-filter: var(--card-blur);
  cursor: pointer;
  transition: border-color .16s;
}
.per-dd:hover { border-color: var(--accent); }
.per-dd:focus-visible { outline: none; border-color: var(--accent); }
.per-dd option { color: #1c2028; }

/* per-paper link icons (see .pub-ic below) */
.pub-links { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }

/* pagination */
.pager { display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; margin-top: 28px; }
.page-btn {
  font-family: var(--font-label);
  font-size: 13px;
  min-width: 36px;
  padding: 8px 10px;
  border-radius: 10px;
  cursor: pointer;
  transition: all .16s;
  border: 1px solid var(--line);
  color: var(--muted);
  background: transparent;
}
.page-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.page-btn.active { border-color: var(--accent); color: var(--bg); background: var(--accent); }
.page-btn:disabled { opacity: 0.35; cursor: default; }
.page-nav { font-size: 15px; line-height: 1; }

/* ---------- news & gallery ---------- */

/* mode toggle — sits in the dark hero, so it uses the hero's local glass vars */
.seg {
  display: inline-flex;
  gap: 4px;
  margin-top: 26px;
  padding: 4px;
  border-radius: 999px;
  border: 1px solid var(--glass-brd);
  background: color-mix(in oklch, var(--card) 40%, transparent);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
}
.seg-btn {
  font-family: var(--font-label);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 8px 18px;
  border-radius: 999px;
  border: none;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition: all .16s;
}
.seg-btn:hover { color: var(--fg); }
.seg-btn.active { background: var(--accent); color: var(--bg); }

/* featured row: equal photo-top cards — every photo slot is a full-width 16:9
   landscape frame (object-fit crop), text is just date + tag + a clamped
   title, so the whole row stays shallow enough to land above the fold */
.featured-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(272px, 1fr));
  gap: 14px;
  margin-bottom: 24px;
}
.feat-card { display: flex; flex-direction: column; overflow: hidden; color: var(--fg); }
.feat-media { aspect-ratio: 16 / 9; border-bottom: 1px solid var(--line); flex-shrink: 0; }
.cover-img, .article-img { width: 100%; height: 100%; object-fit: cover; display: block; }
.feat-body { padding: 14px 18px 17px; display: flex; flex-direction: column; gap: 8px; }
.feat-title {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(16px, 1.6vw, 18px);
  line-height: 1.32;
  margin: 0;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  transition: color .16s;
}
.feat-card:hover .feat-title { color: var(--accent); }
/* a lone featured story spreads photo + text side by side instead */
.featured-card { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 4fr); overflow: hidden; margin-bottom: 24px; color: var(--fg); }
.featured-media { min-height: 240px; border-right: 1px solid var(--line); }
.featured-body { padding: clamp(20px, 2.6vw, 30px); display: flex; flex-direction: column; gap: 12px; align-items: flex-start; }
.featured-flag {
  font-family: var(--font-label);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent);
}
.featured-title {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(19px, 2.3vw, 25px);
  line-height: 1.25;
  letter-spacing: -0.01em;
  margin: 0;
  text-wrap: pretty;
}
.featured-card .link-mono { margin-top: auto; padding-top: 6px; }
.featured-card:hover .link-mono { color: var(--accent); }
@media (max-width: 720px) {
  .featured-card { grid-template-columns: 1fr; }
  .featured-media { min-height: 0; aspect-ratio: 16 / 9; border-right: none; border-bottom: 1px solid var(--line); }
}

/* the full list: quiet hairline rows — titles carry the weight; hovering a
   row unfolds its per-highlight summary lines beneath the title */
/* overflow-anchor off: row expansion must never make the browser re-anchor
   the scroll position (reads as the whole page jumping). JS reserves slack
   below the list (min-height) so expansion can't move the footer either. */
/* the whole news list rides one glass card so the rows read cleanly off the
   halftone band; the card is a surface, not a target (aurora + hover shadow
   stand down above, shared with .filter-panel) */
.news-panel { padding: 4px clamp(16px, 2.4vw, 26px) 12px; }
.news-list { display: flex; flex-direction: column; align-items: stretch; justify-content: flex-start; overflow-anchor: none; }
/* year dividers inside the news card: same teal numeral + grey rules as the
   publications list, but the card already lifts the rows off the band, so the
   fog patch behind the numeral is dropped (it would smudge the frosted card) */
.news-list > .year-divider { margin: 30px 0 6px; }
.news-list > .year-divider:first-child { margin-top: 6px; }
.news-panel .year-num::before { content: none; }
.news-row {
  display: block;
  padding: 15px 6px;
  border-bottom: 1px solid var(--line);
  color: var(--fg);
}
.news-list .news-row:first-child { border-top: 1px solid var(--line); }
.news-row-head { display: flex; align-items: baseline; gap: 14px; }
/* Summary reveal — Dock-magnification model. No transitions here: JS
   (initNewsField in app.js) drives height/opacity every frame as a pure
   function of cursor distance, so the reveal can never cascade or fight
   itself. This block only sets the resting (closed) state. */
.news-row-sum {
  display: block;
  height: 0;
  opacity: 0;
  overflow: hidden;
  /* partially revealed text dissolves at the bottom instead of being cut
     mid-line; JS drives --fade toward 0 as the row reaches fully open
     (inline gradient mask — no fetch, safe under file://) */
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - var(--fade, 0px)), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - var(--fade, 0px)), transparent);
}
.news-row-sum-in { display: block; padding: 7px 0 0 72px; transform-origin: 72px top; } /* block: an inline span has scrollHeight 0, which zeroes the field's natural height; origin sits at the text start so depth-scale grows in place */
.sum-line { display: block; font-size: 13.5px; line-height: 1.55; color: var(--muted); }
.sum-line + .sum-line { margin-top: 3px; }
/* keyboard focus opens fully without the pointer field (!important beats the
   JS-written inline styles) */
.news-row:focus-visible .news-row-sum { height: auto !important; opacity: 1 !important; }
.news-row:focus-visible .news-row-sum-in { transform: none !important; }
/* reduced motion: the field loop is disabled — plain instant reveal */
@media (prefers-reduced-motion: reduce) {
  .news-row:hover .news-row-sum { height: auto; opacity: 1; }
  .news-row-sum-in { transform: none !important; }
  .news-row-title { transform: none !important; }
}
@media (max-width: 560px) {
  .news-row-sum-in { padding-left: 0; transform-origin: left top; }
}
.news-row-title {
  flex: 1;
  min-width: 0;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(15px, 1.9vw, 18px);
  line-height: 1.35;
  transition: color .16s;
  transform-origin: left center; /* field-driven Dock-style magnification */
}
.news-row .tag-pill { flex-shrink: 0; }
.news-row-arrow { color: var(--muted); opacity: 0; transform: translateX(-4px); transition: all .2s; }
.news-row:hover { color: var(--fg); }
.news-row:hover .news-row-title { color: var(--accent); }
.news-row:hover .news-row-arrow { opacity: 1; transform: none; color: var(--accent); }
@media (max-width: 560px) {
  .news-row-head { flex-wrap: wrap; gap: 8px 12px; }
  .news-row-title { flex-basis: 100%; order: 3; }
}

/* article page */
.article-back { display: inline-block; margin-bottom: 26px; color: var(--muted); }
.article-back:hover { color: var(--page); }
.article-meta { margin-bottom: 18px; }
.article-title { max-width: 820px; text-wrap: balance; }
.article-wrap { max-width: 760px; display: flex; flex-direction: column; gap: 22px; align-items: flex-start; }
.article-media {
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: 16px;
  border: 1px solid var(--line);
  overflow: hidden;
}
.article-p { margin: 0; font-size: 16px; line-height: 1.7; color: var(--fg); }
.article-more { margin-top: clamp(36px, 5vh, 56px); }

/* ?tune — dev-only motion tuner (not part of the shipped design) */
.tune-panel {
  position: fixed;
  left: 14px;
  bottom: 14px;
  z-index: 80;
  width: 250px;
  padding: 12px 14px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  background: rgba(13, 18, 28, 0.86);
  backdrop-filter: blur(18px) saturate(1.4);
  -webkit-backdrop-filter: blur(18px) saturate(1.4);
  color: #fff;
  font-family: var(--font-label);
  font-size: 11px;
}
/* feel tuner: same panel, docked above the scroll-feel switcher bottom-left */
.tune-panel--feel {
  left: clamp(16px, 2vw, 28px);
  bottom: calc(clamp(20px, 4.5vh, 48px) + env(safe-area-inset-bottom, 0px) + 64px);
}
.tune-head { letter-spacing: 0.12em; text-transform: uppercase; color: oklch(0.8 0.13 200); margin-bottom: 10px; }
.tune-row { display: grid; grid-template-columns: 82px 1fr 34px; align-items: center; gap: 8px; margin-bottom: 6px; }
.tune-name { color: oklch(0.82 0.014 255); }
.tune-row input[type="range"] { width: 100%; accent-color: oklch(0.7 0.13 200); }
.tune-row output { text-align: right; }
.tune-foot { display: flex; gap: 8px; margin-top: 10px; }
.tune-btn {
  flex: 1;
  padding: 6px 8px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.22);
  background: transparent;
  color: #fff;
  font: inherit;
  cursor: pointer;
}
.tune-btn:hover { border-color: oklch(0.7 0.13 200); color: oklch(0.8 0.13 200); }

/* gallery: instagram cadence — square tiles, tight gutters, caption scrim */
.gallery-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.gal-tile {
  position: relative;
  margin: 0;
  aspect-ratio: 1;
  border-radius: 10px;
  border: 1px solid var(--line);
  overflow: hidden;
}
.gal-img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .5s cubic-bezier(.22,1,.36,1); }
.gal-tile:hover .gal-img { transform: scale(1.04); }
.gal-cap {
  position: absolute;
  inset: auto 0 0 0;
  padding: 26px 12px 10px;
  font-size: 12.5px;
  line-height: 1.4;
  color: #fff;
  background: linear-gradient(transparent, rgba(10, 14, 22, 0.72));
  opacity: 0;
  transition: opacity .25s ease;
}
.gal-cap-date { display: block; font-family: var(--font-label); font-size: 10.5px; letter-spacing: 0.08em; color: oklch(0.82 0.014 255); margin-bottom: 2px; }
.gal-tile:hover .gal-cap, .gal-tile:focus-within .gal-cap, .gal-tile--ph .gal-cap { opacity: 1; }
/* placeholder tiles: hatch + always-visible caption so the grid reads before photos exist */
.gal-tile--ph .gal-cap { color: var(--fg); background: none; padding-bottom: 12px; }
.gal-tile--ph .gal-cap-date { color: var(--muted); }
@media (max-width: 560px) {
  .gallery-grid { gap: 4px; }
  .gal-cap { font-size: 11px; padding: 20px 8px 8px; }
}
@media (prefers-reduced-motion: reduce) {
  .gal-img, .gal-tile:hover .gal-img { transition: none; transform: none; }
}

/* ---------- blog ---------- */

.post-list { display: flex; flex-direction: column; gap: 12px; }
.post-row {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  align-items: baseline;
  padding: 22px;
  border-radius: 14px;
  flex-wrap: wrap;
}
.post-title { font-family: var(--font-display); font-weight: 500; font-size: 19px; margin: 0 0 6px; }
.post-desc { font-size: 14px; color: var(--muted); }
.post-tag {
  font-family: var(--font-label);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 5px 11px;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--muted);
}

/* ---------- join ---------- */

.join-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-stretch: var(--w-hero);
  font-size: clamp(32px, 5.5vw, 62px);
  line-height: 1.05;
  letter-spacing: -0.005em;
  margin: 0;
  text-wrap: balance;
}
.join-sub {
  max-width: 640px;
  margin: 26px 0 0;
  font-size: clamp(16px, 1.8vw, 19px);
  line-height: 1.65;
  color: var(--muted);
  text-wrap: pretty;
}
/* the join hero is a .snap-fill flexbox — stop the seg pill stretching */
.hero.snap-fill .seg { align-self: flex-start; }

/* Korean copy: Zilla Slab has no Hangul, so KO headings drop to Plex Sans KR
   (loaded alongside the other Plex faces); keep-all stops mid-word wraps */
.join-ko { word-break: keep-all; }
.join-ko .join-title, .join-ko .sec-title, .join-ko .pipe-title { font-family: var(--font-ko); letter-spacing: -0.02em; }

.sec-sub { margin: 10px 0 26px; font-size: 16px; line-height: 1.65; color: var(--muted); max-width: 620px; text-wrap: pretty; }

/* form-done state reuses the stage type ramp */
.pipe-title { font-family: var(--font-display); font-weight: 500; font-size: 20px; margin: 0; }
.pipe-body { margin: 0; font-size: 15px; line-height: 1.65; color: var(--muted); }

/* joining + selection side by side: two short three-step sequences, each of
   which wasted a full content column alone. Both columns share the .panel +
   .sel row anatomy; only the marker differs — stages wear the sky's stars
   (hollow → filled at the destination), steps wear ordinals. */
.join-duo { display: grid; grid-template-columns: 1fr 1fr; column-gap: clamp(28px, 4vw, 56px); }
/* the two diagrams share row tracks (subgrid) so blocks and connectors line
   up across the columns; without subgrid support each falls back to its own
   auto rows, which only costs the cross-column alignment */
.join-duo > .flow { display: grid; grid-row: 2 / span 5; grid-template-rows: subgrid; }
.join-duo > .flow:nth-child(3) { grid-column: 1; }
.join-duo > .flow:nth-child(4) { grid-column: 2; }
.sel-star { font-size: 14px; letter-spacing: 0; padding-top: 2px; }
@media (max-width: 940px) {
  .join-duo { grid-template-columns: 1fr; }
  .join-duo > .flow:nth-child(3), .join-duo > .flow:nth-child(4) { display: block; grid-row: auto; grid-column: 1; }
  /* single column: re-interleave to header, diagram, header, diagram */
  .join-duo > :nth-child(3) { order: 1; }
  .join-duo > :nth-child(2) { order: 2; margin-top: 44px; }
  .join-duo > :nth-child(4) { order: 3; }
}

/* one panel, one rhythm: every text block on this page sits on the same
   .panel geometry — identical width (full content column), padding, radius —
   with 18px hairline rows inside. Only the pipeline cards differ, by design. */
.panel { border-radius: 18px; padding: clamp(22px, 3.4vw, 36px) clamp(24px, 4vw, 40px); }

/* block diagram: one glass block per step, an arrow connector between
   blocks. One anatomy serves both sequences. */
.flow-block {
  border-radius: 18px;
  padding: 22px 26px;
  display: grid;
  grid-template-columns: 52px 1fr;
  gap: 4px 0;
}
.flow-link { position: relative; height: 48px; }
.flow-link::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 4px;
  bottom: 5px;
  width: 1px;
  background: var(--muted);
  opacity: 0.5;
}
.flow-link::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: 2px;
  transform: translateX(-50%);
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid var(--accent);
}
.sel-num { font-family: var(--font-label); font-size: 13px; letter-spacing: 0.08em; color: var(--accent); padding-top: 3px; }
.sel-name { margin: 0 0 6px; font-size: 16.5px; font-weight: 550; font-family: inherit; }
.join-ko .sel-name { font-family: var(--font-ko); }
.sel-line { margin: 0; font-size: 15px; line-height: 1.65; color: var(--muted); max-width: 640px; }

/* curriculum rows: same 52px-ordinal grid as selection, link on the right */
.cur-row { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.cur-row .sel-name { margin: 0; }
.cur-link { font-size: 14.5px; }

/* why efficient AI — the card is sized to land inside one viewport, so every
   band here is fixed-height by intent: three claims across, two figures side by
   side, one source line. Nothing scrolls out of view on a laptop. */
.why-panel { display: grid; gap: 20px; }
.why-src { margin: 0; padding-top: 12px; border-top: 1px solid var(--line); max-width: none; }

/* The pair reads as one instrument: same viewBox, same plot height, level
   baselines, split by a plain hairline. No glyph on it — the card carries two
   charts and one source line, and anything else here is decoration. */
.why-figs { display: grid; grid-template-columns: 1fr 1px 1fr; gap: 0 30px; align-items: start; }
.why-hinge { align-self: stretch; background: var(--line); }

.why-fig { margin: 0; display: grid; gap: 8px; min-width: 0; }
/* fixed two-line header so both charts start at the same y */
.why-fig-head { display: grid; gap: 2px; min-height: 34px; align-content: start; }
.why-fig-title { font-family: var(--font-label); font-size: 13px; letter-spacing: 0.02em; color: var(--fg); }
.why-fig-sub { font-family: var(--font-label); font-size: 11.5px; color: var(--muted); }
.wc-svg { width: 100%; height: auto; overflow: visible; font-family: var(--font-label); }
.wc-grid { stroke: var(--line); stroke-width: 1; }
.wc-tick { font-size: 11.5px; fill: var(--muted); }
.wc-line { fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.wc-dot { stroke: var(--card); stroke-width: 2; }
.wc-blabel { font-size: 12px; fill: var(--fg); }
.wc-end { font-size: 12px; fill: var(--muted); }
/* the trend's last value is the punchline — the one number set larger */
.wc-peak { font-size: 17px; font-weight: 600; fill: var(--fg); }

@media (max-width: 899px) {
  /* the fixed snap rail floats over the panel's right edge on phones, and it
     was covering the longest bar's value — hold the plots clear of it */
  .why-figs { grid-template-columns: 1fr; gap: 26px; padding-right: 54px; }
  .why-hinge { display: none; }
  .wc-tick { font-size: 15px; } .wc-end { font-size: 15px; }
  .wc-blabel { font-size: 16px; } .wc-peak { font-size: 22px; }
}

/* perks: the same 18px row rhythm, folded into two columns */
.perks { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 18px 44px; }
.perk { display: grid; grid-template-columns: 28px 1fr; }
.perk-glyph { color: var(--accent); font-size: 14px; padding-top: 2px; }
.perk .sel-name { margin-bottom: 4px; }
.perk .sel-line { max-width: none; }
@media (max-width: 719px) { .perks { grid-template-columns: 1fr; } }

/* application form — reuses the office-hours field vocabulary (.mt-*), one
   type-notch larger: Hangul at the meet page's 11/13px reads too small.
   The card is hv-none and its controls hold their rest state on hover: a form
   being filled in shouldn't shimmer (user call) */
.jf-card .btn-primary:hover {
  filter: none;
  transform: none;
  box-shadow: 0 6px 16px -12px var(--accent), inset 0 1px 0 rgba(255,255,255,0.28);
}
.jf-card .btn-ghost:hover { border-color: var(--line); }
.jf-card .jf-track:not(.is-on):hover { border-color: var(--line); background: transparent; color: var(--muted); }
.jf-card .mt-flabel { font-size: 12px; }
.jf-card .mt-input { font-size: 14.5px; padding: 11px 13px; }
.jf-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 20px; }
.jf-wide { margin-top: 14px; }
/* the Drive-sharing reminder, directly under the two link fields it applies to.
   Spans both columns so it reads as one note rather than a hint on one field —
   and it is a persistent line, not placeholder text, because a placeholder
   disappears at exactly the moment the applicant is pasting the link. */
.jf-note {
  grid-column: 1 / -1;
  margin: -4px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--muted);
}
.jf-req { color: var(--accent); }
.jf-tracks .jf-track { font-size: 12.5px; }
.jf-tracks .jf-track.is-on {
  border-color: var(--accent);
  background: color-mix(in oklch, var(--accent) 15%, transparent);
}
/* honeypot: parked off-canvas, not display:none — bots skip invisible fields */
.jf-hp { position: absolute; left: -9999px; top: auto; width: 1px; height: 1px; overflow: hidden; }
.jf-foot { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; margin-top: 22px; }
.jf-privacy { margin: 0; font-size: 13px; color: var(--muted); }
.jf-actions { display: flex; align-items: center; gap: 12px; margin-left: auto; flex-wrap: wrap; }
.jf-fail { font-size: 13px; line-height: 1.5; color: oklch(0.5 0.16 25); max-width: 320px; }
.jf-submit { padding: 12px 26px; }
.jf-done { text-align: center; padding: 34px 12px; }
.jf-done-star { font-size: 26px; color: var(--accent); margin-bottom: 10px; }
.jf-done .pipe-body { margin-top: 6px; }
@media (max-width: 719px) {
  .jf-grid { grid-template-columns: 1fr; }
  .jf-actions { margin-left: 0; width: 100%; }
  .jf-submit { flex: 1; }
}

/* FAQ: quiet hairline rows, not cards — the pipeline keeps the spotlight */
.faq-item + .faq-item { border-top: 1px solid var(--line); }
.faq-q {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  padding: 18px 2px;
  font-size: 16.5px;
  font-weight: 550;
  list-style: none;
}
.faq-q::-webkit-details-marker { display: none; }
.faq-mark { font-family: var(--font-label); font-size: 14px; color: var(--muted); transition: transform .2s ease; }
details[open] .faq-mark { transform: rotate(45deg); }
.faq-a { margin: 0 0 18px; font-size: 15.5px; line-height: 1.7; color: var(--muted); max-width: 640px; }
@media (prefers-reduced-motion: reduce) { .faq-mark { transition: none; } }

/* ---------- footer ---------- */

.site-footer {
  --fg: #eef1f6;
  --muted: oklch(0.74 0.014 255);
  --line: rgba(255, 255, 255, 0.14);
  --glass-brd: rgba(255, 255, 255, 0.14);
  color: var(--fg);
  border-top: 1px solid var(--glass-brd);
  background: var(--band-dark);
  backdrop-filter: blur(24px) saturate(1.7);
  -webkit-backdrop-filter: blur(24px) saturate(1.7);
}
.footer-top {
  max-width: 1200px;
  margin: 0 auto;
  padding: clamp(36px, 6vh, 56px) clamp(16px, 4vw, 40px);
  display: flex;
  justify-content: space-between;
  gap: 32px;
  flex-wrap: wrap;
}
.footer-brand { max-width: 320px; }
.footer-brand-row { margin-bottom: 14px; }
.footer-brand-link { display: flex; align-items: center; gap: 12px; color: inherit; }
.footer-brand-link:hover { color: inherit; }
.footer-tagline { margin: 0; font-size: 13.5px; line-height: 1.6; color: var(--muted); }
.footer-cols { display: flex; gap: clamp(28px, 5vw, 64px); flex-wrap: wrap; }
.footer-col { display: flex; flex-direction: column; gap: 10px; }
.foot-link { cursor: pointer; font-family: var(--font-label); font-size: 12.5px; color: var(--muted); }
.foot-link:hover { color: var(--accent); }
.footer-bottom { border-top: 1px solid var(--line); }
.footer-bottom-inner {
  max-width: 1200px;
  margin: 0 auto;
  padding: 18px clamp(16px, 4vw, 40px);
  display: flex;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  font-family: var(--font-label);
  font-size: 11.5px;
  color: var(--muted);
}

/* ---------- accessibility ---------- */

/* visually hidden but exposed to screen readers (skip-link target text, the
   aria-live status region) */
.sr-only {
  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: off-screen until keyboard-focused, then slides into view */
.skip-link {
  position: fixed;
  top: 12px; left: 12px;
  z-index: 100;
  padding: 10px 16px;
  border-radius: 10px;
  background: var(--accent);
  color: #fff;
  font-family: var(--font-label);
  font-size: 13px;
  transform: translateY(-160%);
  transition: transform .2s ease;
}
.skip-link:focus { transform: none; }

/* keyboard focus ring for controls that previously had none. The custom cursor
   is mouse-only, so keyboard users depend on this. :focus-visible keeps it off
   for mouse clicks, so the resting visual design is unchanged. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* #main is only a programmatic focus target (skip link + page changes); it
   should scroll into view without drawing a full-width box around the content */
#main:focus, #main:focus-visible { outline: none; }

@media (prefers-reduced-motion: reduce) {
  .skip-link { transition: none; }
}

/* ========================================================================
   Quick-dial FAB — fixed radial constellation of shortcut actions.
   Design: a detached header control. Same blended glass + fg as the header
   (JS mirrors updateHeader), accent only on hover/active (.nav-link
   recipe). Hub + satellite "stars" joined by fg-blended edge-lines.
   Wired by initQuickDial() in app.js. Sits above content (header is z 30).
   ======================================================================== */

.quickdial {
  position: fixed;
  /* sit at the content column's right margin, not jammed in the corner */
  right: clamp(20px, calc(50vw - 600px), 220px);
  /* +48px lifts the hub so the down stepper fits between it and the screen edge */
  bottom: calc(clamp(20px, 4.5vh, 48px) + env(safe-area-inset-bottom, 0px) + 48px);
  z-index: 40;
  /* always docked — visible at every scroll position */
  opacity: 1;
  transform: none;
  pointer-events: auto;
  /* glass material blended by JS (quickDialScroll) to mirror the header menu:
     dark glass over the hero, light glass over the inverted content zone — same
     t and formulas as updateHeader(). These are the resting (top-of-page)
     values; JS overrides them on scroll. --qd-fg flips white→dark for legibility. */
  --qd-fg: #fff;
  --qd-brd: rgba(255, 255, 255, 0.3);
  --qd-bg: linear-gradient(135deg, rgba(255,255,255,0.1), transparent 46%),
           color-mix(in oklch, var(--band-dark) 62%, transparent);
  --qd-sh: 0 16px 48px -18px rgba(0,0,0,0.55), inset 0 1px 0 rgba(255,255,255,0.16);
}

/* ---- the hub ---- */
.qd-hub {
  position: relative;
  z-index: 2;
  width: 60px;
  height: 60px;
  border-radius: 50%;
  overflow: hidden;            /* the BGM nebula (::after) stays inside the disc */
  display: grid;
  place-items: center;
  cursor: pointer;
  color: var(--qd-fg);
  touch-action: none;          /* dragging the hub shouldn't scroll the page */
  border: 1px solid var(--qd-brd);
  background: var(--qd-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--qd-sh);
  transition: border-color .25s ease;   /* header-pill recipe */
}
/* hover wash — the nav-link pill recipe, scaled into the circle. Sits above the
   glass fill, below the glyph, and animates transform+opacity only. */
.qd-hub::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: color-mix(in oklch, var(--accent) 10%, transparent);
  opacity: 0;
  transform: scale(0.6);
  transition: opacity .2s ease, transform .34s cubic-bezier(.22, 1, .36, 1);
}
.qd-hub:hover,
.qd-hub:focus-visible {
  outline: none;
  border-color: var(--accent);          /* header-pill hover recipe */
}
.qd-hub:hover::before,
.qd-hub:focus-visible::before { opacity: 1; transform: scale(1); }
.qd-hub-glyph {
  font-size: 23px;
  line-height: 1;
  color: var(--qd-fg);       /* blends white→ink with the glass, like header text */
  transition: transform .35s cubic-bezier(.3,.7,.3,1), color .3s ease;
}
/* open → glyph rotates toward an "×", wash holds at the .nav-link.active tint */
.quickdial.is-open .qd-hub-glyph {
  transform: rotate(45deg);
}
.quickdial.is-open .qd-hub { border-color: var(--accent); }
.quickdial.is-open .qd-hub::before {
  opacity: 1;
  transform: scale(1);
  background: color-mix(in oklch, var(--accent) 16%, transparent);
}

/* ---- scroll-feel compare panel (temporary review tool, ?feel=...) ----
   Same glass recipe as the quick-dial hub at rest. */
.feel-panel {
  position: fixed;
  left: clamp(16px, 2vw, 28px);
  bottom: calc(clamp(20px, 4.5vh, 48px) + env(safe-area-inset-bottom, 0px));
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 8px 10px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  background: linear-gradient(135deg, rgba(255,255,255,0.1), transparent 46%),
              color-mix(in oklch, var(--band-dark) 62%, transparent);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: 0 16px 48px -18px rgba(0,0,0,0.55), inset 0 1px 0 rgba(255,255,255,0.16);
}
.feel-label {
  font-family: var(--font-label);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.55);
  padding: 0 6px 0 4px;
}
.feel-btn {
  font-family: var(--font-label);
  font-size: 11px;
  letter-spacing: 0.06em;
  color: #fff;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 999px;
  padding: 5px 10px;
  cursor: pointer;
  transition: border-color .2s ease, color .2s ease;
}
.feel-btn:hover, .feel-btn:focus-visible { outline: none; border-color: rgba(255, 255, 255, 0.3); }
.feel-btn.active { color: var(--accent); border-color: var(--accent); }

/* ---- snap steppers: previous/next section, docked above and below the hub.
   Same material recipe as the hub (the --qd-* vars are blended per-button by
   quickDialScroll). Hidden while the section panel is open — the panel rises
   through the up stepper's exact spot. ---- */
.qd-steps {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;          /* pass-through anchor; the buttons re-enable */
  transition: opacity .22s ease;
}
.qd-step {
  position: absolute;
  right: 10px;                   /* centred on the hub's vertical axis */
  pointer-events: auto;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  cursor: pointer;
  color: var(--qd-fg);
  font-size: 15px;
  font-family: var(--font-label);
  border: 1px solid var(--qd-brd);
  background: var(--qd-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--qd-sh);
  transition: border-color .25s ease, opacity .2s ease;
}
.qd-step::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: color-mix(in oklch, var(--accent) 10%, transparent);
  opacity: 0;
  transform: scale(0.6);
  transition: opacity .2s ease, transform .34s cubic-bezier(.22, 1, .36, 1);
}
.qd-step:hover,
.qd-step:focus-visible {
  outline: none;
  border-color: var(--accent);
}
.qd-step:hover::before,
.qd-step:focus-visible::before { opacity: 1; transform: scale(1); }
.qd-step--up { bottom: 70px; }     /* above the hub */
.qd-step--down { bottom: -48px; }  /* below the hub */
.qd-step[disabled] { opacity: 0.35; pointer-events: none; }
.quickdial.is-open .qd-steps { opacity: 0; }
.quickdial.is-open .qd-step { pointer-events: none; }

/* .qd-glyph — the stepper arrows (shared with the old satellite nodes) */
.qd-glyph {
  font-size: 15px;
  line-height: 1;
  letter-spacing: -0.04em;
}

/* ---- BGM in the dial (js/bgm.js drives .bgm-live + the --bgm-* vars) ----
   While music plays the hub is the speaker: the .glass-hover nebula recipe,
   shrunk into the circle — three oversized oklch pools (200 teal / 265
   blue-violet / 315 violet-pink, the hover.js palette) drift across the disc
   on coprime timers and breathe independently, blurred so they read as gas,
   not blobs. On top, --bgm-hue (from js/bgm.js) cycles the whole wheel and
   --bgm-amp (kick onsets) pumps the hub like a speaker cone. transform/filter
   are var-driven, NOT transitioned, so the beat lands frame-accurate. */
@property --qnx1 { syntax: '<percentage>'; inherits: false; initial-value: 25%; }
@property --qny1 { syntax: '<percentage>'; inherits: false; initial-value: 30%; }
@property --qnx2 { syntax: '<percentage>'; inherits: false; initial-value: 75%; }
@property --qny2 { syntax: '<percentage>'; inherits: false; initial-value: 45%; }
@property --qnx3 { syntax: '<percentage>'; inherits: false; initial-value: 50%; }
@property --qny3 { syntax: '<percentage>'; inherits: false; initial-value: 80%; }
@property --qnb1 { syntax: '<number>'; inherits: false; initial-value: 1; }
@property --qnb2 { syntax: '<number>'; inherits: false; initial-value: 1; }
@property --qnb3 { syntax: '<number>'; inherits: false; initial-value: 1; }
.quickdial.bgm-live .qd-hub {
  transform: scale(calc(1 + 0.16 * var(--bgm-amp, 0)));
  /* the under-glow: differentiated from the nebula inside the glass — a single
     steady pool of light beneath the disc, no texture, trailing the nebula's
     hue cycle 90° behind, swelling on the kicks. box-shadow escapes the hub's
     overflow clip, so this is what bleeds onto the page. */
  box-shadow: var(--qd-sh),
    0 6px calc(24px + 30px * var(--bgm-amp, 0)) calc(2px + 6px * var(--bgm-amp, 0))
    oklch(0.7 0.14 calc(270 + var(--bgm-hue, 0)) / calc(0.22 + 0.4 * var(--bgm-amp, 0)));
}
.qd-hub::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;                 /* with ::before below the glyph; ::after paints above ::before */
  border-radius: inherit;
  /* pools larger than the disc with far-out falloff (transparent 70%+), so
     only their soft cores show — the sky nebulae's look, at hub scale */
  background:
    radial-gradient(120% 120% at var(--qnx1) var(--qny1),
      color-mix(in oklch, oklch(0.68 0.13 200) calc(30% * var(--qnb1, 1)), transparent), transparent 70%),
    radial-gradient(130% 130% at var(--qnx2) var(--qny2),
      color-mix(in oklch, oklch(0.66 0.13 265) calc(28% * var(--qnb2, 1)), transparent), transparent 72%),
    radial-gradient(140% 140% at var(--qnx3) var(--qny3),
      color-mix(in oklch, oklch(0.66 0.125 315) calc(26% * var(--qnb3, 1)), transparent), transparent 72%);
  filter: blur(5px)
          hue-rotate(calc(var(--bgm-hue, 0) * 1deg))
          saturate(calc(1 + 0.5 * var(--bgm-amp, 0)));
  opacity: 0;
  transition: opacity .6s ease;   /* only the on/off fade — amp/hue bypass it */
  pointer-events: none;
  /* pool drift + breathing on coprime alternate timers (the .glass::after
     motion model): never in step, never visibly repeating */
  animation:
    qnP1 13s ease-in-out infinite alternate,
    qnP2 17s ease-in-out infinite alternate,
    qnP3 19s ease-in-out infinite alternate,
    qnB1 5.3s ease-in-out infinite alternate,
    qnB2 7.1s ease-in-out infinite alternate,
    qnB3 8.7s ease-in-out infinite alternate;
  animation-play-state: paused;
}
.quickdial.bgm-live .qd-hub::after {
  opacity: 0.9;
  animation-play-state: running;
}
@keyframes qnP1 { from { --qnx1: 12%; --qny1: 18%; } to { --qnx1: 72%; --qny1: 55%; } }
@keyframes qnP2 { from { --qnx2: 85%; --qny2: 65%; } to { --qnx2: 30%; --qny2: 20%; } }
@keyframes qnP3 { from { --qnx3: 35%; --qny3: 88%; } to { --qnx3: 68%; --qny3: 30%; } }
@keyframes qnB1 { from { --qnb1: 0.55; } to { --qnb1: 1.35; } }
@keyframes qnB2 { from { --qnb2: 1.30; } to { --qnb2: 0.60; } }
@keyframes qnB3 { from { --qnb3: 0.65; } to { --qnb3: 1.25; } }
/* waves (default) — the outward expression is sonar rings on each kick instead
   of the steady halo (js/bgm.js spawns .qd-wave spans; strength/hue per ring;
   ?bgmfx=halo revisits the under-glow).
   Rings ride the wave's own kick strength (--wamp) for reach and the nebula
   hue at launch (--wh), so a hard hit travels further than a soft one. */
.quickdial[data-bgmfx="waves"].bgm-live .qd-hub { box-shadow: var(--qd-sh); }
.qd-waves {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 60px;                 /* the hub's box — rings are born at its rim */
  height: 60px;
  z-index: 1;                  /* below the hub (z 2); DOM-first below fan/nav */
  pointer-events: none;
}
.qd-wave {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 1.5px solid oklch(0.75 0.14 var(--wh, 270) / 0.55);
  box-shadow: 0 0 12px oklch(0.75 0.14 var(--wh, 270) / 0.3);
  animation: qd-wave 1.3s cubic-bezier(.16, .6, .4, 1) forwards;
}
@keyframes qd-wave {
  from { transform: scale(0.95); opacity: 0.7; }
  to   { transform: scale(calc(1.8 + 1.6 * var(--wamp, 0.5))); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .qd-wave { animation: none; display: none; }
}
/* while the dial is live, the glyph keeps its ink (--qd-fg, so it still flips
   with the zones) but takes a quiet ~20% tint of the cycling hue — inkish, not
   plain — deepening toward ~45% for a blink on each kick, with a faint glow
   that exists only on hits. Restraint over spectacle: the nebula is the show,
   the star just isn't dead. */
.quickdial.bgm-live .qd-hub-glyph {
  color: color-mix(in oklch,
    var(--qd-fg) calc(80% - 25% * var(--bgm-amp, 0)),
    oklch(0.6 0.16 calc(var(--bgm-hue, 0) + 40)));
  text-shadow: 0 0 8px
               oklch(0.7 0.15 calc(var(--bgm-hue, 0) + 40) / calc(0.6 * var(--bgm-amp, 0)));
  /* drop the base .3s colour transition so the kick tint lands frame-accurate */
  transition: transform .35s cubic-bezier(.3, .7, .3, 1);
}
/* the Music satellite shows the engine state with the .nav-link.active tint */
.quickdial.bgm-live .qd-sat[data-qd="music"] {
  border-color: var(--accent);
  color: var(--accent);
}

/* volume flyout — a bare slider on a hairline rule, left of the Music
   satellite (js/bgm.js appends it and positions it from the node's --sx/--sy).
   Shown while the fan is open and the satellite or the flyout is hovered or
   focused. The wrap itself is an INVISIBLE hover zone: its right padding
   bridges the gap to the satellite's edge so the slider can sit clear of the
   node without the cursor ever crossing a dead zone. */
.qd-volwrap {
  position: absolute;
  z-index: 3;
  transform: translateY(50%);
  display: flex;
  align-items: center;
  padding: 8px 14px 8px 0;     /* right pad = the invisible bridge to the node */
  color: var(--accent);
  opacity: 0;
  pointer-events: none;
  transition: opacity .2s ease;
}
.quickdial.is-open .qd-sat[data-qd="music"]:hover ~ .qd-volwrap,
.quickdial.is-open .qd-sat[data-qd="music"]:focus-visible ~ .qd-volwrap,
.quickdial.is-open .qd-volwrap:hover,
.quickdial.is-open .qd-volwrap:focus-within {
  opacity: 1;
  pointer-events: auto;
}
/* the Music label yields to the slider — one thing in that spot at a time */
.quickdial.is-open .qd-sat[data-qd="music"]:hover .qd-tip,
.quickdial.is-open .qd-sat[data-qd="music"]:focus-visible .qd-tip { opacity: 0; }
.qd-vol {
  -webkit-appearance: none;
  appearance: none;
  width: 54px;
  height: 14px;
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
  /* the UA sheet gives input its own non-inherited colour, so the track and
     thumb (both currentColor) would render fieldtext-black, not accent */
  color: inherit;
}
.qd-vol::-webkit-slider-runnable-track { height: 2px; border-radius: 1px; background: color-mix(in oklch, currentColor 40%, transparent); }
.qd-vol::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 10px;
  height: 10px;
  margin-top: -4px;
  border-radius: 50%;
  border: none;
  background: currentColor;
}
.qd-vol::-moz-range-track { height: 2px; border-radius: 1px; background: color-mix(in oklch, currentColor 35%, transparent); }
.qd-vol::-moz-range-thumb { width: 10px; height: 10px; border: none; border-radius: 50%; background: currentColor; }
@media (prefers-reduced-motion: reduce) {
  /* no cone-pump, no swirl, no hue drift — a calm static nebula is the signal */
  .quickdial.bgm-live .qd-hub { transform: none; }
  .qd-hub::after { filter: none; animation: none; }
  .quickdial.bgm-live .qd-hub::after { opacity: 0.45; }
}

/* ============ the page constellation (quick-dial section fan) ============
   The hub fans open into a constellation of the page: one star per titled snap
   section, rising and leaning left, chained hub → first → second → … by the
   same hairline edges sky.js draws between stars. Each star carries a floating
   label; the one you are currently in is always lit and labelled, the rest
   fade in the moment you point at the fan. Music is the odd one out on purpose
   — a glass satellite like the hub, because it is a control, not a stop.
   Built by initQuickDial() in app.js; blended by quickDialScroll(). ======== */
.qd-fan {
  position: absolute;
  right: 30px;               /* the hub's centre (60px circle) — every node and
                                edge radiates from this one origin */
  bottom: 30px;
  width: 0;
  height: 0;
  z-index: 1;
}
.qd-stars {                  /* rebuilt per page; shares the fan's origin */
  position: absolute;
  right: 0;
  bottom: 0;
  width: 0;
  height: 0;
}

/* ---- a section star: a generous hit area with a point of light in it. NOT a
   glass disc — the hub is the glass control; these are stops on the sky. ---- */
.qd-star {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 34px;
  height: 34px;
  margin: -17px;             /* centre the hit area on the anchor point */
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  /* collapsed at the hub, invisible, non-interactive */
  opacity: 0;
  transform: translate(0, 0) scale(0.4);
  pointer-events: none;
  transition: opacity .28s ease,
              transform .42s cubic-bezier(.34, 1.56, .5, 1);   /* spring-out */
  /* --sd (set by JS) staggers only the spring-out, so the chain draws itself
     outward from the hub; hover feedback stays instant */
  transition-delay: var(--sd, 0s), var(--sd, 0s);
}
.qd-star:focus-visible { outline: none; }
.qd-star-pt {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: color-mix(in oklch, var(--qd-fg) 76%, transparent);
  box-shadow: 0 0 7px color-mix(in oklch, var(--qd-fg) 40%, transparent);
  transition: width .22s ease, height .22s ease,
              background-color .22s ease, box-shadow .3s ease;
}
.qd-star:hover .qd-star-pt,
.qd-star:focus-visible .qd-star-pt {
  width: 9px;
  height: 9px;
  background: var(--accent);
  box-shadow: 0 0 11px color-mix(in oklch, var(--accent) 65%, transparent);
}
/* the section you are resting in: the fan's one bright star */
.qd-star.active .qd-star-pt {
  width: 10px;
  height: 10px;
  background: var(--accent);
  box-shadow: 0 0 0 4px color-mix(in oklch, var(--accent) 15%, transparent),
              0 0 15px color-mix(in oklch, var(--accent) 55%, transparent);
}

/* ---- Music: the original glass satellite, kept as one, because it is a
   control with a glyph rather than a place on the page ---- */
.qd-sat {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 40px;
  height: 40px;
  margin: -20px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  cursor: pointer;
  color: var(--qd-fg);
  font-size: 15px;
  font-family: var(--font-label);
  border: 1px solid var(--qd-brd);
  background: var(--qd-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--qd-sh);
  opacity: 0;
  transform: translate(0, 0) scale(0.4);
  pointer-events: none;
  transition: opacity .28s ease,
              transform .42s cubic-bezier(.34, 1.56, .5, 1),
              border-color .25s ease;
  transition-delay: var(--sd, 0s), var(--sd, 0s), 0s;
}
.qd-sat::before {            /* hover wash, same recipe as the hub */
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: color-mix(in oklch, var(--accent) 10%, transparent);
  opacity: 0;
  transform: scale(0.6);
  transition: opacity .2s ease, transform .34s cubic-bezier(.22, 1, .36, 1);
}
.qd-sat:hover,
.qd-sat:focus-visible { outline: none; border-color: var(--accent); z-index: 2; }
.qd-sat:hover::before,
.qd-sat:focus-visible::before { opacity: 1; transform: scale(1); }

/* ---- the constellation edges. Chained (hub → star → star), not spokes: the
   line traces the page bottom-to-top the way the sections run. One segment per
   gap, anchored at the previous node (JS writes --edge-y/--edge-len).
   -0.5px centres the 1px rule on the star column. Appended before the stars so
   the chain always paints behind them. ---- */
.qd-edge {
  position: absolute;
  right: -0.5px;
  bottom: var(--edge-y, 0px);
  width: 1px;
  height: var(--edge-len, 0px);
  /* fg-blended like sky.js constellation lines: white over the night sky,
     ink over the light content band — legible against both. Fades at each end
     so the chain breathes at every star instead of skewering it. */
  background: linear-gradient(to top,
              transparent,
              color-mix(in oklch, var(--qd-fg) 50%, transparent) 20%,
              color-mix(in oklch, var(--qd-fg) 50%, transparent) 80%,
              transparent);
  opacity: 0;
  transition: opacity .3s ease .04s;
  pointer-events: none;
}

/* ---- open: the chain springs out from the hub, edges fade in ---- */
.quickdial.is-open .qd-star,
.quickdial.is-open .qd-sat {
  opacity: 1;
  /* --sx defaults to 0: section stars sit on the hub's axis and only set
     --sy; the Music satellite is the one node with a horizontal offset */
  transform: translate(var(--sx, 0px), var(--sy)) scale(1);
  pointer-events: auto;
}
.quickdial.is-open .qd-edge { opacity: 1; }

/* ---- the floating labels. Bare text on the sky, no chip: a chip per star
   would put five little panels back on screen, which is the thing the
   constellation is instead of.
   Set in the site's chrome idiom — small, uppercase, wide-tracked, like
   .mono-label and .eyebrow. At body size and weight they read as page copy
   and tangle with the paragraphs they float over (the about page's thrust
   titles landed straight on its own prose). --qd-halo (JS, the inverse of
   --qd-fg) is what carries them over busy content. ---- */
.qd-tip {
  position: absolute;
  right: calc(100% - 2px);
  top: 50%;
  transform: translateY(-50%);
  white-space: nowrap;
  font-family: var(--font-label);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--qd-fg);
  text-shadow: 0 0 3px var(--qd-halo, transparent),
               0 0 7px var(--qd-halo, transparent),
               0 0 14px var(--qd-halo, transparent),
               0 0 22px var(--qd-halo, transparent);
  opacity: 0;
  pointer-events: none;
  transition: opacity .22s ease, color .22s ease;
}
/* the current section stays labelled; everything else is one glance away */
/* the 40px glass satellite needs the label clear of its rim; the 34px star is
   mostly empty hit area, so its label tucks closer to the point of light */
.qd-sat .qd-tip { right: calc(100% + 5px); }
.quickdial.is-open .qd-star.active .qd-tip { opacity: 1; }
.quickdial.is-open .qd-fan:hover .qd-tip,
.quickdial.is-open .qd-fan:focus-within .qd-tip { opacity: 1; }
.qd-star.active .qd-tip { color: var(--accent); font-weight: 600; }
.qd-star:hover .qd-tip,
.qd-star:focus-visible .qd-tip,
.qd-sat:hover .qd-tip,
.qd-sat:focus-visible .qd-tip { opacity: 1; color: var(--accent); }

/* ---- reduced motion: no spring, no stagger, no arc drift ---- */
@media (prefers-reduced-motion: reduce) {
  .quickdial,
  .qd-hub,
  .qd-hub::before,
  .qd-hub-glyph,
  .qd-star,
  .qd-star-pt,
  .qd-sat,
  .qd-sat::before,
  .qd-tip,
  .qd-edge { transition: none !important; }
  .quickdial.is-open .qd-star,
  .quickdial.is-open .qd-sat { transition-delay: 0s !important; }
}

/* ---------- publication project page (#/pub/<slug>) ---------- */

/* list-card affordance: whole row navigates; the title carries the hover cue */
.pub-item { cursor: pointer; }
.pub-title-link { color: inherit; transition: color .2s ease; }
.pub-item:hover .pub-title-link { color: var(--accent); }

.hero--project { padding-top: clamp(56px, 9vh, 112px); }
.proj-back { display: inline-block; margin-bottom: clamp(22px, 4vh, 34px); }
.proj-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(26px, 4vw, 46px);
  line-height: 1.12;
  letter-spacing: -0.02em;
  margin: 0 0 18px;
  max-width: 920px;
  text-wrap: balance;
}
.proj-authors { font-size: 15px; line-height: 1.65; max-width: 780px; margin-bottom: 0; }
.proj-tags { margin-top: 20px; }

.proj-cols { display: flex; flex-wrap: wrap; gap: clamp(24px, 3vw, 44px); align-items: flex-start; }
.proj-abs-col { flex: 1 1 460px; min-width: 0; }
.proj-abs { font-size: 15.5px; line-height: 1.75; margin: 0; max-width: 720px; text-wrap: pretty; }
.proj-abs--missing { color: var(--muted); }
.proj-kw-label { margin-top: 26px; }

.proj-side { flex: 1 1 260px; max-width: 360px; padding: 24px; }
.detail-row { display: flex; justify-content: space-between; gap: 12px; padding: 10px 0; border-top: 1px solid var(--line); font-size: 13.5px; }
.detail-row:first-of-type { border-top: none; padding-top: 0; }
.detail-k { font-family: var(--font-label); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); padding-top: 2px; flex: none; }
.detail-v { text-align: right; min-width: 0; overflow-wrap: anywhere; }
.proj-side .pub-links { margin-top: 18px; }

/* ---------- publications: numbering, dividers, icons, featured ---------- */

/* CV-style number badge replaces the year in the card gutter */
.pub-num { font-family: var(--font-label); font-size: 14px; color: var(--accent); min-width: 46px; padding-top: 2px; }

/* ---- year divider ----
   The year centred between two solid grey rules running the column's full
   width, so the divider actually divides rather than captioning the first card.

   The numeral is lifted off the halftone band by a white bloom — stacked
   text-shadows fading out from the glyph edges — not by a plate or a filled
   box. Nothing is filled, so there is no rectangle and no hard edge; the dots
   simply thin out as they approach the letterforms. Four of the six stops are
   fully opaque, which is what it takes to clear a dot field this dense. The
   numeral is raised a layer so its bloom also washes over the rule tips
   reaching it, and the rules read as dissolving into the year. */

.year-group { display: flex; flex-direction: column; gap: 10px; }
.year-divider { display: flex; align-items: center; margin: 40px 0 10px; gap: 14px; }
.pub-list > .year-group:first-child .year-divider { margin-top: 0; }
.year-rule { flex: 1; height: 1px; background: color-mix(in oklch, var(--fg) 20%, transparent); }
/* The numeral sits on a feathered fog patch, not a bloom and not a hard plate.
   A text-shadow can only paint where the glyph strokes are, so it can never
   fill the gaps around the digits to block the band — that is why shrinking it
   plateaued. Instead ::before lays a solid off-white ellipse behind the number
   whose edge is feathered by a radial-gradient, so it blocks the halftone
   fully yet has no visible rectangle. --year-fog is the sampled band base, so
   the patch reads as the band thickened rather than a bright spot.

   A light same-colour text-shadow softens the last few px into the band. */
.year-num {
  --year-fog: #ebecea;
  position: relative;
  z-index: 1; /* patch + numeral paint over both rules, not under them */
  font-family: var(--font-label);
  font-size: 14px;
  font-weight: 700;
  color: var(--accent);
  letter-spacing: 0.18em;
  /* letter-spacing hangs a trailing space off the last glyph; the indent pushes
     the numeral back so it sits optically centred between the rules */
  text-indent: 0.18em;
  text-shadow: 0 0 5px var(--year-fog), 0 0 10px var(--year-fog);
}
.year-num::before {
  content: '';
  position: absolute;
  z-index: -1; /* behind the digits, above the fog's own text-shadow feather */
  /* reach tuned to 0.85 in the review round */
  inset: calc(-0.28em - 0.16em * 0.85) calc(-0.5em - 0.85em * 0.85);
  border-radius: 999px;
  /* solid core to ~50%, then a soft fade — the feather is what keeps it from
     reading as a pill */
  background: radial-gradient(closest-side,
    var(--year-fog) 0 46%,
    color-mix(in srgb, var(--year-fog) 62%, transparent) 72%,
    transparent 100%);
}

.pub-list { gap: 10px; }
.pub-list .pub-item { margin-top: 2px; }

/* round icon-only link buttons (tooltip carries the word) */
.pub-ic {
  position: relative;   /* anchors the ::after tooltip to the icon */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--muted);
  background: transparent;
  cursor: pointer;
  transition: all .16s;
}
.pub-ic svg { width: 14px; height: 14px; display: block; }
.pub-ic .ic-tx { font-family: var(--font-label); font-size: 11.5px; line-height: 1; }
.pub-ic:hover { border-color: var(--accent); color: var(--accent); }
.pub-ic--paper { border-color: color-mix(in oklch, var(--accent) 45%, transparent); color: var(--accent); }
.pub-ic--paper:hover { background: color-mix(in oklch, var(--accent) 14%, transparent); }
.pub-ic.is-copied { border-color: var(--accent); color: var(--accent); }

/* styled tooltips: aria-label surfaces instantly as a dark mono pill above the
   icon (replaces the sluggish OS title tooltip). Sample-card icons carry no
   aria-label, so they stay caption-only. */
.pub-ic[aria-label]::after {
  content: attr(aria-label);
  position: absolute;
  bottom: calc(100% + 9px);
  left: 50%;
  transform: translateX(-50%) translateY(3px);
  padding: 5px 10px;
  border-radius: 7px;
  background: color-mix(in oklch, var(--band-dark) 94%, transparent);
  color: #fff;
  font-family: var(--font-label);
  font-size: 10.5px;
  letter-spacing: 0.05em;
  white-space: nowrap;
  box-shadow: 0 8px 22px -10px rgba(0, 0, 0, 0.5);
  opacity: 0;
  pointer-events: none;
  transition: opacity .14s ease .05s, transform .18s cubic-bezier(.22, 1, .36, 1) .05s;
  z-index: 6;
}
.pub-ic[aria-label]::before {
  content: '';
  position: absolute;
  bottom: calc(100% + 4px);
  left: 50%;
  transform: translateX(-50%);
  border: 5px solid transparent;
  border-top-color: color-mix(in oklch, var(--band-dark) 94%, transparent);
  opacity: 0;
  pointer-events: none;
  transition: opacity .14s ease .05s;
  z-index: 6;
}
.pub-ic[aria-label]:hover::after, .pub-ic[aria-label]:focus-visible::after { opacity: 1; transform: translateX(-50%) translateY(0); }
.pub-ic[aria-label]:hover::before, .pub-ic[aria-label]:focus-visible::before { opacity: 1; }
/* the BibTeX header sits at the clipped top edge of its card — flip below */
.bib-head .pub-ic[aria-label]::after { bottom: auto; top: calc(100% + 9px); transform: translateX(-50%) translateY(-3px); }
.bib-head .pub-ic[aria-label]::before { bottom: auto; top: calc(100% + 4px); border-top-color: transparent; border-bottom-color: color-mix(in oklch, var(--band-dark) 94%, transparent); }
.bib-head .pub-ic[aria-label]:hover::after { transform: translateX(-50%) translateY(0); }

/* featured papers — the news lead-card grammar; the cover is a topic-hued
   nebula (--fc set inline) with the venue as its wordmark. Multiple flagged
   papers stack in one grid cell and crossfade (JS rotates .active). */
.feat-wrap { position: relative; }
/* side arrows: bare pager glyphs in the margin beside the card — no chrome,
   just type. Hover leans them toward the direction of travel. Hidden on
   narrow screens (the dots carry navigation there). */
.feat-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  border: none;
  background: none;
  padding: 6px 10px;
  color: var(--muted);
  font-family: var(--font-label);
  font-size: 30px;
  line-height: 1;
  cursor: pointer;
  transition: color .16s ease, transform .2s ease;
}
.feat-arrow--prev { left: -46px; }
.feat-arrow--next { right: -46px; }
.feat-arrow:hover { color: var(--accent); }
.feat-arrow--prev:hover { transform: translateY(-50%) translateX(-3px); }
.feat-arrow--next:hover { transform: translateY(-50%) translateX(3px); }
.feat-arrow:active { transform: translateY(-50%) scale(0.9); }
@media (max-width: 1180px) { .feat-arrow { display: none; } }
.pub-feat-stack { display: grid; }
/* slides are the stack's direct children — a featured paper, wide on the pub
   page and tall on home. The transition must restate .glass's
   transform/box-shadow curves — a bare `transition: opacity` would override
   them and kill the hover lift */
.pub-feat-stack > * {
  grid-area: 1 / 1;
  margin-bottom: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity .7s ease,
              transform .5s cubic-bezier(.22, 1, .36, 1),
              box-shadow .5s cubic-bezier(.22, 1, .36, 1);
}
.pub-feat-stack > .active { opacity: 1; pointer-events: auto; }
.feat-dots { display: flex; justify-content: center; gap: 9px; padding: 14px 0 2px; margin-bottom: 8px; }
.feat-dot {
  width: 8px;
  height: 8px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--muted);
  background: transparent;
  cursor: pointer;
  transition: background .2s ease, border-color .2s ease, transform .2s ease;
}
.feat-dot:hover { border-color: var(--accent); transform: scale(1.25); }
.feat-dot.active { background: var(--accent); border-color: var(--accent); }
@media (prefers-reduced-motion: reduce) { .pub-feat-stack > * { transition: none; } }

.pub-feat { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); overflow: hidden; margin-bottom: 22px; color: var(--fg); }
.pub-feat-cover {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 200px;
  border-right: 1px solid var(--line);
  background:
    radial-gradient(120% 70% at 50% -12%, color-mix(in oklch, var(--fc) 34%, transparent), transparent 70%),
    radial-gradient(90% 60% at 12% 110%, color-mix(in oklch, var(--fc) 16%, transparent), transparent 70%);
}
.pub-feat-venue { font-family: var(--font-display); font-weight: 600; font-stretch: var(--w-sec); font-size: clamp(28px, 3.6vw, 44px); letter-spacing: -0.01em; color: var(--fc); }
.pub-feat-yr { font-family: var(--font-label); font-size: 12.5px; letter-spacing: 0.08em; color: var(--muted); }
.pub-feat-body { padding: clamp(20px, 2.6vw, 30px); display: flex; flex-direction: column; gap: 10px; align-items: flex-start; }
.featured-flag {
  font-family: var(--font-label);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
}
.pub-feat-title { font-family: var(--font-display); font-weight: 500; font-size: clamp(18px, 2.2vw, 23px); line-height: 1.3; margin: 0; text-wrap: pretty; }
.pub-feat-abs { margin: 0; font-size: 13.5px; line-height: 1.6; color: var(--muted); }
.pub-feat .link-mono { margin-top: auto; padding-top: 4px; }
.pub-feat:hover .link-mono, .pub-feat:hover .pub-feat-title { color: var(--accent); }
@media (max-width: 720px) {
  .pub-feat { grid-template-columns: 1fr; }
  .pub-feat-cover { min-height: 120px; border-right: none; border-bottom: 1px solid var(--line); }
}
/* home gallery slides: a real photo as the cover, or the photo-pending hatch */
.pub-feat-cover--photo { position: relative; }
.pub-feat-cover--photo .gal-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.pub-feat-cover--ph { background: repeating-linear-gradient(135deg, var(--bg2), var(--bg2) 9px, var(--card) 9px, var(--card) 18px); }


/* record overview: quiet cells in the HERO — the dark-local vars (--fg white,
   --muted light, --line dim) style it for the night sky automatically */
.pub-stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px 22px; margin-top: 34px; max-width: 880px; }
.pub-stat { display: flex; align-items: baseline; gap: 9px; padding: 12px 0 0; border-top: 1px solid var(--line); }
.pub-stat-k { font-family: var(--font-display); font-weight: 600; font-stretch: var(--w-sec); font-size: clamp(24px, 2.6vw, 32px); letter-spacing: -0.01em; color: var(--fg); }
.pub-stat-v { font-family: var(--font-label); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }
/* citations cell links to the Scholar profile */
.pub-stat--link { color: inherit; transition: border-color .2s ease; }
.pub-stat--link:hover { border-top-color: var(--accent); color: inherit; }
.pub-stat--link:hover .pub-stat-k, .pub-stat--link:hover .pub-stat-v { color: var(--accent); }


.bib-card { max-width: 860px; overflow: hidden; }
.bib-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  padding: 12px 18px;
  border-bottom: 1px solid var(--line);
  font-family: var(--font-label);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.bib-pre {
  margin: 0;
  padding: 18px 20px;
  font-family: var(--font-mono);
  font-size: 12.5px;
  line-height: 1.65;
  overflow-x: auto;
  color: var(--fg);
}

/* ---------- sub-page transition (same-tab hop: pub ↔ project) ---------- */

/* Same-tab navigation stays under the same stars — same hue, same nav pill —
   so it trades the full arrival (hero pageFade + staggered riseUp) for one
   quiet focal gesture: the hero band keeps its light while its text swaps
   with a close fade, and content makes a single unified rise. Content
   keyframes stay transform-only (see the pageFade note at the top of this
   file); hero children sit outside .invert-wrap, so a filled opacity
   animation there cannot block the glass backdrop sampling. The reduced-
   motion block above already covers these elements. */

@keyframes subRise { from { transform: translateY(12px); } to { transform: none; } }
@keyframes subHeroSwap { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

main[data-subnav] .hero { animation: none; }
main[data-subnav] section > * { animation: subRise .5s cubic-bezier(.22, 1, .36, 1) both; animation-delay: 0s; }
/* after the rule above so it wins the equal-specificity tie for hero children */
main[data-subnav] .hero > * { animation: subHeroSwap .38s ease both; }

/* News↔gallery toggle: the hero (title + seg control) is byte-identical across
   the two modes, so it stays perfectly still — only the content below rises
   once. The hero rule sits after the content rule to win the tie and cancel the
   riseUp/fadeIn arrival on the hero's children. */
main[data-modeswap] section > * { animation: subRise .5s cubic-bezier(.22, 1, .36, 1) both; animation-delay: 0s; }
main[data-modeswap] .hero, main[data-modeswap] .hero > * { animation: none; }

/* ---------- office hours (prof page, js/meet.js) ---------- */

/* Timetable in the site's vernacular: mono ruler + day heads, free slots as
   quiet accent bars that glint a ✦ on hover (stars = the meaning system),
   busy time as the fine hatch that already means "occupied" elsewhere. */

.meet-intro { max-width: 560px; color: var(--muted); font-size: 14.5px; line-height: 1.6; margin: 0 0 22px; }
.meet-card { padding: 22px 24px 24px; max-width: 1080px; }
.meet-card::after { content: none; }
.meet-card:hover {
  box-shadow:
    0 30px 62px -24px transparent,
    inset 0 0 0 1px rgba(255,255,255,0.05),
    inset 0 20px 44px -30px transparent,
    var(--glass-sh);
}
.meet-body { display: grid; grid-template-columns: minmax(0, 1fr) 384px; gap: 28px; align-items: stretch; }
.meet-side { border-left: 1px solid var(--line); padding-left: 28px; position: relative; }
.mt-hint {
  height: 100%; min-height: 260px; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 14px; text-align: center;
  font-family: var(--font-label); font-size: 11px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--muted); line-height: 2;
}
.mt-hint[hidden] { display: none; }
.mt-hint-star { font-size: 18px; color: var(--accent); }
.meet-form:not([hidden]) { animation: mtForm .25s ease; }
@keyframes mtForm { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .meet-form:not([hidden]) { animation: none; } }
.meet-card--off { padding: 26px 28px; }
.mt-off { margin: 0; font-size: 15px; color: var(--muted); }
.mt-loading { font-family: var(--font-label); font-size: 12.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); padding: 26px 4px; }

.meet-top {
  display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
  font-family: var(--font-label); font-size: 11.5px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--muted); margin-bottom: 18px;
}
.mt-leg { display: inline-flex; align-items: center; gap: 7px; }
.mt-dot { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
.mt-dot--free {
  background: color-mix(in oklch, var(--accent) 16%, transparent);
  border: 1px solid color-mix(in oklch, var(--accent) 55%, var(--line));
}
.mt-dot--busy {
  background: repeating-linear-gradient(135deg, var(--line), var(--line) 2px, transparent 2px, transparent 5px);
  opacity: 0.8;
}
.mt-tzwrap { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
.mt-tzlab { white-space: nowrap; }
.mt-tzsel {
  font-family: var(--font-label); font-size: 11.5px; letter-spacing: 0.04em;
  color: var(--fg); background: transparent; border: 1px solid var(--line);
  border-radius: 8px; padding: 5px 8px; max-width: 230px;
  transition: border-color .18s ease;
}
.mt-tzsel:hover, .mt-tzsel:focus { border-color: var(--accent); outline: none; }
.mt-tzsel { width: 240px; text-transform: none; }
.mt-tzsel.is-missing { border-color: oklch(0.55 0.16 25); }
.mt-tzsel option { background: var(--bg2); color: var(--fg); text-transform: none; }
.mt-pager { margin-left: auto; display: inline-flex; align-items: center; gap: 10px; }
.mt-pager button {
  width: 26px; height: 26px; border-radius: 8px; border: 1px solid var(--line);
  background: none; color: var(--fg); font: 15px/1 var(--font-label); padding: 0;
  transition: border-color .18s ease, opacity .18s ease;
}
.mt-pager button:hover:not(:disabled) { border-color: var(--accent); }
.mt-pager button:disabled { opacity: 0.35; }
.mt-week { color: var(--fg); letter-spacing: 0.1em; min-width: 110px; text-align: center; }

.meet-grid { display: grid; grid-template-columns: 46px repeat(7, 1fr); gap: 0 6px; position: relative; }
/* hour rules across the card so far columns stay readable; --mtTop is set by
   meet.js to the first hour-labelled row (the ruler renders in the viewer's
   timezone, so hours may sit on half-slot boundaries) */
.meet-grid::before {
  content: ''; position: absolute; left: 2px; right: 2px;
  top: var(--mtTop, 31px); bottom: 1px; pointer-events: none;
  background: repeating-linear-gradient(to bottom,
    color-mix(in oklch, var(--fg) 14%, transparent) 0 1px, transparent 1px 44px);
}
.mt-ruler, .mt-day { display: flex; flex-direction: column; gap: 3px; position: relative; }
.mt-ruler .mt-h { text-align: center; transition: color .15s ease; }
.mt-h.is-hl { color: var(--fg); font-weight: 600; }
/* hover crosshair: one soft band across the hovered row (positioned by JS) */
.mt-rowhl {
  position: absolute; left: 2px; right: 2px; height: 21px;
  border-radius: 6px; pointer-events: none;
  background: color-mix(in oklch, var(--accent) 9%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--accent) 18%, transparent);
}
.mt-dhead {
  height: 30px; display: flex; align-items: baseline; gap: 6px; justify-content: center;
  font-family: var(--font-label); font-size: 10.5px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--muted);
}
.mt-dhead b { color: var(--fg); font-size: 12.5px; font-weight: 500; }
.mt-dhead--past, .mt-dhead--past b { color: color-mix(in oklch, var(--muted) 45%, transparent); }
.mt-dhead--today, .mt-dhead--today b { color: var(--accent); }
/* now line: the one warm mark on the card, same red family as form errors */
.mt-now { position: absolute; left: 2px; right: 2px; z-index: 4; height: 0; border-top: 2px solid oklch(0.6 0.17 25 / 0.8); pointer-events: none; }
.mt-now-dot { position: absolute; left: -2px; top: -5px; width: 8px; height: 8px; border-radius: 50%; background: oklch(0.6 0.17 25); }
.mt-h { height: 19px; font-family: var(--font-label); font-size: 10.5px; color: var(--muted); }
.mt-cell, .mt-slot { height: 19px; border-radius: 6px; user-select: none; -webkit-user-select: none; }
.mt-slot { touch-action: none; }
.mt-slot {
  display: block; width: 100%; padding: 0; position: relative;
  background: color-mix(in oklch, var(--accent) 13%, transparent);
  border: 1px solid color-mix(in oklch, var(--accent) 50%, var(--line));
  transition: background .22s ease, border-color .22s ease;
}
/* the one gesture: hover/selection brightens the bar and a small star glints */
.mt-slot::after {
  content: '✦'; position: absolute; inset: 0; display: grid; place-items: center;
  font-size: 9px; line-height: 1; color: var(--accent);
  opacity: 0; transition: opacity .22s ease;
}
.mt-slot:hover { background: color-mix(in oklch, var(--accent) 17%, transparent); border-color: var(--accent); }
.mt-slot:hover::after, .mt-slot.is-sel::after { opacity: 1; }
.mt-slot.is-sel { background: color-mix(in oklch, var(--accent) 24%, transparent); border-color: var(--accent); }
.mt-busy {
  background: repeating-linear-gradient(135deg, var(--line), var(--line) 2.5px, transparent 2.5px, transparent 6px);
  opacity: 0.85;
}
.mt-past { background: var(--line); opacity: 0.35; }
@media (prefers-reduced-motion: reduce) {
  .mt-slot, .mt-slot::after, .mt-pager button { transition: none; }
}

/* mobile: per-day chips of free times instead of the grid */
.meet-list { display: none; }
.mt-lday { padding: 10px 0; border-top: 1px solid var(--line); }
.mt-lday:first-child { border-top: none; }
.mt-lday .mt-dhead { height: auto; margin-bottom: 8px; }
.mt-lrow { display: flex; flex-wrap: wrap; gap: 8px; }
.mt-chip {
  font-family: var(--font-label); font-size: 12px; padding: 7px 14px;
  border-radius: 999px; color: var(--fg);
  background: color-mix(in oklch, var(--accent) 8%, transparent);
  border: 1px solid color-mix(in oklch, var(--accent) 30%, var(--line));
  transition: background .22s ease, border-color .22s ease;
}
.mt-chip:hover, .mt-chip.is-sel { background: color-mix(in oklch, var(--accent) 20%, transparent); border-color: var(--accent); }
.mt-lnone { font-family: var(--font-label); font-size: 12px; color: var(--muted); }
.mt-empty { margin-top: 14px; }
@media (max-width: 719px) {
  .meet-grid { display: none; }
  .meet-list { display: block; }
  .mt-pager { margin-left: 0; }
}

/* request form: fixed rail on the calendar's right (stacks below on mobile) */
.mt-when {
  padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
  background: color-mix(in oklch, var(--card) 55%, transparent);
  font-family: var(--font-label); font-size: 12px; letter-spacing: 0.03em; color: var(--fg);
}
.mt-seg { display: flex; gap: 8px; }
.mt-seg-btn {
  flex: 1; padding: 9px 0; border-radius: 10px; border: 1px solid var(--line);
  background: transparent; color: var(--muted);
  font-family: var(--font-label); font-size: 11.5px; letter-spacing: 0.06em; text-transform: uppercase;
  transition: border-color .18s ease, background .18s ease, color .18s ease;
}
/* semantic hues: green = in person on site, violet = remote/virtual —
   both clearly apart from the teal slot language */
.mt-seg-btn.is-on { color: var(--fg); font-weight: 600; }
.mt-seg-btn[data-mode="off"].is-on {
  border-color: oklch(0.55 0.12 150);
  background: color-mix(in oklch, oklch(0.6 0.13 150) 18%, transparent);
}
.mt-seg-btn[data-mode="on"].is-on {
  border-color: oklch(0.55 0.12 265);
  background: color-mix(in oklch, oklch(0.6 0.13 265) 18%, transparent);
}
.mt-fields { display: grid; grid-template-columns: 1fr; gap: 13px; }
.mt-field { display: flex; flex-direction: column; gap: 6px; }
.mt-area { resize: vertical; min-height: 132px; line-height: 1.55; }
.mt-count { align-self: flex-end; font-family: var(--font-label); font-size: 10.5px; color: var(--muted); margin-top: -2px; }
.mt-flabel { font-family: var(--font-label); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
.mt-input {
  padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
  background: color-mix(in oklch, var(--card) 55%, transparent);
  color: var(--fg); font-family: var(--font-body); font-size: 13px; outline: none;
  transition: border-color .18s ease;
}
.mt-input:focus { border-color: var(--accent); }
.mt-input.is-missing { border-color: oklch(0.55 0.16 25); }
.mt-actions { display: flex; gap: 8px; margin-top: 16px; }
.meet-form .btn-primary { padding: 12px 16px; font-size: 12px; justify-content: center; flex: 1; }
.mt-red { background: oklch(0.55 0.15 25); box-shadow: 0 6px 16px -12px oklch(0.55 0.15 25), inset 0 1px 0 rgba(255,255,255,0.22); }
.mt-note { margin: 14px 0 0; font-size: 12.5px; line-height: 1.6; color: var(--muted); }
@media (max-width: 719px) {
  .meet-body { grid-template-columns: 1fr; gap: 0; }
  .meet-side { border-left: none; border-top: 1px solid var(--line); padding: 18px 0 0; margin-top: 16px; }
  .mt-hint { display: none; }
  .meet-side:has(.meet-form[hidden]) { display: none; }
}

/* ---- career timeline (hero trajectory) ----
   A spine with a star per role. Brightness/size follow --rank (1 newest → 0
   oldest); the current post (year still open) gets a filled glowing star, the
   rest are quieter points. The star + spine tie the record to the night sky. */
.tl { display: flex; flex-direction: column; }
.tl-row {
  display: grid;
  grid-template-columns: clamp(64px, 8vw, 82px) 26px 1fr;
  align-items: start;
  column-gap: 6px;
}
.tl-year {
  font-family: var(--font-label);
  font-size: 10.5px;
  line-height: 1.5;
  color: var(--accent-ink, var(--accent));
  text-align: right;
  padding-top: 14px;
  white-space: nowrap;
}
/* the node column carries the continuous spine (a centered hairline) and the
   star sits on top of it */
.tl-node {
  position: relative;
  align-self: stretch;
  min-height: 44px;
}
.tl-node::before {              /* spine segment through this row */
  content: '';
  position: absolute;
  left: 50%; top: 0; bottom: 0;
  width: 1px; transform: translateX(-50%);
  background: var(--line);
}
.tl-row:first-child .tl-node::before { top: 20px; }   /* start spine at first star */
.tl-row:last-child .tl-node::before { bottom: calc(100% - 20px); } /* stop at last star */
.tl-node::after {               /* the star */
  content: '';
  position: absolute;
  left: 50%; top: 20px;
  width: calc(4px + 4px * var(--rank, 0));
  height: calc(4px + 4px * var(--rank, 0));
  border-radius: 50%;
  transform: translate(-50%, -50%);
  background: color-mix(in oklch, var(--accent-ink, var(--accent)) calc(45% + 55% * var(--rank, 0)), var(--fg));
  opacity: calc(0.5 + 0.5 * var(--rank, 0));
}
/* current post: a filled star with a soft halo */
.tl-row--now .tl-node::after {
  width: 11px; height: 11px;
  background: var(--accent-ink, var(--accent));
  opacity: 1;
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--accent-ink, var(--accent)) 22%, transparent),
              0 0 14px 2px color-mix(in oklch, var(--accent-ink, var(--accent)) 55%, transparent);
}
.tl-body { padding: 8px 0 14px; min-width: 0; }
.tl-row--now .pc-t { color: var(--fg); }
@media (max-width: 560px) {
  .tl-row { grid-template-columns: 56px 22px 1fr; }
}

/* revealed email — an overlay chip inside the icon row. It slides out of the
   envelope over the other icons and collapses back; the layout below never
   moves. The chip is itself the mailto. */
.cv-email-chip {
  position: absolute;
  left: 56px;                     /* clears the envelope, which stays the toggle */
  right: 0;
  top: 50%;
  height: 46px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 0 13px;
  border: 1px solid color-mix(in oklch, var(--accent-ink, var(--accent)) 45%, var(--line));
  border-radius: 10px;
  background: color-mix(in oklch, var(--accent-ink, var(--accent)) 9%, transparent);
  white-space: nowrap;
  overflow: hidden;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(-50%) translateX(-12px);
  transition: opacity .24s ease, transform .24s ease, visibility 0s linear .24s;
}
.email-open .cv-email-chip {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translateY(-50%) translateX(0);
  transition: opacity .24s ease, transform .24s ease;
}
.cv-email-chip:hover { background: color-mix(in oklch, var(--accent-ink, var(--accent)) 16%, transparent); }
.cv-email-addr {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  font-family: var(--font-mono);
  font-size: 12.5px;
  color: var(--fg);
  letter-spacing: 0.01em;
}
/* a lone ✓ — the rail chip is ~196px and the address takes most of it; the
   words live in the sr-only status */
.cv-email-hint {
  flex: none;
  font-family: var(--font-label);
  font-size: 13px;
  color: var(--accent-ink, var(--accent));
}
@media (prefers-reduced-motion: reduce) {
  .cv-email-chip, .email-open .cv-email-chip, .cv-ico { transition: none; }
}
