/* 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; }

/* 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 */
.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); }

.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 ---------- */

/* recent-updates: featured-paper carousel with the update list as its
   navigation — main card left (vertical: cover on top, story below), the
   list right; the rotation walks the list */
/* the card is the protagonist: it takes the leftover width (~1.75:1),
   while the rail column is fixed at what its one-line rows actually need —
   no dead zone trailing the titles, and the pair spans the full band */
.upd-duo { display: grid; grid-template-columns: minmax(0, 1fr) 380px; align-items: stretch; overflow: hidden; }
@media (max-width: 1100px) { .upd-duo { grid-template-columns: minmax(0, 1fr) 330px; } }
/* vertical slide: the photo/cover claims the larger share of the card */
.upd-duo .pub-feat { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) auto; }
.upd-duo .pub-feat-cover { position: relative; border-right: none; border-bottom: 1px solid var(--line); min-height: 300px; }
.upd-duo .pub-feat-body { padding: 18px 22px 20px; gap: 8px; }
/* the tag wordmark speaks more quietly than a venue would */
.upd-duo .pub-feat-venue { font-size: clamp(22px, 2.4vw, 30px); }
.upd-duo .pub-feat-abs {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  font-size: 13px;
}
/* the item's constellation, low in the cover's corner */
.upd-constellation { position: absolute; right: 18px; bottom: 14px; width: 120px; height: 80px; opacity: 0.5; }
/* the story box keeps one fixed height across slides so the card frame
   never shifts as titles and bodies change length; the link rides the
   existing margin-top:auto pin */
.upd-duo .pub-feat-body { height: 134px; padding: 16px 22px 16px; overflow: hidden; }
/* one-line title keeps the box shallow; the rail carries the full text */
.upd-duo .pub-feat-title {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 1;
  overflow: hidden;
}
/* the flag carries the date in the vertical card — the cover keeps only its wordmark */
.upd-duo .pub-feat-cover .pub-feat-yr { display: none; }

/* the list is a constellation rail: one hairline threading a star per
   update — the active star fills with its tag hue and lights its row */
.upd-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 */
.upd-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);
}
.upd-duo:hover::after { opacity: 0; animation-play-state: paused; }
.upd-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 */
.upd-row { transition: transform .5s cubic-bezier(.22, 1, .36, 1); }
.upd-row:hover { transform: translateY(-2px); }
@media (prefers-reduced-motion: reduce) { .upd-row:hover { transform: none; } }
.upd-row-node { grid-column: 1; grid-row: 1 / span 2; position: relative; }
.upd-row-node::before {
  content: '';
  position: absolute;
  left: 8px;
  top: -9px;
  bottom: -9px;
  width: 1px;
  background: var(--line);
}
.upd-row:first-child .upd-row-node::before { top: 15px; }
.upd-row:last-child .upd-row-node::before { bottom: auto; height: 24px; }
.upd-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;
}
.upd-row:hover .upd-row-node::after { border-color: var(--fc); }
.upd-row.active .upd-row-node::after {
  background: var(--fc);
  border-color: var(--fc);
  box-shadow: 0 0 9px color-mix(in oklch, var(--fc) 60%, transparent);
}
.upd-row-meta { grid-column: 2; display: flex; gap: 10px; align-items: baseline; }
.upd-row-date { font-family: var(--font-label); font-size: 11px; letter-spacing: 0.05em; color: var(--muted); transition: color .25s ease; }
.upd-row-tag {
  font-family: var(--font-label);
  font-size: 9.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fc);
  opacity: 0.75;
}
.upd-row-title { grid-column: 2; font-size: 13.5px; line-height: 1.4; color: var(--muted); transition: color .25s ease; }
.upd-row:hover .upd-row-title { color: var(--fg); }
.upd-row.active .upd-row-date { color: var(--fc); }
.upd-row.active .upd-row-title { color: var(--fg); }
@media (max-width: 860px) {
  .upd-duo { grid-template-columns: 1fr; }
  .upd-list { border-left: none; border-top: 1px solid var(--line); }
}
.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); }

/* ---------- 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; }
}
.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; }


/* ---------- 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 */
.cv-links--hero { border: 0; padding: 0; margin: 0; }

@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; }

.people-group { margin-bottom: 36px; }
.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;
  margin-top: 10px;
  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; }

/* 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;
}
.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; }

/* 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; }

/* pipeline: three stages strung on one hairline — the same star-to-star
   language the sky draws, one node per stage, filled star at the destination */
.pipe { position: relative; display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; padding-top: 40px; }
.pipe::before {
  content: '';
  position: absolute;
  top: 11px;
  left: 16.67%;
  right: 16.67%;
  height: 1px;
  background: var(--line);
}
.pipe-stage { position: relative; padding: 26px; display: flex; flex-direction: column; gap: 10px; }
.pipe-node {
  position: absolute;
  top: -40px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 15px;
  line-height: 22px;
  color: var(--accent);
}
.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); }
@media (max-width: 719px) {
  .pipe { grid-template-columns: 1fr; padding-top: 0; gap: 14px; }
  .pipe::before { display: none; }
  .pipe-node { position: static; transform: none; }
}

/* application form — reuses the office-hours field vocabulary (.mt-*), one
   type-notch larger: Hangul at the meet page's 11/13px reads too small */
.jf-card { border-radius: 18px; padding: clamp(24px, 4vw, 40px); }
.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; }
.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 { max-width: 760px; }
.faq-item { border-top: 1px solid var(--line); }
.faq-item:last-child { border-bottom: 1px solid var(--line); }
.faq-q {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  padding: 17px 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,
.quickdial.is-nav .qd-hub-glyph {
  transform: rotate(45deg);
}
.quickdial.is-open .qd-hub,
.quickdial.is-nav .qd-hub { border-color: var(--accent); }
.quickdial.is-open .qd-hub::before,
.quickdial.is-nav .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 the hub.
   Same material recipe as .qd-sat (the --qd-* vars are blended per-button by
   quickDialScroll). Hidden while the fan or nav panel is open — the vertical
   fan rises through this 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,
.quickdial.is-nav .qd-steps { opacity: 0; }
.quickdial.is-open .qd-step,
.quickdial.is-nav .qd-step { pointer-events: none; }

/* ---- Tier 1: the satellite fan ---- */
.qd-fan {
  position: absolute;
  right: 30px;               /* the hub's centre (60px circle) — satellites and
                                edges radiate from here in both layout variants */
  bottom: 30px;
  width: 0;
  height: 0;
  z-index: 1;
}
.qd-sat {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 40px;
  height: 40px;
  margin: -20px;              /* centre the 40px node on the anchor point */
  border-radius: 50%;
  display: grid;
  place-items: center;
  cursor: pointer;
  color: var(--qd-fg);       /* blends with the glass, like the hub glyph */
  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);
  /* 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 */
              border-color .25s ease;
  /* --sd (set by JS) staggers only the spring-out; hover stays instant */
  transition-delay: var(--sd, 0s), var(--sd, 0s), 0s;
}
/* hover wash, same recipe as the hub */
.qd-sat::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-glyph {
  font-size: 15px;
  line-height: 1;
  letter-spacing: -0.04em;   /* keeps the "A文" language glyph compact */
}
.qd-sat:hover,
.qd-sat:focus-visible {
  outline: none;
  border-color: var(--accent);
  z-index: 2;   /* lift above sibling satellites so the tooltip never paints behind one */
}
.qd-sat:hover::before,
.qd-sat:focus-visible::before { opacity: 1; transform: scale(1); }
/* hair-thin edge-line from the hub to each node (constellation motif).
   A separate fan child anchored at the hub, rendered BEHIND the satellite
   buttons (the buttons are appended after the edges, so they paint on top). */
.qd-edge {
  position: absolute;
  right: 0;                  /* anchored at the hub / fan origin */
  bottom: 0;
  height: 1px;
  width: var(--edge-len, 0px);
  transform-origin: right center;
  transform: rotate(var(--edge-rot, 0deg));
  /* fg-blended like sky.js constellation lines: white over the night sky,
     ink over the light content zone — legible against both */
  background: linear-gradient(to left,
              transparent 6%,
              color-mix(in oklch, var(--qd-fg) 55%, transparent) 50%,
              transparent 96%);
  opacity: 0;
  transition: opacity .3s ease .04s;
  pointer-events: none;
}
/* fan open: satellites spring to their arc positions, edges fade in */
.quickdial.is-open .qd-fan .qd-sat {
  opacity: 1;
  transform: translate(var(--sx), var(--sy)) scale(1);
  pointer-events: auto;
}
.quickdial.is-open .qd-fan .qd-edge { opacity: 1; }

/* satellite tooltip (text label on hover/focus) */
.qd-sat .qd-tip {
  position: absolute;
  right: calc(100% + 12px);
  top: 50%;
  transform: translateY(-50%);
  white-space: nowrap;
  padding: 5px 10px;
  border-radius: 8px;
  font-family: var(--font-label);
  font-size: 11.5px;
  letter-spacing: 0.03em;
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.16);
  background: color-mix(in oklch, var(--band-dark) 78%, transparent);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: 0 8px 22px -12px rgba(0,0,0,0.6);
  opacity: 0;
  pointer-events: none;
  transition: opacity .2s ease;
}
.qd-sat:hover .qd-tip,
.qd-sat:focus-visible .qd-tip { opacity: 1; }

/* ---- 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);
}
/* 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 tooltip-glass chip holding a slider, left of the music
   satellite (js/bgm.js appends + positions it from the node's --sx/--sy).
   Shown while the fan is open and the satellite (or the flyout) is hovered/
   focused. The wrap itself is INVISIBLE hover zone: its right padding bridges
   the 12px visual gap to the satellite's edge, so the chip can sit clear of
   the node (tooltip spacing) without the cursor ever crossing a dead zone. */
.qd-volwrap {
  position: absolute;
  z-index: 1;
  transform: translateY(50%);
  display: flex;
  align-items: center;
  padding: 6px 12px 6px 0;     /* right pad = the invisible bridge to the node */
  opacity: 0;
  pointer-events: none;
  transition: opacity .2s ease;
}
.qd-volchip {
  display: flex;
  align-items: center;
  padding: 8px 12px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  background: color-mix(in oklch, var(--band-dark) 78%, transparent);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: 0 8px 22px -12px rgba(0,0,0,0.6);
  color: #fff;
}
.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 satellite's text tip yields to the slider chip */
.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: 76px;
  height: 18px;
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
}
.qd-vol::-webkit-slider-runnable-track { height: 2px; border-radius: 1px; background: color-mix(in oklch, currentColor 35%, 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; }
}

/* ---- Tier 2: Navigate list (glass pills) ---- */
.qd-nav {
  position: absolute;
  right: 4px;
  bottom: 62px;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 7px;
  padding: 10px;
  border-radius: 16px;
  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: translateY(10px) scale(0.96);
  transform-origin: bottom right;
  pointer-events: none;
  transition: opacity .3s ease, transform .4s cubic-bezier(.3,.7,.3,1);
}
.quickdial.is-nav .qd-nav {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
.qd-pill {
  min-width: 128px;
  text-align: right;
  padding: 8px 13px;
  border-radius: 999px;
  cursor: pointer;
  color: var(--qd-fg);
  font-family: var(--font-label);
  font-size: 12.5px;
  letter-spacing: 0.03em;
  border: 1px solid var(--qd-brd);
  background: transparent;
  transition: color .22s ease, border-color .22s ease, background-color .22s ease;
}
.qd-pill:hover,
.qd-pill:focus-visible {
  outline: none;
  color: var(--qd-fg);
  border-color: var(--accent);          /* header-pill hover recipe */
  background: color-mix(in oklch, var(--accent) 10%, transparent);
}
.qd-pill.active {
  color: var(--qd-fg);
  font-weight: 600;                     /* .nav-link.active recipe */
  border-color: var(--qd-brd);
  background: color-mix(in oklch, var(--accent) 16%, transparent);
}

/* ---- reduced motion: no arc, no spring, instant show ---- */
@media (prefers-reduced-motion: reduce) {
  .quickdial,
  .qd-hub,
  .qd-hub::before,
  .qd-hub-glyph,
  .qd-sat,
  .qd-sat::before,
  .qd-tip,
  .qd-edge,
  .qd-nav { transition: none !important; }
  .quickdial.is-open .qd-fan .qd-sat {
    /* stack straight up from the hub — no diagonal arc */
    transform: translate(var(--sx-rm), var(--sy-rm)) scale(1);
  }
  /* the arc connectors don't match a vertical stack — hide them */
  .qd-edge { display: none; }
}

/* ---------- 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 on the pub page,
   a page of update cards 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; }
}
