/* ── The typeface ──────────────────────────────────────────
   NanumSquare (Naver), self-hosted. The .woff2 files are committed to this
   repo, so whoever deploys it is redistributing them: keep Naver's licence
   text alongside static/fonts/ and check it still permits that. Nobody has
   verified it here.

   It replaced a
   three-family Google Fonts set — Inter for text, Space Grotesk for the lab
   name, IBM Plex Mono for the small uppercase labels — because that set had
   no Korean: /ko fell through to whatever "Malgun Gothic" the visitor
   happened to have, so the two languages were never the same site. One face
   carries both now.

   Self-hosted, not fetched: the files are ours, so there is no third-party
   request on first paint and no dependency on fonts.googleapis.com being
   reachable. Built by build_fonts.py in the repo root from the .ttf
   originals — subset to drop 4,620 hanja (never typed here) and repacked as
   WOFF2, which is 173KB/176KB for the full 11,172-syllable modern hangul
   set. All 11,172 are kept on purpose: a partial 2,350-syllable subset
   drops a rare syllable to a fallback face in the middle of a word.
   The .woff2 files are committed, so a checkout needs no build step.

   Three weights. 400 is the body face. 700 is insurance rather than design:
   the design sets b/strong/th back to 400 and uses no bold, but shipping a
   real 700 means an editor or a runaway rule gets Bold instead of a
   browser-synthesised smear. 800 is used on purpose, in exactly two places
   — the header nav and the hero's lab name.

   All three are declared under one family name at different weights, which
   is what lets `font-weight: 800` pick the real ExtraBold file. Declare only
   400 and 700 and the browser will happily accept `800` and fake it by
   smearing 700, with no error anywhere to tell you.

   font-display: swap — text paints immediately in the fallback and reflows
   when the face lands, which for a 173KB Korean face beats a blank screen.

   MIND THE PATH. A relative url() resolves against the stylesheet it is
   served from, and this file is never served: css_build.py concatenates it
   into static/style.css. So the path has to be written relative to
   static/style.css ("fonts/…"), not to this file's own directory
   ("../fonts/…", which would resolve to /fonts/ and 404). Any url() added
   to any css/*.css file has the same constraint. */
@font-face {
  font-family: "NanumSquare";
  src: url("fonts/NanumSquare-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "NanumSquare";
  src: url("fonts/NanumSquare-Bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "NanumSquare";
  src: url("fonts/NanumSquare-ExtraBold.woff2") format("woff2");
  font-weight: 800;
  font-style: normal;
  font-display: swap;
}

:root {
  /* One stack, named once. Every font-family in the codebase points here, so
     changing the face is one edit rather than fourteen. The fallbacks are
     the platform Korean faces, in case the WOFF2 fails to load. */
  --font: "NanumSquare", -apple-system, BlinkMacSystemFont, "Segoe UI",
    "Apple SD Gothic Neo", "Malgun Gothic", sans-serif;
  /* The lab's own palette: dark blue-slate surfaces with a mint-teal
     accent, sampled 1:1 from the Paper Radar (bist.t-ranno.com).

     This began as a homepage-only theme in hero.css, scoped to html.home,
     with everything else on a neutral black. Two palettes on one site is
     one too many — the teal is the lab's colour, so it is the site's. The
     homepage override is gone; these are the tokens now.

     The editors are exempt by design: editor_light.css redefines the same
     names inside body.editing, so a dense data-entry screen stays light. */
  --paper: #0a0f13;      /* page  (Paper Radar --bg) */
  --card: #0e1418;       /* --bg-elev */
  --card-2: #111a20;     /* raised: inputs, hovers, bubbles */
  --line: #152028;       /* --border-soft */
  --line-2: #1b2630;     /* --border */

  --ink: #e6f1ef;        /* 16.1:1 on a card — body text */
  --ink-2: #8aa0a8;      /* 6.8:1  — secondary text you still read */
  /* Was #5a6b73, which measured 3.3:1 on a card — under the 4.5:1 WCAG asks
     for body text, and this is the colour a publication's journal, volume,
     year and citation count are set in. Those are read, not glanced at.
     Raised to 5.4:1, which is legible without competing with --ink. */
  --muted: #7a8d96;      /* 5.4:1  — labels, counts, quiet metadata */

  /* Accent. --brand stays the deep bar colour for solid fills that carry
     white text; the lit teal is what reads as a mark on a dark page. */
  --brand: #05282f;
  --brand-2: #1c3c36;    /* soft fills: active chips, lit nodes */
  --brand-3: #1f6f5c;    /* solid buttons */
  --on-brand: #ffffff;
  --brand-lit: #3fe0b0;  /* THE teal — links, nodes, paths, small marks */

  /* Amber: the second accent, for "look at this" without "something broke".
     Index badges, the newest point on a chart, warnings that are a
     consequence rather than a failure. It was written literally in four
     places across two files before it had a name. */
  --warn: #e8a44c;
  --warn-soft: rgba(232, 164, 76, .12);
  --warn-line: rgba(232, 164, 76, .30);
  --warn-ink: #8a5a1c;      /* on the light editor background */
  --warn-bg: #fdf3e3;

  /* Aliases so existing rules keep working. */
  --accent: var(--brand-3);
  --accent-ink: var(--brand-lit);

  --green: #4a9e86;
  --rust: #c0764a;
  --blue: var(--brand-lit);
  --r: 5px;

  /* One gutter, used everywhere. The page has no centred container:
     grids and images run to the edge, text is held to a readable
     measure inside them. */
  /* No gutter, and no cap. The page runs to the edges of the screen.
     Everything on it was drawn for that, and putting it in a centred column
     showed how much — the hero, the news rail and the members network all
     needed a rule each to climb back out. Worth revisiting once the
     elements themselves are ready for it.

     The tokens stay, at zero, so the choice is one number rather than a
     search through the stylesheet. */
  /* Just enough that nothing touches the glass. Small on purpose — the
     elements were drawn for a full-width page, and the previous 40px read
     as a margin around a document rather than as breathing room. */
  --gutter: 16px;
  --page: none;
  --measure: 680px;
}
* { box-sizing: border-box; }
/* ─────────────────────────────────────────────────────────
   Accessibility: keyboard focus, and touch/keyboard access to
   controls that were previously mouse-hover only.
   ───────────────────────────────────────────────────────── */

/* One visible focus ring for everything reachable by keyboard. Form inputs
   set their own outline:none and draw a border+glow instead, so they opt out
   via :focus (not :focus-visible) below — this rule is for links, buttons,
   nav, cards, the carousel, and the language toggle, which had nothing. */
:focus-visible {
  outline: 2px solid var(--brand-lit);
  outline-offset: 2px;
  border-radius: 2px;
}
/* A mouse click shouldn't leave a ring on things that aren't text fields. */
:focus:not(:focus-visible) { outline: none; }

/* Skip link: first thing a keyboard user hits, lets them jump past the header
   on every page. Off-screen until focused. */
.skip-link {
  position: absolute; left: 8px; top: -48px; z-index: 100;
  background: var(--brand-3); color: var(--on-brand);
  padding: 9px 14px; border-radius: var(--r); font-size: 13px; font-weight: 400;
  transition: top .15s;
}
.skip-link:focus { top: 8px; text-decoration: none; }

/* Controls that were revealed on parent :hover also appear when a child is
   focused (keyboard), and are permanently visible on touch devices that have
   no hover at all (phones, tablets) — otherwise they're invisible there. */
.ui-card:focus-within .card-edit,
.news-shot:focus-within .shot-nav { opacity: 1; }
@media (hover: none) {
  .card-edit, .shot-nav { opacity: 1; }
}

/* Respect a reduced-motion preference. */
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
  .thumb { transform: none !important; }
}

/* The `hidden` attribute, made reliable.

   The UA stylesheet says `[hidden] { display: none }`, which any rule
   containing a class outranks — so an element with `display: flex` stays on
   screen after script sets hidden = true, silently. It bit the publications
   year dividers, and jsdom cannot reproduce it (it reports display:none
   either way), so no test we can run would catch the next one.

   !important is right here: this is not a style, it is the attribute
   meaning what it says. Anything that genuinely needs to show while hidden
   has bigger problems. */
[hidden] { display: none !important; }

/* Anchors ease rather than jump: the scroll cue, the news feed's
   /news#<slug> links and the blog's in-page links all land through this.
   Guarded, because for some people a moving page is a symptom rather
   than a nicety. */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}
html, body { margin: 0; padding: 0; background: var(--paper); color: var(--ink);
  font-size: 15px;
  font-family: var(--font); line-height: 1.6; -webkit-font-smoothing: antialiased; }
a { color: var(--blue); text-decoration: none; }
a:hover { text-decoration: underline; }

.site-header { display: flex; align-items: center; justify-content: space-between;
  gap: 24px; padding: 0 var(--gutter); height: 58px; background: var(--paper);
  border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 10; }



/* Brand left, everything else right. The nav's slots are a fixed size, so
   the nav is a fixed width whatever language it is in — it does not need
   protecting from the brand, it simply cannot change. */
.brand { display: flex; align-items: baseline; gap: 9px; min-width: 0;
  font-weight: 400; font-size: 18px; letter-spacing: 2px; color: var(--ink); }
.brand small { font-weight: 400; font-size: 11px; letter-spacing: .5px;
  color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Nav and actions ride together at the right edge. */
.header-end { display: flex; align-items: center; gap: 20px; margin-left: auto;
  height: 100%; }
.acts { display: flex; align-items: center; gap: 16px; }

/* Identical slots, centred: a link's own width stops mattering, so no label
   in any language or font can shift the row. This is the whole trick — it
   beats measuring text, because it never asks how wide the text is. */
nav.main { display: grid; grid-auto-flow: column; grid-auto-columns: 96px;
  align-items: center; flex: 0 0 auto; height: 100%; }
/* ExtraBold, deliberately: the nav is the one piece of chrome asked to carry
   weight. Safe against the fixed slot — the labels were measured in the real
   face at 13px and the widest, "Publications", comes to 77.5px against a
   96px slot. Korean labels don't change width at all between weights, since
   hangul is drawn on a square em and the advance stays 1em. If a longer
   label is ever added, measure it: `white-space: nowrap` means it will not
   wrap, it will overlap its neighbour. */
nav.main > a { display: flex; align-items: center; justify-content: center;
  text-align: center; height: 100%; position: relative;
  font-size: 13px; font-weight: 800; color: var(--ink-2); white-space: nowrap; }
nav.main > a:hover { color: var(--ink); text-decoration: none; }
/* An underline that grows from the centre, so hover doesn't nudge anything. */
nav.main > a::after { content: ""; position: absolute; left: 50%; bottom: 14px;
  transform: translateX(-50%); width: 0; height: 1.5px; background: var(--brand-lit);
  transition: width .25s; }
nav.main > a:hover::after,
nav.main > a[aria-current="page"]::after { width: 60%; }
nav.main > a[aria-current="page"] { color: var(--ink); }

/* The auth link swaps between "Log in" and "Log out" — and their Korean
   twins — so it reserves a slot too. */
.act-link { font-size: 13px; color: var(--ink-2); min-width: 62px; text-align: center; }
.act-link:hover { color: var(--ink); text-decoration: none; }


/* Each link holds both of its labels: the visible one, and the other
   language's stacked invisibly underneath. The link is therefore as wide as
   the WIDER of the two — measured by the browser in the font it actually
   loaded, so switching language cannot change any link's width, and nothing
   downstream shifts. No px is reserved by hand. */
.nav-item { position: relative; display: inline-grid; place-items: center;
  white-space: nowrap; }
.site-header nav a { color: var(--ink-2); font-size: 13px; }
.site-header nav a:hover { color: var(--ink); text-decoration: none; }
.badge { font-size: 12px; background: var(--brand); color: var(--ink);
  padding: 4px 11px; border-radius: 999px; border: 1px solid var(--brand-2);
  white-space: nowrap; }

/* A name cannot be boxed — "윤성민" is 3 characters, "Sungmin Yoon" is 12 —
   so the SLOT is boxed instead, and the badge sizes itself freely inside.
   The row only ever sees the slot, which is a fixed width in every language.
   Right-aligned within it, so the badge sits against the link beside it and
   the slack falls away from the row. */
/* A real width, not just a flex-basis. A basis is a starting size that the
   layout may still resolve against content — and `max-width: 100%` on the
   badge has no definite width to resolve against, so the badge takes its
   natural size and stretches the slot. `width` is definite; the slot is
   then 130px whatever the name, and the badge clips inside it. */
.who-slot { width: 130px; min-width: 130px; max-width: 130px; flex: 0 0 130px;
  display: flex; justify-content: center; align-items: center; overflow: hidden; }
.who-slot .badge { min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }

/* No centred container: the page runs to the edges, held off them by one
   gutter. Text inside is constrained separately (see --measure) — full-width
   prose is unreadable on a wide monitor, which is why MIT runs images edge
   to edge but never body copy. */
/* Centred, with a maximum. The page still runs to the edges — the header,
   the footer and anything marked .bleed ignore this — but the reading
   content stops before it becomes a line of text a metre wide. */
.container { max-width: var(--page); margin-inline: auto;
  padding: 30px var(--gutter) 88px; }

/* Out to the edges from inside a centred container. The hero strip on the
   homepage does this by hand today; this makes it a thing anything can ask
   for. */
.bleed { width: calc(100% + var(--gutter) * 2);
  margin-inline: calc(var(--gutter) * -1); }

/* Nothing sticks out sideways. A stray element a few pixels too wide gives
   the whole page a little horizontal play, which feels like a fault even
   when nothing is visibly cut off. */
html { overflow-x: hidden; }

/* Prose stays readable however wide the window is. */
/* Prose stays readable however wide the window is.

   NOT .lede — that name was already taken by the homepage's four-column
   band, and this rule was quietly capping it at 680px until home.css
   happened to override it a file later. Two different things had the same
   name and the cascade decided which won, which is exactly the kind of
   accident that only shows up when someone reorders the files. */
.prose { max-width: var(--measure); }
.eyebrow { font-size: 10px; letter-spacing: 2.2px; text-transform: uppercase;
  color: var(--brand-lit); font-weight: 400; margin-bottom: 9px; }
h1 { font-size: 23px; letter-spacing: -.3px; margin: 0 0 6px; font-weight: 400; }
h2 { font-size: 11px; letter-spacing: 1.4px; text-transform: uppercase; color: var(--muted); margin: 34px 0 14px; font-weight: 400; }
.muted { color: var(--muted); font-size: 13px; }
.toolbar { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 32px; }

/* ── Home: the first screen ────────────────────────────────
   A tagline and three paragraphs at the same rhythm as the footer columns,
   then the work strip running off the right edge. The lede is a text block,
   not a headline: the lab says something at length, then shows the work. */
/* Four columns that use the width. The lede does not share the footer's
   grid — the footer is a sitemap in small type, this is the lab talking,
   and it earns wider measures and bigger text. Columns stretch, gaps stay
   fixed: the block should reach across the screen, not huddle. */
/* Four explicit columns: tagline, then three paragraphs. This band is always
   1 + 3 — auto-fit was wrong here, since it packs by minimum width and then
   grows them, which strands the fourth column on its own row at widths where
   three happen to fit. The columns shrink together and only fold when they
   genuinely cannot hold a readable line. */
.lede { display: grid; gap: 30px 34px; padding: 26px 0 46px;
  grid-template-columns: minmax(180px, 0.8fr) repeat(3, minmax(240px, 1fr));
  max-width: 1560px; }
/* Below this the three paragraphs can no longer hold a readable measure
   side by side, so stack them. One breakpoint, at the point of real failure
   rather than a guessed screen size. */
@media (max-width: 940px) {
  .lede { grid-template-columns: minmax(0, 520px); gap: 24px; }
}

.lede-mark { font-size: 16px; font-weight: 400; letter-spacing: 3px; color: var(--ink);
  margin-bottom: 14px; }
.lede-tag p { margin: 0; font-size: 16.5px; line-height: 1.5; color: var(--ink);
  font-weight: 400; letter-spacing: -.2px; max-width: 20ch; }

.stmt-col h2 { font-size: 11px; letter-spacing: 1.4px; text-transform: uppercase;
  color: var(--brand-lit); margin: 0 0 10px; font-weight: 400; }
.stmt-col p { margin: 0; font-size: 15px; line-height: 1.6; color: var(--ink-2); }

/* One section-header pattern: a title on the left, action links on the right.
   `.spaced` adds the top rule + margin used between major sections. Replaces
   the old strip-head / team-head / toolbar trio. */
.section-head { display: flex; align-items: baseline; justify-content: space-between;
  gap: 16px; margin-bottom: 16px; }
.section-head.spaced { margin-top: 54px; padding-top: 28px;
  border-top: 1px solid var(--line); align-items: center; }
.section-head h2 { margin: 0; }
.section-links { display: flex; gap: 18px; }
.section-links a { font-size: 12px; color: var(--muted); white-space: nowrap; }
.section-links a:hover { color: var(--accent-ink); text-decoration: none; }

/* Full-bleed: break out of the container, run to the right edge and past it. */
/* The strip breaks the gutter on the right and runs off the screen. */
/* The rail runs off the right edge of the screen, which is what says there
   is more to scroll. It used to reach past the gutter, which was the edge
   when the container ran full width; now it has the centring margin to
   clear as well. */
/* Runs off the right edge, which is what says there is more to scroll.
   Measured from the gutter rather than from 100vw, which includes the
   vertical scrollbar and so pushed the page a few pixels wider than the
   window. */
.strip { margin-right: calc(var(--gutter) * -1); }
.strip-rail {
  display: flex; gap: 14px; overflow-x: auto; scroll-snap-type: x proximity;
  padding: 2px var(--gutter) 20px 0;
  scrollbar-width: thin; scrollbar-color: var(--line-2) transparent;
}
.strip-rail::-webkit-scrollbar { height: 7px; }
.strip-rail::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 4px; }
.strip-rail::-webkit-scrollbar-track { background: transparent; }
.strip-rail .ui-card { flex: 0 0 265px; scroll-snap-align: start; }

.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 18px; }
.ui-card { position: relative; background: transparent; border: none; border-radius: var(--r); overflow: hidden; }
.card-link { display: block; color: inherit; text-decoration: none; }
.card-link:hover { text-decoration: none; }
.ui-card .thumb {
  display: block; width: 100%; aspect-ratio: 1 / 1; object-fit: cover;
  background: var(--card-2); border-radius: var(--r); border: 1px solid var(--line);
  transition: transform .25s ease, border-color .2s;
  /* a project thumbnail can carry its own zoom (see cover_zoom), so the
     image must be clipped to the card - .ui-card already sets overflow */
  transform-origin: center;
}
/* A framed thumbnail carries its own inline transform (see cover_style), and
   an inline style beats this rule - so hover only changes the border there.
   Unframed images still get the subtle zoom. */
.card-link:hover .thumb { transform: scale(1.02); border-color: var(--line-2); }
.card-link:hover .thumb[style] { transform: none; }
.ui-card .thumb.placeholder {
  display: flex; align-items: center; justify-content: center;
  color: var(--ink-2); font-size: 30px; font-weight: 400; letter-spacing: 3px;
}
.ui-card .ui-body { padding: 10px 2px 0; }
.ui-card h3 { margin: 0; font-size: 13.5px; font-weight: 400; letter-spacing: -.1px; color: var(--ink); line-height: 1.4; }
.ui-card h3 .ext { color: var(--muted); font-size: 13px; font-weight: 400; }
.ui-card .year { font-size: 11px; color: var(--muted); margin-top: 3px; letter-spacing: .3px; }
.card-edit {
  position: absolute; top: 10px; right: 10px; width: 30px; height: 30px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(8,9,9,.82); border: 1px solid var(--line-2); border-radius: 6px;
  color: var(--ink-2); font-size: 14px; opacity: 0; transition: opacity .15s;
}
/* Visual chip stays 30px; the tap target is expanded to the 44px minimum
   with an invisible overlay, so touch users can actually hit it. */
.card-edit::before {
  content: ""; position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%); width: 44px; height: 44px;
}
.ui-card:hover .card-edit { opacity: 1; }
.card-edit:hover { color: var(--accent); border-color: var(--accent); text-decoration: none; }

.status { font-size: 11px; text-transform: uppercase; letter-spacing: 1px; font-weight: 400;
  display: inline-flex; align-items: center; gap: 6px; }
.status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.status.ongoing { color: var(--rust); }
.status.live { color: var(--green); }
.status.completed { color: var(--muted); }

.btn { display: inline-block; background: var(--card-2); color: var(--ink); border: 1px solid var(--line-2);
  padding: 10px 18px; border-radius: var(--r); font: inherit; font-size: 14px; cursor: pointer; }
.btn:hover { background: #1f2323; text-decoration: none; }
.btn.primary { background: var(--brand-3); color: var(--on-brand);
  border-color: var(--brand-3); font-weight: 400; }
.btn.primary:hover { background: var(--brand-2); border-color: var(--brand-lit); }
.btn.small { padding: 6px 12px; font-size: 13px; }

table.list { width: 100%; border-collapse: collapse; background: var(--card);
  border: 1px solid var(--line); border-radius: var(--r); overflow: hidden; }
table.list th, table.list td { text-align: left; padding: 12px 15px; border-bottom: 1px solid var(--line); font-size: 14px; }
table.list th { color: var(--muted); font-weight: 400; font-size: 11px; text-transform: uppercase;
  letter-spacing: .5px; background: var(--card-2); }
table.list tr:last-child td { border-bottom: none; }




.empty { border: 1px dashed var(--line-2); border-radius: var(--r); padding: 52px; text-align: center;
  color: var(--muted); background: var(--card); }

/* ── Forms ─────────────────────────────────────────────── */
form.ui-entry { display: grid; gap: 26px; max-width: var(--measure); }
.field { display: grid; gap: 7px; }
.field label { font-size: 12px; font-weight: 400; color: var(--ink-2); letter-spacing: .4px; }
.field .req { color: #e8896f; margin-left: 2px; }
.field input, .field select, .field textarea {
  padding: 11px 13px; border: 1px solid var(--line-2); border-radius: var(--r);
  font: inherit; font-size: 15px; width: 100%; background: var(--card-2); color: var(--ink);
  transition: border-color .12s, background .12s, box-shadow .12s;
}
.field input::placeholder, .field textarea::placeholder { color: var(--muted); }
.field input:hover, .field select:hover, .field textarea:hover { border-color: var(--line-2); }
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none; border-color: var(--brand-lit); background: #1c2020;
  box-shadow: 0 0 0 3px rgba(95, 188, 203, .12);
}
.field textarea { min-height: 120px; resize: vertical; }
.field .hint { font-size: 12px; color: var(--muted); line-height: 1.5; }
@media (max-width: 640px) { .row2 { grid-template-columns: 1fr; } }

/* File input (browsers style these badly by default) */
.field input[type="file"] { padding: 9px; font-size: 13px; cursor: pointer; color: var(--ink-2); }
.field input[type="file"]::file-selector-button {
  background: var(--card); color: var(--ink); border: 1px solid var(--line-2);
  border-radius: 4px; padding: 6px 12px; margin-right: 12px; font: inherit; font-size: 13px; cursor: pointer;
}
.field input[type="file"]::file-selector-button:hover { background: #1f2323; border-color: var(--brand-lit); }
.thumb-preview { border-radius: 6px; border: 1px solid var(--line); max-width: 180px; display: block; margin-bottom: 8px; }

/* Progressive disclosure fold */
details.more { border-top: 1px solid var(--line); padding-top: 22px; }
details.more > summary {
  cursor: pointer; font-size: 13px; font-weight: 400; color: var(--ink-2);
  list-style: none; display: flex; align-items: center; gap: 8px; letter-spacing: .3px;
}
details.more > summary::-webkit-details-marker { display: none; }
details.more > summary::before { content: "▸"; color: var(--muted); font-size: 11px; }
details.more[open] > summary::before { content: "▾"; }
details.more > summary:hover { color: var(--ink); }
details.more .inner { display: grid; gap: 26px; margin-top: 26px; }

/* People tagger */
.people { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }

/* Form actions */
.form-actions { display: flex; gap: 10px; padding-top: 4px; }
/* Right-aligned variant: Save + Cancel at the bottom-right of the editor. */
.form-actions.end { justify-content: flex-end; }

/* ── Destination picker (upload page OR external link) ─── */
.dest { border: 1px solid var(--line); border-radius: var(--r); padding: 20px; background: rgba(23,29,37,.5); display: grid; gap: 14px; }
.dest-opt {
  display: grid; grid-template-columns: auto 1fr; gap: 12px; align-items: start;
  padding: 16px; border: 1px solid var(--line); border-radius: var(--r);
  background: var(--card); cursor: pointer; transition: border-color .15s, background .15s;
}
.dest-opt:hover { border-color: var(--line-2); }
.dest-opt:has(input[type="radio"]:checked) { border-color: var(--brand-lit); background: var(--brand); }
.dest-opt > input[type="radio"] { margin-top: 3px; accent-color: var(--accent); width: 16px; height: 16px; }
.dest-opt > span { display: grid; gap: 8px; }
.dest-opt b { font-size: 14px; color: var(--ink); font-weight: 400; }
.dest-opt .hint { font-size: 12px; color: var(--muted); line-height: 1.5; }
.dest-opt input[type="file"], .dest-opt input[name="external_url"] {
  padding: 9px 11px; border: 1px solid var(--line-2); border-radius: var(--r);
  background: var(--card-2); color: var(--ink); font: inherit; font-size: 14px; width: 100%;
}
.dest-opt input[name="external_url"]:focus, .dest-opt input[type="file"]:focus {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(95, 188, 203, .12);
}
.dest-opt input[type="file"]::file-selector-button {
  background: var(--paper); color: var(--ink); border: 1px solid var(--line-2);
  border-radius: 4px; padding: 5px 11px; margin-right: 10px; font: inherit; font-size: 13px; cursor: pointer;
}

/* ── Language switch ───────────────────────────────────── */
/* A control, not a link: a bordered pill holding both labels at once, the
   current one filled in. This was set in the platform monospace so EN and KR
   came out the same width by construction; it is on the site face now, along
   with everything else. The pill's width is still fixed either way — both
   labels are always rendered, so switching language cannot resize it. What
   the change does cost is that the pill's two halves are no longer identical
   in width, since K and R are wider than E and N in a proportional face. */
.lang { display: inline-flex; align-items: center;
  font-family: var(--font);
  font-size: 10.5px; letter-spacing: .1em;
  border: 1px solid var(--line-2); border-radius: 100px; overflow: hidden; }
.lang a { color: var(--muted); padding: 5px 9px; line-height: 1.4; }
.lang a:hover { color: var(--ink); text-decoration: none; }
.lang a.on { color: var(--on-brand); background: var(--brand-3); }

/* The footer carries the same control, a touch smaller. */
.foot-base .lang { font-size: 9.5px; }
.foot-base .lang a { padding: 4px 8px; }

/* ── Narrow single-column pages (login, claim) ─────────── */
.ui-narrow { max-width: 420px; margin: 20px auto; }
.ui-stack { display: grid; gap: 12px; margin-top: 24px; }
.small-note { margin-top: 28px; font-size: 13px; }
.form-error {
  border: 1px solid #5a3230; background: rgba(232,139,128,.08);
  color: #e88b80; border-radius: var(--r); padding: 12px 14px; font-size: 14px; margin-top: 16px;
}

/* ── People grid ───────────────────────────────────────── */
/* home.css owns .section-head as a flex header (title + action links). The
   members page uses the same name for a plain "Alumni" label, which merged
   the two into one confused rule. Scoped to where it actually appears: the
   heading between the two people grids. */
.people-grid + .section-head { font-size: 13px; letter-spacing: 1.5px; text-transform: uppercase;
  color: var(--muted); margin: 56px 0 20px; padding-top: 28px; border-top: 1px solid var(--line); }
.name-alt { font-size: 13px; color: var(--ink-2); }
.interests { margin: 8px 0 0; padding-left: 15px; }
.interests li { font-size: 12px; color: var(--muted); line-height: 1.5; }

/* One discreet roster edit link */
.edit-roster { font-size: 13px; color: var(--muted); border: 1px solid var(--line);
  border-radius: var(--r); padding: 7px 13px; white-space: nowrap; }
.edit-roster:hover { color: var(--accent-ink); border-color: var(--accent); text-decoration: none; }

/* ── Roster bulk editor ────────────────────────────────── */
.ui-notice { font-size: 13px; color: var(--ink-2); background: rgba(95, 188, 203, .07);
  border: 1px solid rgba(95, 188, 203, .25); border-radius: var(--r); padding: 12px 14px; margin-bottom: 20px; }
.roster-scroll { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--r); }
/* The table is as wide as its columns need and scrolls sideways inside
   .roster-scroll, rather than squeezing everything into the viewport. */
table.roster { border-collapse: collapse; width: max-content; min-width: 100%;
  background: var(--card); }
table.roster th { text-align: left; font-size: 10px; text-transform: uppercase; letter-spacing: .6px;
  color: var(--muted); font-weight: 400; padding: 10px 8px; background: var(--card-2);
  border-bottom: 1px solid var(--line); white-space: nowrap; position: sticky; top: 0; z-index: 2; }
table.roster td { padding: 12px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
table.roster tr:last-child td { border-bottom: none; }
table.roster input, table.roster select {
  width: 100%; background: transparent; border: 1px solid transparent;
  border-radius: 4px; padding: 7px 8px; color: var(--ink); font: inherit; font-size: 13px;
}
/* Selects render a native dropdown arrow on the right; add room so long option
   text (e.g. "Undergraduate Researcher") doesn't clip against it. */
table.roster select { padding-right: 22px; }
/* Columns sized to what they actually hold, rather than one min-width for all. */
table.roster th, table.roster td { min-width: 0; }

.col-name-ko  { width: 110px; }
.col-name-en  { width: 170px; }
.col-role     { width: 220px; }
/* Page-scoped: projects.css also defines .col-status, at a different width,
   and both editors use table.roster - so scoping to the table isn't enough. */
.edit-members .col-status { width: 110px; }
/* Wide enough for the YYYY-MM placeholder plus cell padding - at 84px the
   text clipped. Scoped to the roster because news.css also defines .col-date
   (at a different width) and loads later: an unscoped rule here loses. */
.edit-members .col-date { width: 108px; }
/* Plain YYYY-MM text field (no native month picker, which renders locale text
   like 년/월). Placeholder shows a dashed blank so an empty date reads clearly. */
/* The measured problem: the input computed to 68px with 8px padding each side
   and inherited letter-spacing, leaving ~50px for text that needs ~55px - so
   "YYYY-MM" clipped. Sized here explicitly rather than relying on the column,
   and the inherited letter-spacing is cleared: it widens every character for
   no benefit on a fixed-format field. */
.date-in {
  text-align: center;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0 !important;
  width: 92px;
  padding-left: 4px;
  padding-right: 4px;
}
.date-in::placeholder { color: var(--muted); }
.col-email    { width: 210px; }
.col-dept     { width: 190px; max-width: 190px; }
.col-interests { width: 200px; max-width: 200px; }

/* A cell that previews its content (interests, and the department pair) as a
   left-aligned, clipped label rather than hiding everything behind a count. */
.preview-btn { width: 100%; text-align: left; justify-content: flex-start;
  white-space: nowrap; overflow: hidden; }
/* Stacked, one item per line, each clipped — a couple of lines read far
   better than one long comma-run stretching the column sideways. */
.preview-stack { display: flex; flex-direction: column; gap: 1px; min-width: 0; width: 100%; }
.preview-line { display: block; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font-size: 11.5px; color: var(--ink-2); line-height: 1.35; }
.preview-more { font-size: 10.5px; color: var(--muted); }
.preview-empty { font-size: 12px; color: var(--muted); }
.col-sid      { width: 120px; }
.col-rno      { width: 140px; }
.edit-members .col-mobile { width: 148px; }
.edit-members .col-birth  { width: 132px; }
table.roster input:hover, table.roster select:hover { border-color: var(--line-2); background: var(--card-2); }
table.roster input:focus, table.roster select:focus {
  outline: none; border-color: var(--brand-lit); background: #1c2020; box-shadow: 0 0 0 2px rgba(95, 188, 203, .12);
}
table.roster th.private-col { color: var(--accent-ink); }
table.roster td.private-col { background: rgba(95, 188, 203, .03); }
.col-photo { width: 66px; }
.col-del { width: 44px; text-align: center; }

/* Square, like the card and the crop tool. At 64x48 this row thumbnail
   previewed a wider crop than the page would show, so a photo could look
   right here and be cut off there. */
.photo-cell { position: relative; display: block; width: 52px; height: 52px; cursor: pointer; }
.photo-cell img, .photo-cell .face-mini {
  width: 52px; height: 52px; border-radius: 6px; object-fit: cover;
  background: var(--card);
  border: 1px dashed var(--line-2); background: var(--card-2); display: flex;
  align-items: center; justify-content: center; color: var(--muted);
}
.photo-cell img { border-style: solid; }
.photo-cell .face-mini svg { width: 20px; height: 20px; }
.photo-cell .photo-hover svg { width: 18px; height: 18px; }
.photo-hover { position: absolute; inset: 0; border-radius: 6px; display: flex;
  align-items: center; justify-content: center; background: rgba(8,9,9,.72);
  color: var(--accent-ink); font-size: 13px; opacity: 0; transition: opacity .12s; }
.photo-cell:hover .photo-hover { opacity: 1; }
.photo-cell.changed img { border-color: var(--green); }

.del-box input { width: 15px; height: 15px; accent-color: #d1665c; cursor: pointer; min-width: 0; }
tr.to-delete { opacity: .38; }
tr.to-delete input, tr.to-delete select { text-decoration: line-through; }
tr.new-row td { background: rgba(74, 158, 134, .04); }
tr.new-row input::placeholder { color: var(--green); opacity: .7; }

/* ── Homepage team list (tiered text; faces live on /members) ── */
.team { display: grid; gap: 30px; }
.team-role { font-size: 13px; color: var(--muted); font-style: italic; }
/* A name that points somewhere: subtle until hovered (MIT-style). */
.team-name.linked, .person h3 .linked { color: inherit; }
.team-name.linked:hover, .person h3 .linked:hover { color: var(--accent-ink); text-decoration: none; }
.team-name.linked::after, .person h3 .linked::after {
  content: "↗"; font-size: .72em; color: var(--line-2); margin-left: 4px;
  vertical-align: super; transition: color .12s;
}
.team-name.linked:hover::after, .person h3 .linked:hover::after { color: var(--accent-ink); }

/* ── The roster card ────────────────────────────────────────
   Laid out like the lab's own page: a portrait, both names, the role in both
   languages, contact, department, then history and interests as lists.

   Three across, because at this density a card is a small profile rather than
   a thumbnail - and a round photo reads as a person rather than an ID badge. */
/* Columns fitted to the viewport, and wide enough for the photo to sit
   BESIDE the text rather than on top of it.

   A square photo at full card width was the tallest thing on the page and
   the first thing you saw of everyone — overbearing for what is really a
   name and a research interest. Beside the text it is still square, still
   the same crop, just no longer the whole card. Cards go from roughly 540px
   tall to 220px. */
/* Every card in a row is the same height. `align-items: start` let each one
   size to its own contents, so a member with three research interests stood
   taller than one with none and the grid came out ragged — which reads as a
   layout fault rather than as a difference in how much someone has filled
   in. `stretch` is the default; it was overridden. */
/* Clear of the network above it. The section had no space after it, so the
   first row of cards sat against the panel. */
.net + .people-grid,
.net ~ .people-grid { margin-top: 34px; }
.people-grid { display: grid;
  grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
  gap: 30px 28px; align-items: stretch; }

/* Filling the cell is what makes the row's height mean anything: without
   it the grid stretches the track and the card sits at its own height
   inside it, which looks identical to the ragged version. */
/* A card, not a bare row on the page background. It had no surface of its
   own — the photo and the text simply sat on the page — so twenty of them
   read as one long list rather than twenty things, and the equal heights
   fixed above had nothing to show for themselves.

   The same tokens the rest of the site uses for a raised surface, so this
   matches the panels on the publications and news pages and follows the
   editor's light theme without any extra rules. */
.person { display: flex; gap: 14px; align-items: flex-start; height: 100%;
  background: var(--card); border: 1px solid var(--line-2);
  border-radius: var(--r); padding: 16px;
  transition: border-color .18s, background .18s; }
.person:hover { border-color: var(--brand-3); background: var(--card-2); }
.person-body { flex: 1; min-width: 0; }

/* 4:3, deliberately. A square or portrait frame implies the photo's only job
   is to document a face, which pushes everyone toward formal headshots. A
   landscape frame leaves room for fieldwork, a conference, a whiteboard - the
   photo can say something about the person, while the grid stays orderly.
   Freedom comes from what's IN the photo, not from every card being a
   different shape. */
.person .face {
  /* Fixed, not 100% of the card. Square as before — the editor's crop tool
     shows the same frame, so what someone frames is what appears here. */
  flex: none; width: 150px; aspect-ratio: 1 / 1;
  border-radius: var(--r); overflow: hidden;
  background: var(--card); border: 1px solid var(--line-2);
}
.person .face img {
  display: block; width: 100%; height: 100%;
  transform-origin: center;
}
.person .face.placeholder {
  display: flex; align-items: center; justify-content: center;
  font-size: 40px; font-weight: 400; color: var(--muted);
}

.person-body { width: 100%; display: flex; flex-direction: column; gap: 4px; }
.person h3 { margin: 0; font-size: 16px; font-weight: 400;
  letter-spacing: -.1px; }
.name-alt { font-size: 14px; color: var(--ink-2); margin-bottom: 10px; }

/* Role on the left, contact on the right - the old page's arrangement. */
.person-meta { display: flex; justify-content: space-between; gap: 14px;
  align-items: flex-start; font-size: 12.5px; }
.person-role { display: flex; flex-direction: column; gap: 1px; }
.person-role .role-en { font-weight: 400; color: var(--ink); }
.person-role .role-ko { color: var(--ink-2); }
.person-contact { display: flex; flex-direction: column; gap: 1px;
  text-align: right; color: var(--muted); }
.person-contact a { color: var(--muted); }
.person-contact a:hover { color: var(--accent-ink); }

.person-dept { font-size: 11.5px; color: var(--accent-ink); margin: 12px 0 4px; }

.person-block { margin-top: 12px; }
.person-block h4 { margin: 0 0 4px; font-size: 12px; font-weight: 400;
  color: var(--ink); }
.person-block ul { margin: 0; padding-left: 16px; }
.person-block li { font-size: 12px; line-height: 1.55; color: var(--ink-2);
  margin-bottom: 2px; }

/* members.css — Members: public grid + roster editor.
   App concatenates css/*.css in fixed order (see css_build.py);
   don't move rules between files. */


/* ── Roster editor: wider, roomier ─────────────────────── */
/* The roster is the one page that earns more width than the container. */
/* Was a hack to escape the centred container; the page is full width now. */
.roster-wide { max-width: none; }
.links-btn {
  background: var(--card-2); border: 1px solid var(--line-2); border-radius: 5px;
  color: var(--ink-2); font: inherit; font-size: 12px; padding: 5px 9px; cursor: pointer;
  white-space: nowrap;
}
.links-btn:hover { border-color: var(--accent); color: var(--accent-ink); }

/* ── Links popup ───────────────────────────────────────── */
.modal-back {
  position: fixed; inset: 0; background: rgba(5,6,6,.76); z-index: 50;
  display: flex; align-items: center; justify-content: center; padding: 24px;
}
.modal-back[hidden] { display: none; }
.modal {
  background: var(--card); border: 1px solid var(--line-2); border-radius: 10px;
  padding: 24px; width: 100%; max-width: 620px; max-height: 82vh; overflow-y: auto;
  box-shadow: 0 24px 70px rgba(0,0,0,.55);
}
.modal-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.modal-head h3 { margin: 0; font-size: 18px; letter-spacing: -.2px; }
.modal-x { background: none; border: none; color: var(--muted); font-size: 24px;
  line-height: 1; cursor: pointer; padding: 0 4px; }
.modal-x:hover { color: var(--ink); }
.modal-note { font-size: 12.5px; color: var(--muted); margin: 8px 0 18px; line-height: 1.55; }
.modal-foot { display: flex; justify-content: flex-end; margin-top: 20px;
  padding-top: 16px; border-top: 1px solid var(--line); }

.link-row { display: grid; grid-template-columns: 150px auto 1fr auto auto; gap: 8px;
  align-items: center; margin-bottom: 9px; }
.link-row .link-other[hidden] { display: none; }
.link-row select, .link-row input.link-url {
  background: var(--card-2); border: 1px solid var(--line-2); border-radius: 5px;
  padding: 8px 10px; color: var(--ink); font: inherit; font-size: 13px; width: 100%;
}
.link-row select:focus, .link-row input.link-url:focus {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px rgba(95, 188, 203, .12);
}

/* Degrees popup: one free-text line per degree. */
.modal-degrees { max-width: 880px; }
/* Degrees popup behaves like a table: a header row of labels, then input rows
   sharing the exact same column widths so everything lines up. Field/major and
   institution get the most room; type and year stay just wide enough. */
/* History popup: type, what it was, where, and a span. One grid for degrees
   and positions both - a degree just leaves "from" empty. */
.hist-head, .hist-row { display: grid;
  grid-template-columns: 150px 1.7fr 1.7fr 84px 84px 28px; gap: 8px; align-items: center; }
.hist-head { margin-bottom: 6px; padding: 0 2px; }
.hist-head span { font-size: 10px; text-transform: uppercase; letter-spacing: .6px;
  color: var(--muted); font-weight: 400; }
.hist-row { margin-bottom: 9px; }
.hist-row select, .hist-row input {
  background: var(--card-2); border: 1px solid var(--line-2); border-radius: 5px;
  padding: 8px 10px; color: var(--ink); font: inherit; font-size: 13px; width: 100%;
}
.hist-row select:focus, .hist-row input:focus {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px rgba(95, 188, 203, .12);
}
.link-main { display: flex; align-items: center; gap: 5px; font-size: 11.5px;
  color: var(--muted); cursor: pointer; white-space: nowrap; padding: 0 4px; }
.link-main input { accent-color: var(--accent); cursor: pointer; }
.link-main:hover { color: var(--accent-ink); }

/* ── Cell buttons (interests / links popups) ───────────── */
.col-btn { width: 66px; text-align: center; }
.cell-btn {
  background: var(--card-2); border: 1px solid var(--line-2); border-radius: 5px;
  color: var(--ink-2); font: inherit; font-size: 12px; padding: 6px 12px; cursor: pointer;
  display: inline-flex; align-items: center; gap: 5px; min-width: 46px; justify-content: center;
}
.cell-btn:hover { border-color: var(--accent); color: var(--accent-ink); }
.cell-btn::before { content: "✎"; font-size: 11px; opacity: .75; }
.links-btn::before { content: "🔗"; }
.cell-btn-n { font-weight: 400; }

/* ── Popups ────────────────────────────────────────────── */
.modal.ui-wide { max-width: 820px; }
.two-lang { display: grid; grid-template-columns: 1fr 1fr; gap: 26px; }
@media (max-width: 720px) { .two-lang { grid-template-columns: 1fr; } }

/* Department popup: two stacked boxes, wide enough to read a full name. */
.modal-dept { max-width: 760px; }
.dept-stack { display: flex; flex-direction: column; gap: 16px; }
.dept-stack input { width: 100%; }
.lang-head { font-size: 10px; letter-spacing: 1.4px; text-transform: uppercase;
  color: var(--brand-lit); font-weight: 400; margin-bottom: 10px; }
.bullet-rows { margin-bottom: 10px; }
.bullet-row { display: grid; grid-template-columns: 12px 1fr auto; gap: 8px;
  align-items: center; margin-bottom: 7px; }
.bullet { color: var(--muted); font-size: 15px; line-height: 1; }
.bullet-text {
  background: var(--card-2); border: 1px solid var(--line-2); border-radius: 5px;
  padding: 8px 10px; color: var(--ink); font: inherit; font-size: 13px; width: 100%;
}
.bullet-text:focus { outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(95, 188, 203, .12); }
.row-del { background: none; border: none; color: var(--muted); font-size: 17px;
  cursor: pointer; padding: 0 4px; line-height: 1; }
.row-del:hover { color: #e88b80; }
.link-row .link-other {
  background: var(--card-2); border: 1px solid var(--accent); border-radius: 5px;
  padding: 8px 10px; color: var(--ink); font: inherit; font-size: 13px; width: 100%;
}
/* Messages the server leaves after a redirect. Amber rather than red: this
   is "some of what you asked for didn't happen", not a failure. */
.flashes { margin: 0 0 16px; }
.flash { margin: 0 0 8px; padding: 11px 14px; border-radius: 8px;
         font-size: 14px; line-height: 1.5;
         color: var(--warn); background: rgba(232, 164, 76, .10);
         border: 1px solid var(--warn-line); }
body.editing .flash { color: var(--warn-ink); background: var(--warn-bg); border-color: var(--warn-line); }

/* ── Teams menu in the nav ──────────────────────────────────────
   nav.main is a grid of fixed 96px columns and its links are styled by
   `nav.main > a` — a direct-child selector, so none of it reaches a button
   nested one level deeper. The button therefore restates those properties
   rather than inheriting them, and .nav-drop fills its grid cell the way a
   link does. */
.nav-drop { position: relative; display: flex; height: 100%; }
/* font-weight after `font: inherit`, not before: the shorthand resets every
   font sub-property, so a weight declared above it would be wiped. The
   button has to match nav.main > a or Teams reads thinner than the four
   real links beside it. */
.nav-drop-btn { display: flex; align-items: center; justify-content: center;
                gap: 4px; width: 100%; height: 100%;
                font: inherit; font-size: 13px; font-weight: 800;
                text-align: center;
                white-space: nowrap; color: var(--ink-2);
                background: none; border: 0; padding: 0; cursor: pointer; }
nav.main .nav-drop:hover .nav-drop-btn,
.nav-drop.open .nav-drop-btn { color: var(--ink); }
/* The same underline the links get, so the row reads as one set. */
.nav-drop-btn::after { content: ""; position: absolute; left: 50%; bottom: 14px;
                       transform: translateX(-50%); width: 0; height: 1.5px;
                       background: var(--brand-lit); transition: width .18s; }
.nav-drop:hover .nav-drop-btn::after,
.nav-drop.open .nav-drop-btn::after { width: 60%; }



/* Opens on hover, and on keyboard focus so a tab-user gets it too. The
   .open class is only for touch screens, where there is no hover — the
   script adds it on tap.

   A gap between button and menu would close it as the pointer crosses, so
   the menu starts flush against the header and pads itself instead. */
.nav-menu { display: none; position: absolute; top: calc(100% - 2px); left: 50%;
            transform: translateX(-50%); min-width: 250px; z-index: 60;
            background: var(--card); border: 1px solid var(--line-2);
            border-radius: 10px;
            /* Padding, not margin. A 4px margin left a dead strip between the
               button and the menu: the pointer crossed it on the way down,
               :hover ended, and the menu shut before it could be reached.
               Padding keeps the gap visually and keeps the pointer inside. */
            padding: 10px 6px 6px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, .45); }
.nav-drop:hover .nav-menu,
.nav-drop:focus-within .nav-menu,
.nav-drop.open .nav-menu { display: block; }
.nav-menu a { display: block; padding: 8px 12px; border-radius: 7px;
              font-size: 13px; color: var(--ink-2); text-decoration: none;
              white-space: nowrap; }
.nav-menu a:hover { background: var(--card-2); color: var(--ink); }
.nav-menu a[aria-current="page"] { color: var(--brand-lit); }

/* The narrow-screen behaviour of these menus lives in header.css now, with
   the rest of the header. There used to be a partial version here as well,
   and below 720px the two disagreed about padding, borders and alignment -
   which is what left the open panel looking half-styled. *//* ── Roster editor bands ───────────────────────────────── */
tr.band-row td {
  background: var(--paper); padding: 18px 8px 7px; border-bottom: 1px solid var(--line-2);
}
tr.band-row:first-child td { padding-top: 8px; }
.band {
  font-size: 10px; letter-spacing: 1.6px; text-transform: uppercase;
  color: var(--accent-ink); font-weight: 400;
}
.band-n { font-size: 11px; color: var(--muted); margin-left: 8px; }
.fixed-cell { font-size: 13px; color: var(--muted); padding: 7px 8px; display: inline-block; }
tr.band-row.new-band td { padding-top: 26px; border-bottom-color: rgba(74, 158, 134, .35); }
tr.band-row.new-band .band { color: var(--green); }
.field .opt, .dest-head .opt { color: var(--muted); font-weight: 400; font-size: 11px;
  text-transform: lowercase; letter-spacing: .3px; margin-left: 4px; }
/* Only editors see this: a nudge that the card doesn't go anywhere yet. */
.no-dest { color: var(--rust); }
.ui-card:not(:has(.card-link)) .thumb { opacity: .82; }

/* ── Projects table ──────────────────────────────────────
   Columns are grouped by meaning (identity / timeline+state / people /
   content / metadata / destination). Widths are set so each cell is
   READABLE - the table may scroll horizontally, that's fine; cramped
   unreadable columns are not. */
/* The table is an INDEX: it carries what you scan, not what you write. One
   name column (Korean leads - most editors are Korean), and the small values
   stay inline because that's what rows are good at. */
.col-name    { min-width: 0; width: 26%; }
.col-yr      { width: 116px; }
.edit-projects .col-status { width: 158px; }
.col-live    { width: 92px; text-align: center; }
.col-fill    { width: 232px; }
/* .btn is a page-level button (10px/18px padding, 14px type) - far too big
   for a table cell, where it overflowed the column. Size it down in-row. */
/* `table.roster th` forces text-align:left, so the header has to be told
   again or it sits left of its centred button. */
.col-edit    { width: 86px; text-align: center; }
table.roster th.col-edit,
table.roster th.col-live,
table.roster th.col-del { text-align: center; }
.edit-btn { padding: 5px 12px; font-size: 12.5px; white-space: nowrap;
  line-height: 1.6; }
.col-thumb   { width: 66px; }

/* Read-only row cells. Everything is edited in the window now; these stay
   inputs rather than becoming spans because the window mirrors them by name
   and writes back into them, so the row updates the instant the window saves.
   Styled flat so they read as values, not as fields going spare. */
.cell.ro, .cell.ro:hover, .cell.ro:focus {
  background: none; border-color: transparent; box-shadow: none;
  cursor: default; color: var(--ink);
}
.cell.ro::placeholder { color: var(--muted); opacity: .55; }
/* A <select> has no readonly attribute, so it is locked here instead. */
select.cell.ro { pointer-events: none; appearance: none; padding-left: 0; }
.svc-box.ro { pointer-events: none; opacity: .75; }

/* Inline cells: quiet until you touch them, so the table reads as a list
   rather than a wall of boxes. */
.cell { font: inherit; font-size: 13px; padding: 5px 7px; width: 100%;
  border: 1px solid transparent; border-radius: 4px; background: transparent;
  color: var(--ink); }
.cell:hover { border-color: var(--line-2); background: #fff; }
.cell:focus { outline: none; border-color: var(--accent); background: #fff;
  box-shadow: 0 0 0 2px rgba(15, 74, 86, .14); }
.cell.dt { width: 104px; font-variant-numeric: tabular-nums;
  letter-spacing: .02em; }
.pname { display: flex; flex-direction: column; gap: 2px; }
.pname .nm-ko { font-weight: 400; }
.pname .nm-en { font-size: 12px; color: var(--muted); }

/* Filled: the bar says publishable or not; the boxes say which section is
   holding it up. Every box is done (green) or blocking (red) - nothing here
   is optional, so there's no third state to decode. */
.fillwrap { display: flex; flex-direction: column; gap: 5px; }
.fillrow { display: flex; align-items: center; gap: 8px; }
.bar { flex: 1; height: 6px; border-radius: 3px; background: #e2e7e7; overflow: hidden; }
.bar span { display: block; height: 100%; background: var(--green); }
.bar.short span { background: var(--rust); }
.state { font-size: 10.5px; letter-spacing: .04em; text-transform: uppercase;
  font-weight: 400; white-space: nowrap; }
.state.live { color: var(--green); }
.state.draft { color: var(--rust); cursor: help; }
.secs { display: flex; gap: 3px; }
.secs i { min-width: 38px; height: 14px; border-radius: 2px; display: flex;
  align-items: center; justify-content: center; font-size: 9px; font-style: normal;
  padding: 0 3px; cursor: help; background: #d4e8e0; color: #2f7d67; }
.secs i.miss { background: #f2ddd0; color: #a85a2e; }
/* The whole label is the click target, so the hit area is the box below -
   not just the glyph. Browsers cap how far they'll honour width/height on a
   native checkbox (they draw their own glyph at a fixed size), so the glyph
   is enlarged with transform: scale() instead. A transform doesn't affect
   layout, hence the explicit size on the wrapper. */
.svc-box { display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; border-radius: 6px; width: 44px; height: 34px; }
.svc-box:hover { background: #eef3f3; }
.svc-box input { accent-color: var(--accent); cursor: pointer;
  width: 16px; height: 16px; margin: 0;
  transform: scale(1.35); transform-origin: center; }

/* Square, matching the card. A wide cell implied a wide crop and the two
   disagreed. */
.photo-cell.ui-wide { width: 54px; height: 54px; }
.photo-cell.ui-wide img, .photo-cell.ui-wide .face-mini { width: 54px; height: 54px; border-radius: 4px; }
table.roster .col-photo:has(.ui-wide) { width: 66px; }

/* People popup */
.person-row { display: grid; grid-template-columns: 1fr auto auto auto; gap: 10px;
  align-items: center; padding: 7px 0; border-bottom: 1px solid var(--line); }
.person-row:last-child { border-bottom: none; }
.person-name { font-size: 13px; color: var(--ink); }
.person-row.raw .person-name { font-style: italic; color: var(--ink-2); }
.role-pick { display: flex; align-items: center; gap: 4px; font-size: 11px;
  color: var(--muted); cursor: pointer; white-space: nowrap; }
.role-pick input { accent-color: var(--accent); cursor: pointer; }
.role-pick:hover { color: var(--accent-ink); }
.role-pick.off { opacity: .3; cursor: default; }
.people-add { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 14px; }
.people-add select, .people-add input {
  background: var(--card-2); border: 1px solid var(--line-2); border-radius: 5px;
  padding: 8px 10px; color: var(--ink); font: inherit; font-size: 13px; width: 100%;
}
.people-add select:focus, .people-add input:focus { outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(95, 188, 203, .12); }
@media (max-width: 640px) { .detail-grid { grid-template-columns: 1fr; } }

/* Destination popup reuses .dest-opt from the old form */
#dest-modal .dest-opt { margin-bottom: 10px; }
#dest-modal .dest-opt input#dest-url {
  background: var(--card-2); border: 1px solid var(--line-2); border-radius: 5px;
  padding: 8px 10px; color: var(--ink); font: inherit; font-size: 13px; width: 100%;
}



/* ── News ──────────────────────────────────────────────── */
/* News is a reading column, not a full-bleed page. A card is a 300px photo
   beside a text column, and the words in it stop at --measure. The page is
   full-bleed by default (--page: none), so that column keeps growing while
   the text does not, which reads as a wide empty margin down the right.

   Cap the page at exactly what one card needs and the column ends where the
   text ends: 300px photo + --measure + 44px of .news-body padding + the two
   container gutters. Written as a sum rather than the 1056px it evaluates
   to, so it follows --measure and --gutter if either is ever retuned.

   Caps .container rather than .news-list on purpose: the eyebrow, the h1 and
   the count have to move with the cards, or the heading sits hard left while
   the cards are centred. Scoped to .news-page (set in news.html) so the
   homepage's .news-list.compact and every other page are untouched. */
.news-page .container {
  max-width: calc(300px + var(--measure) + 44px + 2 * var(--gutter));
}

.news-list { display: grid; gap: 14px; }
.news-card {
  display: grid; grid-template-columns: 300px 1fr;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--r);
  overflow: hidden;
  /* A floor, not a ceiling: every card is at least this tall so a row of
     short posts stays even, but a longer one grows to fit its text instead
     of hiding it. The photo cannot stretch the row - see .news-shot .shots,
     which is taken out of flow so a tall portrait contributes no height. */
  min-height: 210px;
}
@media (max-width: 720px) {
  .news-card { grid-template-columns: 1fr; min-height: 0; height: auto; }
  .news-shot { height: 200px; position: relative; }
}

/* Whole-tile link: covers the tile, above the photo but below the carousel
   controls, so clicking the image or text goes to /news while the arrows and
   dots still work. Top-level — must NOT be nested in the media query above,
   which was the bug that made this dead on desktop. */
/* .news-tile's look is defined in home.css; this only adds the cursor. */
.news-tile { cursor: pointer; }
.tile-link { position: absolute; inset: 0; z-index: 1; }
.news-shot .shot-nav,
.news-shot .shot-dots { z-index: 3; }
.news-shot .tile-text { z-index: 2; }  /* stays position:absolute from its base rule; just layered above the link */

.news-shot { position: relative; background: var(--card-2); overflow: hidden; }
/* Absolute, so the photo fills whatever height the text ends up needing and
   never contributes any of its own. Without this a portrait shot resolves to
   its intrinsic ratio and drags the whole row taller. */
.news-shot .shots { position: absolute; inset: 0; width: 100%; height: 100%;
  display: block; z-index: 0; }
.news-shot img {
  width: 100%; height: 100%; display: block;
  object-fit: cover;      /* fill the frame, crop the overflow */
  object-position: center;
}
/* `display: block` above would override the hidden attribute — HTML's hidden
   is only a UA `display: none`, and any author rule beats it. The carousel
   hides its off-screen shots with that attribute, so say so explicitly. */
.news-shot img[hidden] { display: none; }
.news-shot .shots.empty {
  display: flex; align-items: center; justify-content: center;
  color: var(--ink-2); height: 100%;
}
.news-shot .shots.empty svg { width: 30px; height: 30px; }
.shot-nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  background: rgba(8,9,9,.6); border: 1px solid var(--line-2); color: var(--ink);
  min-width: 44px; width: 44px; height: 44px; cursor: pointer;
  font-size: 19px; line-height: 1; opacity: 0; transition: opacity .15s;
}
.shot-nav.prev { left: 0; border-radius: 0 4px 4px 0; border-left: none; }
.shot-nav.next { right: 0; border-radius: 4px 0 0 4px; border-right: none; }
.news-shot:hover .shot-nav { opacity: 1; }
.shot-nav:hover { background: rgba(8,9,9,.85); color: var(--accent-ink); }
.shot-dots { position: absolute; bottom: 9px; left: 0; right: 0;
  display: flex; justify-content: center; gap: 5px; }
.shot-dots button { width: 5px; height: 5px; border-radius: 50%;
  background: rgba(255,255,255,.32); border: none; padding: 0; cursor: pointer;
  position: relative; }
/* The visible dot is 5px; the tap area is padded out so it is reachable. */
.shot-dots button::before { content: ""; position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%); width: 22px; height: 22px; }
.shot-dots button.on { background: var(--brand-lit); }

.news-body { padding: 18px 22px; display: flex; flex-direction: column; gap: 7px;
  min-height: 0; /* grid child: allow it to be smaller than its content box */ }
.news-cat { font-size: 10px; letter-spacing: 1.4px; text-transform: uppercase;
  color: var(--brand-lit); font-weight: 400; }
.news-body h3 { margin: 0; font-size: 17px; font-weight: 400; letter-spacing: -.2px;
  line-height: 1.35; }
.news-text { color: var(--ink-2); font-size: 13px; line-height: 1.65;
  max-width: var(--measure); }
.news-text p { margin: 0 0 9px; }
.news-text p:last-child { margin-bottom: 0; }
.news-date { margin-top: auto; padding-top: 10px; font-size: 12px; color: var(--muted); }

/* Homepage: same cards, tighter */
/* The homepage tiles are teasers and stay a fixed height, so here the text
   does clip - and fades rather than ending mid-word. That is honest on the
   homepage because /news is where the whole post is. On /news itself there is
   no further page to open, so nothing there is allowed to hide text. */
.news-list.compact .news-card { grid-template-columns: 200px 1fr; height: 140px; }
.news-list.compact .news-text {
  overflow: hidden; flex: 1; min-height: 0;
  -webkit-mask-image: linear-gradient(180deg, #000 72%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 72%, transparent 100%);
}
.news-list.compact .news-body { padding: 14px 17px; gap: 4px; }
.news-list.compact .news-body h3 { font-size: 15px; }
.news-list.compact .news-text { font-size: 12.5px; }
@media (max-width: 720px) {
  .news-list.compact .news-card { height: auto; }
  .news-list.compact .news-shot { height: 160px; }
}

/* ── The project edit window ────────────────────────────────
   Everything substantial about a project in one place: filling one in is a
   single sitting, not three round-trips through separate popups. */
.modal.win { max-width: 1080px; padding: 0; max-height: 88vh; display: flex;
  flex-direction: column; }
.win-head { display: flex; align-items: center; gap: 12px; padding: 16px 24px;
  border-bottom: 1px solid var(--line); }
.win-head h3 { margin: 0; font-size: 17px; flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Progress, visible while you work rather than only on close. */
.win-fill { display: flex; align-items: center; gap: 9px; width: 190px;
  flex: 0 0 auto; }
.win-fill .bar { flex: 1; }
.win-head .btn, .win-foot .btn { padding: 7px 15px; font-size: 13px; }
.win-body { padding: 18px 24px 4px; overflow-y: auto; flex: 1; }
.win-foot { display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 14px 24px; border-top: 1px solid var(--line); }
.foot-state { font-size: 12px; color: var(--muted); }

.tabs { display: flex; gap: 2px; margin-bottom: 18px; border-bottom: 1px solid var(--line); }
.tab { background: none; border: none; font: inherit; font-size: 12.5px;
  padding: 8px 14px; color: var(--muted); cursor: pointer;
  border-bottom: 2px solid transparent; }
.tab:hover { color: var(--ink); }
.tab.on { color: var(--accent-ink); border-bottom-color: var(--accent); font-weight: 400; }

.wsec { margin-bottom: 24px; }
.wsec h4 { margin: 0 0 10px; font-size: 12px; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted); font-weight: 400; }
.wsec h4 .opt { text-transform: none; letter-spacing: 0; color: #9aa2a2;
  font-weight: 400; }
.sub-h { margin: 18px 0 0; font-size: 12px; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted); font-weight: 400; }
.sub-h + .thumb-editor { margin-top: 10px; padding-top: 0; border-top: none; }
.wnote { font-size: 12px; color: var(--muted); margin: 0 0 12px; line-height: 1.55; }
.wnote.strong { background: #f3f7f8; border: 1px solid #dbe6e8; border-radius: var(--r);
  padding: 10px 12px; color: var(--ink-2); }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.grid3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; }
.f label { display: block; font-size: 11.5px; color: var(--muted); margin-bottom: 4px; }
.f input, .f select, .f textarea {
  width: 100%; font: inherit; font-size: 13px; padding: 8px 10px;
  border: 1px solid var(--line-2); border-radius: var(--r);
  background: var(--card-2); color: var(--ink); }
.f textarea { resize: vertical; min-height: 76px; line-height: 1.5; }
.f input:focus, .f select:focus, .f textarea:focus { outline: none;
  border-color: var(--accent); box-shadow: 0 0 0 2px rgba(15, 74, 86, .14); }
/* nowrap: the label wraps before the badge does otherwise, dropping the
   word onto its own line under a short field name. */
.req { color: var(--rust); font-size: 10px; font-weight: 400; margin-left: 4px;
  white-space: nowrap; }
.file-name { font-size: 12px; color: var(--muted); margin-left: 8px; }
@media (max-width: 720px) { .grid2, .grid3 { grid-template-columns: 1fr; } }


/* ── News editor ───────────────────────────────────────── */
.col-date  { width: 140px; }
.col-cat   { width: 120px; }
.col-title { width: 280px; }
#body-modal textarea {
  width: 100%; background: var(--card-2); border: 1px solid var(--line-2);
  border-radius: 5px; padding: 10px 12px; color: var(--ink); font: inherit;
  font-size: 13px; line-height: 1.6; resize: vertical;
}
#body-modal textarea:focus { outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(95, 188, 203, .12); }
.photo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
  gap: 9px; margin-bottom: 14px; }
.photo-tile { position: relative; aspect-ratio: 4 / 3; border-radius: 5px;
  overflow: hidden; border: 1px solid var(--line-2); }
.photo-tile.fresh { border-color: var(--green); }
.photo-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
.tile-x { position: absolute; top: 3px; right: 3px; width: 19px; height: 19px;
  border-radius: 50%; border: none; background: rgba(8,9,9,.8); color: var(--ink);
  cursor: pointer; font-size: 13px; line-height: 1; padding: 0; }
.tile-x:hover { background: #d1665c; }

/* The row thumbnail is a <button> (it opens the edit window) but is styled
   as the shared .photo-cell, which assumes a <label>. Reset the button
   chrome so it looks identical to the members page. */
button.photo-cell { background: none; border: none; padding: 0; font: inherit;
  appearance: none; }
/* NOTE: .photo-cell is defined in members.css (size, position, cursor). These
   two lines EXTEND it rather than redefine it - the projects thumbnail can be
   zoomed, so it must clip. Deliberate, but a shared class name across files is
   how .col-date and .col-status silently broke, so keep them complementary. */
.photo-cell { overflow: hidden; border-radius: 4px; }
.photo-cell img { transform-origin: center; }

/* A file input we open from JS. `hidden` (display:none) can't be .click()ed
   in some browsers, so take it out of the layout without removing it. */
.vis-hidden { position: absolute; width: 1px; height: 1px; padding: 0;
  margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
  border: 0; }

/* ── Thumbnail framing ──────────────────────────────────────
   The card crops to a square, so a portrait or panoramic photo loses its edges.
   This shows exactly that crop and lets you drag to choose what survives -
   the file itself is never altered, only a focal point is stored. */
.thumb-editor { display: grid; grid-template-columns: 260px 1fr; gap: 18px;
  align-items: start; margin-top: 18px; padding-top: 18px;
  border-top: 1px solid var(--line); }
.thumb-frame { position: relative; width: 260px; aspect-ratio: 1 / 1;
  border-radius: var(--r); overflow: hidden; background: #eceeee;
  border: 1px solid var(--line-2); cursor: grab; touch-action: none; }
.thumb-frame.dragging { cursor: grabbing; border-color: var(--accent); }
/* NOT object-fit: cover. Cover CROPS the image to the box and throws the
   overflow away, so panning could only move an already-cropped result - the
   discarded edges came back as grey. Instead the img is laid out at its full
   natural size (JS sets width/height to "just covers the frame") and moved
   with transform, so every part of it is reachable. */
/* top/left are set by the script, in the same terms CSS object-position
   uses — see projects_edit.js. */
.thumb-frame img { display: block; position: absolute; top: 0; left: 0;
  max-width: none; max-height: none;
  user-select: none; -webkit-user-drag: none; transform-origin: center; }
.thumb-empty { position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; color: var(--muted); font-size: 12.5px;
  pointer-events: none; }
/* display:flex above beats the `hidden` attribute, so say it explicitly -
   otherwise the placeholder sits on top of the image forever. */
.thumb-empty[hidden] { display: none; }
.thumb-side { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; }
.thumb-side .wnote { margin: 0 0 2px; max-width: 420px; }
.zoom-row { display: flex; align-items: center; gap: 10px; font-size: 12px;
  color: var(--muted); width: 100%; max-width: 340px; }
.zoom-row input[type="range"] { flex: 1; accent-color: var(--accent); cursor: pointer; }
.zoom-row span { font-variant-numeric: tabular-nums; min-width: 42px; text-align: right; }
.thumb-side .btn { padding: 6px 13px; font-size: 12.5px; }

@media (max-width: 720px) { .thumb-editor { grid-template-columns: 1fr; }
  .thumb-frame { width: 100%; } }

/* ── Photo drop zone ───────────────────────────────────── */
.drop-zone {
  border: 2px dashed var(--line-2); border-radius: var(--r);
  padding: 26px 20px; text-align: center; cursor: pointer;
  transition: border-color .15s, background .15s; margin-bottom: 16px;
}
.drop-zone:hover { border-color: var(--accent); background: rgba(95, 188, 203, .04); }
.drop-zone.over { border-color: var(--green); background: rgba(74, 158, 134, .08); }
.drop-icon { color: var(--muted); margin-bottom: 8px; }
.drop-icon svg { width: 26px; height: 26px; }
.drop-zone.over .drop-icon { color: var(--green); }
.drop-text { font-size: 14px; color: var(--ink-2); font-weight: 400; }
.drop-sub { font-size: 12px; color: var(--muted); margin-top: 3px; }
.photo-tile { cursor: grab; }
.photo-tile.dragging { opacity: .4; cursor: grabbing; }
/* The row's image cell previews photo 0 and opens the photos popup —
   it is not an upload of its own. */
.photo-cell.as-btn { border: none; background: none; padding: 0; cursor: pointer; }
.photo-cell.as-btn .photo-hover { font-size: 12px; font-weight: 400; color: var(--accent-ink); }
.hidden-cell { width: 0; padding: 0 !important; border: none !important; }
/* The news editor leads with its add row, so it needs no top gap. */
tr.band-row.new-band.first td { padding-top: 8px; }
.form-actions.top { padding-top: 0; margin-bottom: 18px; }

/* ── Photo viewer ──────────────────────────── */
/* Reuses .modal-back for the backdrop and the [hidden] rule; everything
   below overrides the boxed .modal look, because a photo wants the screen
   rather than a panel. */
.news-shot.can-zoom { cursor: zoom-in; }

.lightbox { padding: 0; }
.lb-frame {
  margin: 0; display: flex; flex-direction: column; align-items: center;
  gap: 10px; max-width: 92vw; max-height: 92vh;
}
/* The picture takes what is left after the bar; object-fit keeps it whole
   rather than cropping, since the point is to see the original. */
#lb-img {
  max-width: 92vw; max-height: 82vh; width: auto; height: auto;
  display: block; object-fit: contain; border-radius: 4px;
  background: var(--card-2);
}

.lb-bar {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  justify-content: center; width: 100%;
  font-size: 12.5px; color: var(--muted);
}
.lb-count { font-variant-numeric: tabular-nums; }
.lb-caption { color: var(--ink-2); }
.lb-acts { display: flex; gap: 8px; margin-left: auto; }
@media (max-width: 720px) { .lb-acts { margin-left: 0; } }

.lb-btn {
  background: var(--card); border: 1px solid var(--line-2); border-radius: 5px;
  color: var(--ink); font: inherit; font-size: 12.5px; padding: 6px 12px;
  cursor: pointer; text-decoration: none; line-height: 1.4;
}
.lb-btn:hover { border-color: var(--accent); }
.lb-btn:disabled { opacity: .55; cursor: default; }

/* Close and step controls sit on the backdrop, clear of the picture. */
.lb-x {
  position: absolute; top: 14px; right: 18px; z-index: 2;
  background: none; border: none; color: var(--muted);
  font-size: 30px; line-height: 1; cursor: pointer; padding: 4px 8px;
}
.lb-x:hover { color: var(--ink); }
.lb-nav {
  background: none; border: none; color: var(--muted); cursor: pointer;
  font-size: 44px; line-height: 1; padding: 0 14px; flex: 0 0 auto;
}
.lb-nav:hover { color: var(--ink); }
.lb-nav[hidden] { display: none; }
@media (max-width: 720px) {
  .lb-nav { position: absolute; top: 50%; transform: translateY(-50%); z-index: 2; }
  .lb-nav.prev { left: 2px; }
  .lb-nav.next { right: 2px; }
  .lb-frame { max-width: 100vw; }
}/* ── Homepage news tiles ───────────────────────────────────
   The photo is the card: title and date ride on it. Full cards with body
   text live on /news; this is a glance. */
.news-tile { border-radius: var(--r); overflow: hidden; border: 1px solid var(--line); }
/* One row, off the edge. Cards hold their width so the row stays a row. */
.news-rail .news-tile { flex: 0 0 250px; scroll-snap-align: start; }
.news-tile .news-shot { height: 190px; }
.news-tile .shots.empty { background: var(--card-2); }

/* A scrim, always on: the text has to survive a bright photo. */
.tile-text {
  position: absolute; inset: auto 0 0 0; padding: 34px 14px 12px;
  background: linear-gradient(180deg, transparent, rgba(4,6,6,.5) 42%, rgba(4,6,6,.88));
  pointer-events: none;
}
.tile-cat { font-size: 9px; letter-spacing: 1.4px; text-transform: uppercase;
  font-weight: 400; color: var(--brand-lit); margin-bottom: 4px; }
.tile-text h3 { margin: 0; font-size: 13.5px; font-weight: 400; line-height: 1.35;
  color: #fff; letter-spacing: -.1px;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.tile-date { font-size: 11px; color: rgba(255,255,255,.62); margin-top: 5px; }

/* Dots move out of the text's way. */
.shot-dots.corner { top: 9px; bottom: auto; right: 10px; left: auto; justify-content: flex-end; }
.news-tile .shot-nav { height: 44px; }/* ── Footer ────────────────────────────────────────────────
   The lab's whole surface in four columns: the roster, what we work on,
   the newsletter, and how to reach us. Small type, dense — it is a
   sitemap, not decoration. */
/* Same background as the page, deliberately: a footer with its own colour
   reads as chrome and gets skipped. This one carries the roster, so it has
   to read as content. A rule and space do the separating instead. */
.site-foot { border-top: 1px solid var(--line); padding: 38px var(--gutter) 0; }
/* Fixed widths at an even rhythm, starting from the left margin — the slack
   collects on the right rather than being spread into the gaps. Stretching
   to meet both edges is what makes a footer look like a form; even columns
   with room after them read as a set. */
/* Flex, not grid: the columns hold different things and want different
   widths — Newsletter is a list of months, Contact is an address. A grid
   forces one width on all of them. Flex wraps on its own, so there are
   still no breakpoints to guess wrong. */
/* Fixed-width columns at an even rhythm, anchored to the left margin; slack
   collects on the right (MIT Senseable footer). Every column is the SAME
   width, so the set reads as a grid rather than a ragged row, and they wrap
   as a block with no breakpoints. 15rem is the one knob that sets the rhythm. */
.foot-cols {
  display: grid;
  grid-template-columns: repeat(auto-fill, 15rem);
  justify-content: start;
  gap: 34px 40px;
  padding-bottom: 32px;
  align-items: start;
}
/* Every column is one uniform track. The Team roster is long (many names)
   but not wide (each row is just name + role), so it needs no extra width —
   spanning tracks only left an empty half-column beside it. */
.foot-col { min-width: 0; }

/* A column whose content doesn't exist yet. Says so rather than pretending. */
.foot-note.placeholder { color: var(--muted); font-style: italic; font-size: 11px; }

.foot-col h3 { margin: 0 0 10px; font-size: 11px; letter-spacing: 1.4px;
  text-transform: uppercase; color: var(--ink); font-weight: 400; }
.foot-note { margin: 0 0 9px; font-size: 12px; line-height: 1.55; color: var(--ink-2); }
.foot-note a { color: var(--ink-2); }
.foot-note a:hover { color: var(--brand-lit); text-decoration: none; }
.foot-sub { font-size: 10px; letter-spacing: 1.1px; text-transform: uppercase;
  color: var(--muted); margin: 11px 0 5px; font-weight: 400; }

/* One name per row. A packed grid saves space but reads as a word cloud;
   a single column reads as a roster. */
.foot-list { list-style: none; margin: 0; padding: 0; }
.foot-list li { display: flex; align-items: baseline; gap: 7px; padding: 0; line-height: 1.65; }
.foot-list.quiet .foot-name { color: var(--muted); }
.foot-name { font-size: 12px; color: var(--ink-2); }
a.foot-name:hover { color: var(--brand-lit); text-decoration: none; }
.foot-name.lead { font-size: 13.5px; font-weight: 400; color: var(--ink); }
.foot-tier { display: flex; align-items: baseline; gap: 7px; margin-bottom: 9px; }
.foot-role { font-size: 10.5px; color: var(--muted); font-style: italic; }

.foot-plain { list-style: none; margin: 0; padding: 0; }
.foot-plain li { font-size: 12px; color: var(--ink-2); line-height: 1.5;
  padding: 4px 0; border-bottom: 1px solid var(--line); }
.foot-plain li:last-child { border-bottom: none; }
.foot-plain a { color: var(--ink-2); display: block; }
.foot-plain a:hover { color: var(--brand-lit); text-decoration: none; }
.issues .issue-title { color: var(--muted); margin-left: 7px; }

.foot-more { display: inline-block; margin-top: 12px; font-size: 11.5px; color: var(--muted); }
.foot-more:hover { color: var(--brand-lit); text-decoration: none; }

.foot-base { display: flex; align-items: center; justify-content: space-between;
  gap: 16px; padding: 16px 0 20px; border-top: 1px solid var(--line);
  font-size: 11px; color: var(--muted); }
.col-month { width: 150px; }

.dept-btn .dept-label { display: flex; flex-direction: column; gap: 1px;
  min-width: 0; width: 100%; }
.dept-label.is-empty { display: block; font-size: 12px; line-height: 1.4; }
.dept-en, .dept-ko { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  display: block; }
.dept-en { font-size: 11.5px; color: var(--ink); line-height: 1.25; }
.dept-ko { font-size: 10.5px; color: var(--muted); line-height: 1.2; }
.col-dept .dept-btn { width: 100%; text-align: left; justify-content: flex-start;
  white-space: nowrap; overflow: hidden; align-items: flex-start;
  align-self: start; }   /* don't stretch to the row's height *//* ── Team assignment popup rows ────────────────────────────
   One row per membership: team dropdown + role, with a delete. Mirrors the
   link-row layout. */
.team-row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
.team-row .team-pick { flex: 1 1 auto; min-width: 0; }
.team-row .team-role { flex: 0 0 auto; }
.col-teams { width: 200px; max-width: 200px; }


/* ── Frozen header row + name column (like Excel 셀 고정) ──────
   Keeps the column names and each person's name visible while scrolling a
   wide/tall roster, so you never lose track of which cell belongs to whom. */

/* allow vertical sticky to work: the scroll box needs a height cap, otherwise
   there's nothing to scroll against vertically and the header can't stick. */
.roster-scroll { max-height: 78vh; overflow: auto; }

/* Header row sticks to the top. */
table.roster thead th { position: sticky; top: 0; z-index: 2; }

/* Name (KO) column sticks to the left — both its header and body cells. */
table.roster th.col-name-ko,
table.roster td.col-name-ko {
  position: sticky; left: 0; z-index: 1;
  background: var(--card);           /* opaque so scrolled cells don't show through */
}
table.roster thead th.col-name-ko { background: var(--card-2); }

/* The top-left cell is sticky on BOTH axes and must sit above everything. */
table.roster thead th.col-name-ko { z-index: 3; }

/* Body name cells get the row background; a subtle right edge separates the
   frozen column from the scrolling ones. */
table.roster td.col-name-ko { box-shadow: 1px 0 0 var(--line); }
table.roster thead th.col-name-ko { box-shadow: 1px 0 0 var(--line); }


/* ── Compact header for the roster editor ────────────────────
   Reclaims vertical space above the table so the scroll box starts on-screen
   (target: 1920x1080 Chrome). Scoped to .editor-compact so nothing else is
   affected. Row height and the table itself are untouched. */
.editor-compact { margin-top: -14px; }               /* pull up under the nav */
.editor-compact .eyebrow { margin-bottom: 2px; }
.editor-compact h1 { font-size: 19px; margin: 0 0 2px; }
.editor-compact .muted { font-size: 12px; margin: 0; }
/* Put the title block and the private-notice on one row, so the notice no
   longer costs its own vertical band. */
.editor-compact .toolbar { align-items: center; margin-bottom: 12px; gap: 20px; }
.editor-compact .ui-notice { margin: 0; padding: 7px 11px; font-size: 12px;
  flex: 0 1 auto; max-width: 46%; }




/* Self-explaining column headers. Shared by the member and project
   editors, so it lives here rather than in one editor's own file. */
/* A header that explains itself on hover, rather than relying on a title
   attribute nobody waits for. */
.help-th { position: relative; cursor: help; }
.help-th .tip {
  position: absolute; top: calc(100% + 6px); left: 50%; transform: translateX(-50%);
  width: 260px; padding: 10px 12px; border-radius: 6px; z-index: 30;
  /* Deliberately dark in BOTH themes - a dark tooltip on a light page is
     the convention, and it keeps the tip distinct from the table. */
  background: #142020; color: #f2f5f5; font-size: 12px; line-height: 1.6;
  font-weight: 400; text-transform: none; letter-spacing: 0;
  box-shadow: 0 10px 30px rgba(0,0,0,.22);
  opacity: 0; visibility: hidden; transition: opacity .13s;
  /* The <th> sets nowrap + uppercase for the column label; the tooltip is a
     sentence, so it has to opt out of both or it runs past its own box. */
  white-space: normal; word-break: keep-all; text-align: left;
}
/* Columns near the right edge would push their tooltip off the table, so
   those anchor to the right instead of centring. */
.help-th:last-of-type .tip,
.help-th.tip-right .tip { left: auto; right: 0; transform: none; }
.help-th:hover .tip { opacity: 1; visibility: visible; }


/* ── Audit log table ─────────────────────────────────────────
   Readable change history: entity + label, an action chip, and a from -> to
   diff with the old value dimmed and the new value highlighted. */
.audit-table td { font-size: 12px; white-space: nowrap; }
.audit-when { color: var(--muted); font-variant-numeric: tabular-nums; }
.audit-entity { text-transform: uppercase; font-size: 10px; letter-spacing: .6px;
  color: var(--muted); margin-right: 6px; }
.audit-label { color: var(--ink); }
.audit-field { color: var(--accent-ink); }
.audit-old { color: var(--muted); text-decoration: line-through; max-width: 260px;
  overflow: hidden; text-overflow: ellipsis; }
.audit-new { color: var(--ink); max-width: 260px; overflow: hidden; text-overflow: ellipsis; }
.audit-act { font-size: 10px; text-transform: uppercase; letter-spacing: .5px;
  padding: 2px 7px; border-radius: 999px; border: 1px solid var(--line-2); }
.audit-act-create { color: #6fceb0; border-color: rgba(111,206,176,.4); }
.audit-act-update { color: var(--accent-ink); border-color: rgba(95,188,203,.4); }
.audit-act-delete { color: #d98b8b; border-color: rgba(217,139,139,.4); }
/* Things that happened to files rather than to columns: a page uploaded, a
   photo replaced. Amber, so they read as a different kind of event from an
   ordinary edit. */
.audit-act-file   { color: #e0b062; border-color: rgba(224,176,98,.4); }/* A date that isn't a real calendar date. The server drops it (bist/dates.py),
   so say so before the save rather than let a typo vanish. */
input.bad-date { border-color: var(--rust) !important;
  box-shadow: 0 0 0 2px rgba(192, 118, 74, .16) !important; }


/* ── Which row is yours ─────────────────────────────────────
   The roster shows everyone - it's the lab's own directory, and hiding rows
   would make it useless. But you may only change your own (admins excepted).

   The distinction is carried by the FIELDS, not by a background tint: your
   row keeps its input borders, everyone else's loses them and reads as text.
   That's a much stronger signal than shading, because an input without a
   border simply doesn't look like something you can type in.

   The fills live in editor-light.css, which owns the table's colour system. */

/* Dark mode (the public-side palette) still needs its own band. */
tr.mine > td {
  background: rgba(95, 188, 203, .16);
  border-top: 1px solid rgba(95, 188, 203, .4);
  border-bottom: 1px solid rgba(95, 188, 203, .4);
}
tr.mine > td:first-child { box-shadow: inset 4px 0 0 var(--accent-ink); }

/* Everyone else's: inert, and drawn as text rather than as fields. */
tr.not-mine input,
tr.not-mine select,
tr.not-mine textarea,
tr.not-mine button { pointer-events: none; }
tr.not-mine input,
tr.not-mine select,
tr.not-mine textarea {
  background: transparent !important;
  border-color: transparent !important;
  box-shadow: none !important;
  color: var(--ink-2);
  padding-left: 2px;
}
tr.not-mine select { appearance: none; }
tr.not-mine .cell-btn {
  background: transparent; border-color: transparent; color: var(--muted);
}
tr.not-mine .photo-cell { pointer-events: none; opacity: .75; }
tr.not-mine .photo-cell .photo-hover { display: none; }
/* A read-only row has nothing to drag or delete, so its cells stay empty. */
tr.not-mine .drag-handle, tr.not-mine .del-box { display: none; }

/* Roster position: drag the row rather than type a number. */
.col-order { width: 34px; text-align: center; }
.drag-handle {
  display: inline-block; cursor: grab; user-select: none;
  color: var(--muted); font-size: 15px; line-height: 1; padding: 4px 2px;
}
.drag-handle:active { cursor: grabbing; }
tr:hover .drag-handle { color: var(--ink-2); }
/* Where the row will land. Drawn as an inset line so it costs no layout - a
   real border would shift every row by 2px the moment it appeared. The name
   column keeps its own right-hand edge, so it restates both shadows. */
tr.drop-above > td { box-shadow: inset 0 2px 0 var(--brand-lit); }
tr.drop-below > td { box-shadow: inset 0 -2px 0 var(--brand-lit); }
tr.drop-above > td.col-name-ko { box-shadow: inset 0 2px 0 var(--brand-lit), 1px 0 0 var(--line); }
tr.drop-below > td.col-name-ko { box-shadow: inset 0 -2px 0 var(--brand-lit), 1px 0 0 var(--line); }

/* The grip sits with the row, not at the top of it. */
.col-order { vertical-align: middle; }

tr.dragging { opacity: .5; }
tr.dragging > td { background: rgba(95, 188, 203, .12); }
body.editing tr.dragging > td { background: #d7e9ed; }


/* ORCID: a fixed-width identifier, so it gets tabular figures and room for
   all 19 characters. */
.edit-members .col-orcid { width: 168px; }
.orcid-in { font-variant-numeric: tabular-nums; letter-spacing: 0 !important; }


/* Fill state, same as the projects editor - it says why someone isn't on the
   public page yet. */
.edit-members .col-fill { width: 132px; }


/* Phone and full-date fields: fixed formats, so tabular figures and no
   inherited letter-spacing (which clipped the roster's date boxes). */
.tel-in, .date-full { font-variant-numeric: tabular-nums;
  letter-spacing: 0 !important; }


/* ── Photo framing ──────────────────────────────────────────
   The card crops to a circle, which is unforgiving: a group photo or an
   off-centre portrait loses the face. Same editor as the project thumbnail -
   drag to choose the focal point, zoom to fill - but the preview is round,
   because that's the shape the decision is actually about. */
.photo-editor { display: grid; grid-template-columns: 260px 1fr; gap: 20px;
  align-items: start; }
/* Must match the card's frame exactly (see .person .face). If these two
   ever disagree, the preview shows a crop the page will not reproduce. */
.photo-frame { position: relative; width: 260px; aspect-ratio: 1 / 1;
  border-radius: var(--r); overflow: hidden;
  /* Same fill as the card, so what you frame is what you get. */
  background: var(--card);
  border: 1px solid var(--line-2); cursor: grab; touch-action: none; }
.photo-frame.dragging { cursor: grabbing; border-color: var(--accent); }
/* top/left are set by the script, in the same terms CSS object-position
   uses — see apply() in members_edit.js. */
.photo-frame img { display: block; position: absolute; top: 0; left: 0;
  max-width: none; max-height: none; user-select: none;
  -webkit-user-drag: none; transform-origin: center; }
.btn.danger { color: var(--rust); border-color: rgba(192,118,74,.4); }
.btn.danger:hover { background: rgba(192,118,74,.1); border-color: var(--rust); }
body.editing .btn.danger { color: #a85a2e; border-color: #e0c6b3; }
body.editing .btn.danger:hover { background: #f8efe9; border-color: #a85a2e; }
.photo-side .btn.danger { margin-top: 6px; }

.photo-empty { position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; color: var(--muted); font-size: 12.5px;
  pointer-events: none; }
.photo-empty[hidden] { display: none; }
.photo-side { display: flex; flex-direction: column; gap: 10px;
  align-items: flex-start; }
.photo-side .wnote { margin: 0 0 2px; max-width: 380px; font-size: 12px;
  color: var(--muted); line-height: 1.55; }
@media (max-width: 720px) {
  .photo-editor { grid-template-columns: 1fr; }
  .photo-frame { width: 100%; height: auto; }
}
/* The framing leaves part of the frame empty. Amber, not red: it is a
   choice with a consequence, not a mistake. Sits under the zoom control
   because that is what changes it. */
.photo-warn { margin: 4px 0 0; padding: 9px 11px; border-radius: 7px;
              font-size: 12px; line-height: 1.5;
              color: var(--warn-ink); background: var(--warn-bg); border: 1px solid var(--warn-line); }

/* Which columns the publish gate asks for. Required or not — nothing in
   between, because a column that doesn't apply to someone already shows
   that in their own row.
   Deliberately quiet: a hint on a dense table, not a warning. */
.req { display: inline-block; width: 5px; height: 5px; margin-left: 5px;
       border-radius: 50%; background: var(--accent-ink); opacity: .55;
       vertical-align: 3px; }
th:hover .req { opacity: .9; }
.req-key { font-size: 11px; color: var(--muted); margin: 0 0 10px; }
.req-key .req { margin: 0 4px 0 0; }

/* "No Scopus profile" — the answer that isn't an ID. Sits under the field
   because it is the same question, not a separate one.

   inline-flex, not flex: a block-level flex row fills the cell, and since
   the cell is wider than "no profile" the box and its label were pushed to
   opposite edges. Shrink-to-fit keeps them together as one control. */
.no-id { display: inline-flex; align-items: center; gap: 5px; margin-top: 4px;
         font-size: 11px; color: var(--muted); white-space: nowrap;
         cursor: pointer; }
/* table.roster sets width:100% on every input, which stretched the box into
   a bar. A checkbox has its own size. */
.no-id input[type="checkbox"] { width: 13px; height: 13px; flex: none;
                                margin: 0; accent-color: var(--brand-lit);
                                cursor: pointer; }
.no-id:hover { color: var(--ink-2); }
.scopus-cell .scopus-in { margin-bottom: 0; }
/* When "no profile" is ticked the field above it is answered, not empty.
   Dimming it says so — otherwise it reads as a live field someone has
   wiped. :has() is the only way to reach a sibling's earlier element. */
.scopus-cell:has(.no-id input:checked) .scopus-in { opacity: .4; }
.scopus-cell:has(.no-id input:checked) .no-id { color: var(--ink-2); }
/* Wide enough for an 11-digit id and the label beneath it, so neither wraps. */
.edit-members .col-scopus { width: 150px; }

/* Above the table: the key note on the left, save on the right. Twenty rows
   put the old save bar off the bottom of the screen, so saving meant
   scrolling past the whole roster to reach it. */
.roster-head { display: flex; align-items: center; gap: 16px;
               flex-wrap: wrap; margin: 0 0 10px; }
.roster-head .req-key { margin: 0; }
.roster-head .form-actions { margin-left: auto; display: flex; gap: 8px; }



/* A team page's roster links to each person's card on /members. */
.tm-name a { color: inherit; text-decoration: none;
             border-bottom: 1px solid transparent; }
.tm-name a:hover { border-bottom-color: var(--accent-ink); }
/* The card being jumped to, so arriving from a team page is not a guess. */
.person:target { outline: 2px solid var(--brand-3); outline-offset: 10px;
                 border-radius: 6px; }

/* A team's own introduction, if it has written one. */
.team-lede { font-size: 15px; line-height: 1.75; color: var(--ink-2);
             max-width: 66ch; margin: 0 0 30px; }

/* Team chips on a member card — the roster's route in to these pages. */
.person-teams { display: flex; gap: 5px; flex-wrap: wrap; margin-top: 8px; }
.team-chip { font-size: 11px; padding: 2px 9px; border-radius: 99px;
             text-decoration: none; color: var(--brand-lit);
             background: var(--brand-2); border: 1px solid transparent; }
.team-chip:hover { border-color: var(--brand-lit); }
.person:target { outline: 2px solid var(--brand-3); outline-offset: 10px;
                 border-radius: 6px; }

/* ── The network ─────────────────────────────────────────────────
   The roster drawn as five team rings with their people around them, and
   anyone on more than one team bridging between. Drafted by 황여진 as a
   standalone page; these are its rules, minus the header and layout ones
   base.css already provides.

   The token block the draft carried is dropped, as its own comment asked:
   every name in it is one base.css already defines, so the editor's light
   theme keeps overriding them the same way. */
/* ══ 토큰 ═══════════════════════════════════════════════════════════════
   base.css 에서 1:1로 복사. 실제 빌드에서는 이 블록을 삭제한다. */
/* The three values this page adds of its own.

   They arrived inside a :root block the mockup carried, which also
   redefined twenty-one of base.css's tokens — every colour, the radius,
   the gutter — along with the header, the nav, h1, `a`, the universal
   box-sizing reset and a .container capped at 1240px.

   All of that was overriding the site on EVERY page, because style.css is
   one file. It is what kept putting a cap and a gutter back on a layout
   set to run full width, and I mistook it for a rule in base.css twice.
   Scoped to this page now, and only what is genuinely its own. */
.net, .people-grid {
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --gold: #e8c16a;
  --gold-2: rgba(232, 193, 106, .75);
  --gold-3: rgba(232, 193, 106, .38);
}
.sep { color: var(--muted); opacity: .45; }
.dept-note { margin: 0; font-size: 12px; color: var(--ink-2); text-align: right; line-height: 1.55; }
.dept-note span { display: block; color: var(--muted); font-size: 11px; }
@media (max-width: 700px) {
    /* The gutter belongs to base.css. This page carried a copy of the whole token block and it was overriding the site. */
  .dept-note { text-align: left; }
}
h2.sec { font-size: 11px; letter-spacing: 1.4px; text-transform: uppercase;
  color: var(--muted); margin: 0; font-weight: 400; }
/* ══ Research Network ═══════════════════════════════════════════════════

   이 영역의 색 사용 원칙 (중요):

   팀을 5가지 색으로 칠하는 안을 먼저 검증했다가 버렸다. 두 가지 이유다.
   (1) 다섯 개의 범주색은 색각 이상 조건에서 all-pairs 분리 기준을 통과하지
       못한다 — 특히 이 페이지처럼 다섯 색이 한 화면에 동시에 놓이는 경우.
   (2) 더 중요한 이유로, BIST 의 디자인 언어는 base.css 가 명시하듯 "틸 하나가
       연구실의 색"이고 앰버(--warn)는 "고장이 아니라 주목"에만 쓰는 두 번째
       악센트다. 다섯 개의 새 색은 이 시스템에 이물질이다.

   그래서 팀의 정체성은 색이 아니라 위치 · 이름 · 번호(01–05)가 지고,
   색은 상태(선택됨 / 연관됨 / 흐려짐)를 인코딩한다. 그 결과 앰버는 본래
   용도대로 단 하나의 의미에만 남는다 — 여러 팀에 걸친 "잇는 사람". */

.net { margin: 34px 0 0; border-top: 1px solid var(--line); padding-top: 26px; }
.net-head { display: flex; align-items: flex-end; justify-content: space-between;
  gap: 20px; flex-wrap: wrap; margin-bottom: 4px; }
.net-lede { max-width: 60ch; font-size: 13px; color: var(--ink-2); margin: 10px 0 0; line-height: 1.7; }
/* 팀 필터 — 그래프의 팀 노드와 완전히 같은 동작. 모바일에서는 이쪽이
   주 조작 수단이 된다. */
.net-filters { display: flex; flex-wrap: wrap; gap: 8px; margin: 20px 0 0; }
.tf {
  font: inherit; font-size: 12px; line-height: 1.4;
  display: inline-flex; align-items: center; gap: 8px;
  padding: 7px 13px; border-radius: 99px; cursor: pointer;
  background: var(--card); border: 1px solid var(--line-2); color: var(--ink-2);
  transition: border-color .15s, color .15s, background .15s;
}
.tf:hover { border-color: var(--brand-3); color: var(--ink); }
.tf .tf-i { font-family: var(--mono); font-size: 10px; color: var(--muted); letter-spacing: .06em; }
.tf .tf-n { font-family: var(--mono); font-size: 10.5px; color: var(--muted);
  padding-left: 8px; border-left: 1px solid var(--line-2); }
.tf[aria-pressed="true"] {
  background: var(--brand-2); border-color: var(--brand-lit); color: var(--ink);
}
.tf[aria-pressed="true"] .tf-i, .tf[aria-pressed="true"] .tf-n { color: var(--brand-lit); }
.tf.tf-all[aria-pressed="true"] { background: var(--brand-3); border-color: var(--brand-3); color: var(--on-brand); }
/* 무대: 그래프 + 설명 패널 */
/* Both columns are the same height, and that height does not move.

   The panel had none of its own, so it grew and shrank with whichever team
   was selected — a team with eight members and a long vision pushed the
   page down, and choosing an empty one pulled it back up. Reading it meant
   watching the page jump.

   The figure sets the height: its SVG is 1120×720, so at any column width
   it is 0.643 of that width tall. The panel stretches to match and scrolls
   inside itself when a team has more to say than fits. */
/* Both columns are exactly this tall, and the row does not grow.

   `align-items: stretch` alone was not enough: it makes each cell fill the
   row, but the ROW is as tall as its tallest item — so a team with eight
   members and a long vision still pushed it down and the page jumped. The
   row height is stated outright instead, and the panel scrolls inside it.

   520px is what the graph needs before its outer ring of members starts to
   crowd the frame. */
.net-stage { display: grid;
  grid-template-columns: minmax(0, 1.55fr) minmax(280px, .95fr);
  grid-template-rows: 520px;
  gap: 26px; margin-top: 22px; }
.net-figure { position: relative; min-width: 0;
  /* 홈 히어로와 같은 바탕: 성긴 어둠에 중앙만 아주 옅게 들린다 */
  background: radial-gradient(110% 85% at 50% 46%, rgba(31,111,92,.09), transparent 64%);
  border: 1px solid var(--line); border-radius: 10px; padding: 6px; /* The figure does NOT clip. It used to, for its rounded corners, and that
     is what cut the hover tooltip off at every edge — no amount of
     repositioning helps when the box itself is the scissors. The SVG clips
     itself at its own viewBox, so the graph never spilled out here anyway;
     only the tooltip did, and it is meant to.

     The corners are kept by rounding the background instead, which is the
     only thing that needed clipping. */
  overflow: visible;
  /* A fixed height, so the row does not resize as the window does. The SVG
     scales inside it: at 1120×720 its own proportions are preserved by
     preserveAspectRatio, and the figure simply gives it a box. 520px is
     what the graph needs before the outer ring of members starts to crowd
     the frame. */
  min-height: 0; }
#graph { display: block; width: 100%; height: 100%;
  touch-action: manipulation; }
/* ── 그래프 요소 ──────────────────────────────────────────────────────
   클래스 이름과 수치를 홈 히어로(hero-graph.js / hero.css)에서 그대로
   가져왔다. 두 그래프가 같은 어휘를 쓰면 한 사이트의 두 화면으로 읽힌다.

     .og-link  = 관계선            stroke brand-3, opacity .6
     .og-ring  = 노드 바깥 고리    stroke brand-3, fill none
     .og-dot   = 노드 속 점        fill brand-lit
     .og-name  / .og-count         이름은 y+30, 개수는 y+46
     .og-mote  = 배경 먼지         brand-lit, 9초 호흡
     .focus    = 무언가를 가리키는 중 — 관계없는 것들이 물러난다

   히어로에서 금색(#e8c16a)은 한가운데 "연구실" 노드다. 여기서는 그 자리에
   해당하는 것이 팀을 잇는 사람이므로 같은 금색을 쓴다. base.css 의 --warn
   대신 히어로의 값을 쓰는 이유는, 맞추려는 대상이 그 화면이기 때문이다. */
.og-link { stroke: var(--brand-3); stroke-width: 1.6; opacity: .6;
  transition: stroke .25s, opacity .25s, stroke-width .25s; }
.og-link.near { stroke: var(--brand-lit); opacity: .95; stroke-width: 2.3; }
.og-link.lit { stroke: var(--brand-lit); opacity: .85; stroke-width: 2.2; }
.og-link.gold { stroke: var(--gold-3); }
.og-link.gold.near, .og-link.gold.lit { stroke: var(--gold); opacity: .95; }
.og-grab { fill: transparent; }
.node { cursor: pointer; }
.node:focus { outline: none; }
.node:focus-visible .og-ring { stroke: var(--brand-lit); stroke-width: 2; }
.og-ring { fill: none; stroke: var(--brand-3); stroke-width: 1.9;
  transition: stroke .3s, opacity .3s, stroke-width .3s; }
.og-dot { fill: var(--brand-lit); opacity: .8; transition: fill .3s, opacity .3s; }
/* 글자 크기가 히어로보다 큰 이유:
   히어로의 SVG 는 화면을 꽉 채우므로 viewBox 단위가 곧 화면 픽셀이다.
   이 그래프는 폭 1120 의 viewBox 가 700px 남짓한 칸에 들어가므로 0.63배로
   줄어든다. 히어로와 같은 12.5px 로 적으면 화면에서는 7.8px 로 나와 읽을
   수 없다. 그래서 축소율만큼 키워 적는다 — 화면에서 히어로와 같은 크기로
   보이는 값이다. */
.og-name { fill: var(--ink); font-size: 19px; letter-spacing: .2px;
  text-anchor: middle; pointer-events: none; transition: fill .3s, opacity .3s; }
.og-count { fill: var(--ink-2); font-size: 15.5px; opacity: .9; text-anchor: middle;
  pointer-events: none; font-family: var(--mono); transition: opacity .3s; }
/* 구성원 — 히어로의 '자식 노드'(og-kid)와 같은 크기·같은 배치.
   이름이 점 옆으로 뻗어 나가므로 원 안에 이니셜을 우겨넣지 않아도 된다. */
.og-mname { fill: var(--ink-2); font-size: 17px; pointer-events: none;
  transition: fill .3s, opacity .3s; }
.node.mem:hover .og-ring { stroke: var(--brand-lit); stroke-width: 2.5; }
.node.mem:hover .og-dot { fill: #fff; opacity: 1; }
.node.mem:hover .og-mname { fill: var(--brand-lit); }
.node.team:hover .og-ring { stroke: var(--brand-lit); stroke-width: 2.6; }
.node.team:hover .og-dot { opacity: 1; }
/* 팀을 잇는 사람 — 히어로 한가운데 노드와 같은 금색 */
.node.mem.bridge .og-ring { stroke: var(--gold-2); stroke-width: 2.2; }
.node.mem.bridge .og-dot { fill: var(--gold); opacity: 1; }
.node.mem.bridge .og-halo { fill: none; stroke: var(--gold-3); stroke-width: 1.6;
  transition: opacity .3s, stroke .3s; }
.node.mem.bridge:hover .og-halo, .node.mem.bridge.on .og-halo { stroke: var(--gold); }
.node.mem.bridge:hover .og-mname { fill: var(--gold); }
/* 선택/연관 상태 */
.node.team.on .og-ring { stroke: var(--brand-lit); stroke-width: 3.1; }
.node.team.on .og-dot { opacity: 1; }
.node.team.on .og-name { fill: var(--brand-lit); }
.node.team.rel .og-ring { stroke: var(--brand-lit); stroke-width: 2.3; opacity: .9; }
.node.mem.on .og-ring { stroke: var(--brand-lit); stroke-width: 2.5; }
.node.mem.on .og-mname { fill: var(--ink); }
.node.mem.bridge.on .og-ring { stroke: var(--gold); }
/* 히어로와 같은 물러남: 가리키는 것과 무관한 것들이 옅어진다 */
#graph.focus .og-link:not(.near) { opacity: .12; }
#graph.focus .og-mote { opacity: .14; }
#graph.focus .node:not(.near) .og-name,
#graph.focus .node:not(.near) .og-mname { opacity: .3; }
#graph.focus .node:not(.near) .og-ring { opacity: .25; }
#graph.focus .node:not(.near) .og-count { opacity: .2; }
/* 팀을 골랐을 때 — 무관한 것은 아주 뒤로 */
.dim { opacity: .14; }
#graph .dim { transition: opacity .25s; }
/* 배경 먼지. 히어로의 og-mote 와 같은 색·같은 호흡.
   위치는 고정 시드로 한 번만 계산하므로 새로고침해도 같은 하늘이다. */
.og-mote { fill: var(--brand-lit); opacity: .34;
  animation: og-breathe 9s ease-in-out infinite; }
@keyframes og-breathe { 0%,100% { opacity: .2; } 50% { opacity: .5; } }
/* 그래프 위 툴팁 */
.g-tip {
  position: absolute; z-index: 5; pointer-events: none;
  background: var(--card); border: 1px solid var(--line-2); border-radius: 8px;
  padding: 9px 12px; box-shadow: 0 12px 34px rgba(0,0,0,.5);
  font-size: 12.5px; line-height: 1.5; max-width: 250px;
  opacity: 0; transform: translate(-50%, -100%); transition: opacity .12s;

/* Flipped below its node when there is no room above. The tooltip is
   translated up by its own height, so for a node in the top third of the
   figure it landed outside the box — and .net-figure clips its overflow,
   which is what cut it off. */
.g-tip.below { transform: translate(-50%, 0); }
}
.g-tip.show { opacity: 1; }
.g-tip b { font-weight: 400; color: var(--ink); display: block; }
.g-tip .t-ko { color: var(--ink-2); font-size: 11.5px; }
.g-tip .t-teams { color: var(--brand-lit); font-size: 11px; margin-top: 4px; display: block; }
.g-tip .t-hint { color: var(--muted); font-size: 10.5px; margin-top: 5px; display: block; }
/* ── 설명 패널 ──────────────────────────────────────────────────────── */
.net-panel { background: var(--card); border: 1px solid var(--line-2);
  border-radius: 10px; padding: 20px; min-width: 0;
  /* min-height:0 is what actually lets it scroll. A grid child's default
     minimum is its content, so without this it refuses to shrink and
     overflow-y never engages — the box just grows. */
  min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  /* A thin, quiet scrollbar. The browser default is a wide light-grey bar
     that sits on a dark panel looking like a mistake. `scrollbar-width` and
     `scrollbar-color` are the standard properties and cover Firefox; the
     ::-webkit rules below cover the rest. */
  scrollbar-width: thin;
  scrollbar-color: var(--line-2) transparent; }

.net-panel::-webkit-scrollbar { width: 8px; }
.net-panel::-webkit-scrollbar-track { background: transparent; }
.net-panel::-webkit-scrollbar-thumb {
  background: var(--line-2); border-radius: 4px;
  /* Inset from the edge, so it reads as part of the panel rather than
     stuck to its border. */
  border: 2px solid transparent; background-clip: content-box; }
.net-panel:hover::-webkit-scrollbar-thumb { background-color: var(--brand-3);
  background-clip: content-box; }
.np-idx { font-family: var(--mono); font-size: 10px; letter-spacing: .14em; color: var(--brand-lit); }
.np-title { font-size: 17px; font-weight: 400; margin: 6px 0 0; letter-spacing: -.2px; line-height: 1.35; }
.np-count { font-size: 12px; color: var(--muted); margin: 8px 0 0; font-family: var(--mono); }
.np-vision { font-size: 12.5px; line-height: 1.8; color: var(--ink-2); margin: 14px 0 0;
  padding-top: 14px; border-top: 1px solid var(--line); word-break: keep-all; }
.np-lbl { font-size: 10px; letter-spacing: 1.5px; text-transform: uppercase;
  color: var(--muted); margin: 18px 0 9px; }
.np-topics { display: flex; flex-wrap: wrap; gap: 6px; }
.np-topics span { font-size: 11px; color: var(--ink-2); background: var(--card-2);
  border: 1px solid var(--line); border-radius: 4px; padding: 3px 8px; }
.np-topics span.hot { color: var(--brand-lit); border-color: var(--brand-2); }
.np-people { display: flex; flex-direction: column; gap: 2px; }
.np-person {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  font: inherit; background: none; border: 0; border-radius: 6px;
  padding: 6px 8px; cursor: pointer; color: var(--ink-2);
}
.np-person:hover { background: var(--card-2); color: var(--ink); }
.np-person img { width: 30px; height: 30px; border-radius: 50%; object-fit: cover;
  border: 1px solid var(--line-2); flex: none; }
.np-ini { width: 30px; height: 30px; border-radius: 50%; flex: none;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--line-2); background: var(--card-2);
  font-family: var(--mono); font-size: 10px; color: var(--muted); }
.np-person .n { font-size: 12.5px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.np-person .r { font-size: 10.5px; color: var(--muted); margin-left: auto; white-space: nowrap; font-family: var(--mono); }
.np-person .b { color: var(--gold); font-size: 10px; }
/* 외부 프로필 버튼 두 개. 사이트 안을 도는 아래 링크들과 성격이 다르므로
   (여기서 나가면 BIST 를 떠난다) 한 단 위에서 더 큰 덩어리로 세운다. */
.np-profiles { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px; }
.pbtn {
  font: inherit; font-size: 12.5px; line-height: 1.4;
  display: flex; align-items: center; justify-content: center; gap: 6px;
  padding: 10px 12px; border-radius: var(--r); text-align: center;
  background: var(--card-2); border: 1px solid var(--line-2); color: var(--ink-2);
  cursor: pointer; transition: border-color .15s, color .15s, background .15s;
}
a.pbtn:hover { border-color: var(--brand-3); color: var(--brand-lit);
  background: var(--brand); text-decoration: none; }
.pbtn .ext { font-size: 11px; opacity: .7; }
/* 값이 없는 사람 — 눌리지 않는다. 테두리를 점선으로 바꿔, 색만 흐린 것이
   아니라 "여기에는 넣을 것이 아직 없다"로 읽히게 한다. */
.pbtn[disabled] {
  cursor: not-allowed; color: var(--muted); opacity: .55;
  background: transparent; border-style: dashed;
}
.np-links { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 18px;
  padding-top: 16px; border-top: 1px solid var(--line); }
.np-link { font-size: 12px; color: var(--ink-2); border: 1px solid var(--line-2);
  border-radius: var(--r); padding: 6px 11px; }
.np-link:hover { color: var(--brand-lit); border-color: var(--brand-3); text-decoration: none; }
/* ── 개요 패널: 핵심 숫자와 구성원 특성을 나눈다 ──────────────────────

   앞서 학부연구생과 "두 팀 이상"을 재학 구성원 아래로 들여쓰고 왼쪽에
   꺾은선을 그었다가 되돌렸다. 셋이 한 줄기로 묶이니 1 + 5 = 14 로 읽혔는데,
   그건 틀린 셈이다. 두 값은 재학 14명을 서로 다른 잣대로 센 것이고 —
   하나는 학위과정, 하나는 연구팀 참여 — 심지어 겹친다(장병준은 학부연구생
   이면서 두 팀에 걸쳐 있다). 합계로 읽힐 수 있는 모양을 쓰면 안 된다.

   그래서 위에는 서로 독립인 숫자 셋만 같은 크기로 나란히 두고, 특성은
   아래에 따로 두되 무엇을 기준으로 센 값인지 각 줄에 밝힌다. */
.np-keys { display: grid; grid-template-columns: repeat(4, 1fr); margin-top: 18px; }
/* 좁아지면 네 칸이 한 줄에 들어가지 않는다 — "재학 구성원"이 두 줄로 접히면
   숫자 밑줄이 어긋나 보인다. 두 칸씩 두 줄로 내린다. */
@media (max-width: 420px) {
  .np-keys { grid-template-columns: repeat(2, 1fr); row-gap: 14px; }
}
.np-key .v { display: block; font-family: var(--mono); font-size: 23px; line-height: 1.1;
  color: var(--brand-lit); padding-bottom: 10px; border-bottom: 1px solid var(--line); }
.np-key .k { display: block; padding: 10px 10px 0 0; font-size: 11.5px; color: var(--ink-2); }
/* 범례 한 줄.

   "두 팀 이상 5명"을 숫자 목록에 한 줄로 넣었더니, 위 세 숫자와 나란히 놓여
   14명 밖의 또 다른 인원처럼 보였다. 학부연구생 1명도 마찬가지였다. 둘은
   재학 14명을 각각 다른 잣대로 다시 센 값이라 애초에 같은 목록에 설 수 없다.

   숫자를 세로로 세우는 대신 문장으로 눕히면 그 오해가 생기지 않는다 —
   "14명 중 5명"이라고 적히는 순간 포함 관계가 글로 확정된다. 금색 세로선은
   이 문장이 관계도의 금색 고리를 설명하는 범례임을 말한다. */
.np-legend { display: block; margin: 22px 0 0; padding: 2px 0 2px 13px;
  border-left: 2px solid var(--gold); font-size: 12.5px; line-height: 1.75;
  color: var(--ink-2); word-break: keep-all; }
.np-legend b { font-weight: 400; color: var(--ink); }
.np-legend .sep { color: var(--muted); padding: 0 4px; }
/* ── 모바일 대체 뷰 ─────────────────────────────────────────────────────
   좁은 화면에서 노드-링크 그래프는 읽을 수 없다. 그래프를 축소해 우겨넣는
   대신 같은 데이터를 목록으로 바꿔 낸다 — 팀, 소속 인원, 그리고 어떤 팀과
   이어지는지. 정보 구조는 그대로 남는다. */
.net-list { display: none; margin-top: 20px; }
.nl-team { border: 1px solid var(--line-2); border-radius: 10px; background: var(--card);
  margin-bottom: 10px; overflow: hidden; }
.nl-btn { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  font: inherit; background: none; border: 0; padding: 14px 16px; cursor: pointer; color: var(--ink); }
.nl-btn .i { font-family: var(--mono); font-size: 10px; color: var(--brand-lit); }
.nl-btn .t { font-size: 13.5px; min-width: 0; }
.nl-btn .c { margin-left: auto; font-family: var(--mono); font-size: 11px; color: var(--muted); }
.nl-btn .caret { color: var(--muted); font-size: 11px; transition: transform .18s; }
.nl-team.open .nl-btn .caret { transform: rotate(90deg); }
.nl-body { padding: 0 16px 16px; border-top: 1px solid var(--line); }
.nl-body .np-vision { margin-top: 12px; padding-top: 12px; }
/* ══ 명단 ═══════════════════════════════════════════════════════════════ */
.roster { margin-top: 46px; }
/* ── 과정별 바로가기 ──────────────────────────────────────────────────
   페이지가 길어졌으므로 명단 머리에 과정 목록을 둔다. 숨기는 필터가 아니라
   앵커다 — 모든 과정이 계속 화면에 남고, 누르면 그 자리로 내려간다. */
.roster-head { display: flex; align-items: center; justify-content: space-between;
  gap: 18px; flex-wrap: wrap; margin-bottom: 6px; }
.course-nav { display: flex; flex-wrap: wrap; gap: 6px; }
.cn {
  font-size: 12px; line-height: 1.4; display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 12px; border-radius: 99px; text-decoration: none;
  background: var(--card); border: 1px solid var(--line-2); color: var(--ink-2);
  transition: border-color .15s, color .15s, background .15s;
}
.cn:hover { border-color: var(--brand-3); color: var(--ink); text-decoration: none; }
.cn-n { font-family: var(--mono); font-size: 10.5px; color: var(--muted); }
/* 지금 보고 있는 과정 */
.cn.on { background: var(--brand-2); border-color: var(--brand-lit); color: var(--ink); }
.cn.on .cn-n { color: var(--brand-lit); }
.band { display: flex; align-items: baseline; gap: 12px;
  margin: 40px 0 18px; padding-top: 22px; border-top: 1px solid var(--line);
  /* 헤더가 sticky 라, 앵커로 내려왔을 때 제목이 그 아래 깔리지 않도록 */
  scroll-margin-top: 78px; }
.band:focus { outline: none; }
.band:first-of-type { border-top: 0; padding-top: 0; margin-top: 26px; }
.band h3 { font-size: 12px; letter-spacing: 1.5px; text-transform: uppercase;
  color: var(--muted); margin: 0; font-weight: 400; }
.band .ko { font-size: 12px; color: var(--ink-2); letter-spacing: 0; text-transform: none; }
.band .n { margin-left: auto; font-family: var(--mono); font-size: 11px; color: var(--muted); }
/* ── 졸업생 구역 ──────────────────────────────────────────────────────
   재학 구성원과 한눈에 갈리되, 흐려서 덜 중요해 보이면 안 된다. 그래서
   투명도를 낮추는 대신 바탕을 한 겹 깔아 "다른 구역"으로 만든다. 글자색과
   대비는 위와 똑같이 유지한다. */
.alum-zone { margin-top: 44px; padding: 4px 24px 26px; border-radius: 12px;
  background: var(--card); border: 1px solid var(--line); }
.alum-zone .band { border-top: 0; margin-top: 26px; padding-top: 0; }
.alum-zone .band h3 { color: var(--gold); }
.alum-note { font-size: 12px; color: var(--muted); margin: 0 0 20px; line-height: 1.6; }
/* 팀을 고른 동안 — 이 구역은 그 질문의 대상이 아니다. 카드를 한 장씩
   흐리는 대신 구역째 물린다. */
.alum-zone { transition: opacity .25s; }
/* Same for the alumni block: it stays readable while a team is selected.
   It was faded to say "this section is not what you are asking about",
   which is true but costs the whole section's legibility to say it. */
.alum-zone.filtered { opacity: 1; }
/* 졸업생 카드 — 같은 뼈대에 내용만 덜어낸다. 사진은 조금 작고, 학과·기간·
   당시 팀이 중심이다. */
.person.alum { background: var(--card-2); }
.person.alum:hover { background: var(--card); border-color: var(--gold-3); }
.person.alum .face { width: 84px; }
.p-alum-meta { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px;
  margin: 8px 0 0; align-items: baseline; }
.p-alum-meta .k { font-size: 10.5px; color: var(--muted); white-space: nowrap; }
.p-alum-meta .v { font-size: 11.5px; color: var(--ink-2); font-family: var(--mono); }
.p-alum-meta .v.none { color: var(--muted); font-family: inherit; }
/* 졸업생의 팀은 과거 기록이라 누를 수 없다 — 관계도로 데려가면 지금 그 팀에
   있다는 뜻이 되어 버린다. */
.chip.static { cursor: default; background: rgba(232,193,106,.10);
  color: var(--gold); border-color: var(--gold-3); }
/* align-items 를 건드리지 않는다 = stretch(기본값).

   처음에는 start 로 두었는데, 그러면 카드가 저마다 제 내용만큼만 높아진다.
   관심분야가 하나인 사람과 셋인 사람이 나란히 서면 높이가 눈에 띄게
   어긋나고, 명단이 들쭉날쭉해 보인다. stretch 면 한 줄에 놓인 카드들이
   그 줄에서 가장 큰 카드의 높이를 함께 갖는다.

   고정 높이를 주지 않는 이유: 관심분야가 세 줄까지 늘어나는 사람이 있어
   한 값으로 묶으면 누군가는 잘리고 누군가는 빈다. 줄 단위로 맞추는 것이
   내용을 지키면서 정렬을 얻는 방법이다. */
.people-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
  gap: 26px 24px; }
/* 카드 — base.css 의 .person 구조를 따르되, 카드 전체가 하나의 버튼이다.
   (기존 members.css 는 사진 150px + 본문. 여기서도 사진이 본문 옆에 선다.) */
.person {
  display: flex; gap: 15px; align-items: flex-start; width: 100%; text-align: left;
  font: inherit; color: inherit; background: var(--card);
  border: 1px solid var(--line); border-radius: 10px; padding: 16px;
  cursor: pointer; transition: border-color .16s, background .16s, transform .16s;
}
.person:hover { border-color: var(--brand-3); background: var(--card-2); transform: translateY(-2px); }
/* Selecting a team no longer fades the cards it does not match.
   Twenty cards at 28% opacity is most of the page greyed out, and a person
   is not less real for working on something else — the graph above is where
   a team selection belongs, and it already answers the question by lighting
   its own nodes. A thin marker on the matching cards says the same thing
   without taking the colour out of everything around it. */
.person.dimmed { opacity: 1; }
.person.dimmed .p-teams .chip,
.person.dimmed .person-teams .chip { opacity: .45; }
/* "명단에서 보기"로 내려온 카드. 기존 사이트가 팀 페이지에서 넘어온 카드를
   .person:target 으로 표시하는 것과 같은 처리다(members.css 참고).
   .dimmed 보다 뒤에 두어, 팀이 걸린 채로 내려와도 흐려지지 않는다. */
/* The selected member. Picking someone in the graph rings their card here
   and puts them in the address bar; arriving at /members#member-12 rings
   them on load, which is how the homepage ontology hands over.

   It replaced a popup that repeated what the card already said, one person
   at a time, in a window you then had to close. */
.person.jumped { opacity: 1; outline: 2px solid var(--brand-lit);
  outline-offset: 6px; border-color: var(--brand-3);
  background: var(--card-2); }
/* And the node it came from, so the two ends of the selection agree. */
#graph .node.mem.picked .og-dot { fill: var(--brand-lit); }
#graph .node.mem.picked .og-name { fill: var(--ink); font-weight: 500; }
#graph .node.mem.picked circle:first-child { stroke: var(--brand-lit);
  stroke-width: 2; }
.person .face { flex: none; width: 104px; aspect-ratio: 1 / 1; border-radius: var(--r);
  overflow: hidden; background: var(--card-2); border: 1px solid var(--line-2); position: relative; }
.person .face img { display: block; width: 100%; height: 100%; object-fit: cover;
  /* Photos are in colour, and stay in colour.

   The draft loaded them greyscale and coloured them on hover. That is a
   common enough device, but a roster of grey faces reads as a page that has
   not finished loading — and there is nothing to reveal, since the colour
   was never hidden information. */
  transition: transform .4s; }
.person .face .ini { position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; font-family: var(--mono); font-size: 22px; color: var(--muted); }
/* align-self: stretch — 카드는 align-items:flex-start 라 사진이 위에 붙어
   정사각을 지키는데, 그 때문에 본문도 제 내용 높이에 머물렀다. 그러면 아래
   .p-bridge 의 margin-top:auto 가 밀어낼 여백이 없어 표시가 제각각 높이에
   선다. 본문만 카드 높이까지 늘려, 사진은 그대로 두고 바닥 정렬을 얻는다. */
.p-body { flex: 1; min-width: 0; display: flex; flex-direction: column;
  align-self: stretch; }
.p-name { font-size: 15.5px; font-weight: 400; margin: 0; letter-spacing: -.1px; }
.p-ko { font-size: 12.5px; color: var(--ink-2); margin: 1px 0 0; }
.p-role { font-size: 12px; color: var(--muted); margin: 7px 0 0; }
.p-role .en { color: var(--ink-2); }
/* 기존 members.css 의 .person-dept 와 같은 역할·같은 색 */
.p-dept { font-size: 11px; color: var(--accent-ink); margin: 6px 0 0; }
.p-teams { display: flex; gap: 5px; flex-wrap: wrap; margin-top: 10px; }
.chip {
  font: inherit; font-size: 10.5px; line-height: 1.5; padding: 2px 9px; border-radius: 99px;
  color: var(--brand-lit); background: var(--brand-2); border: 1px solid transparent;
  cursor: pointer; white-space: nowrap;
}
.chip:hover { border-color: var(--brand-lit); }
.chip.none { color: var(--muted); background: var(--card-2); border-color: var(--line-2); }
.p-int { margin: 11px 0 0; padding-left: 15px; }
.p-int li { font-size: 11.5px; line-height: 1.55; color: var(--ink-2); margin-bottom: 2px; }
/* margin-top:auto — 카드가 늘어나 생긴 여백을 이 줄 위로 몰아, 한 줄에 선
   카드들의 이 표시가 같은 높이에 오게 한다. 색은 관계도의 고리와 같은
   금색이다. 같은 뜻이 두 곳에서 다른 색으로 나오면 안 된다. */
.p-bridge { margin-top: auto; padding-top: 12px; font-size: 10.5px; color: var(--gold);
  font-family: var(--mono); letter-spacing: .04em; }
/* PI 카드 — 한 줄 전체를 쓴다 */
.person.lead { grid-column: 1 / -1; gap: 22px; padding: 22px; }
.person.lead .face { width: 150px; }
.person.lead .p-name { font-size: 19px; }
.lead-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; margin-top: 12px; }
.lead-cols .lbl { font-size: 10px; letter-spacing: 1.5px; text-transform: uppercase;
  color: var(--muted); margin: 0 0 6px; }
.lead-cols ul { margin: 0; padding-left: 15px; }
.lead-cols li { font-size: 11.5px; color: var(--ink-2); line-height: 1.6; margin-bottom: 3px; }
@media (max-width: 620px) { .lead-cols { grid-template-columns: 1fr; } }
.empty-note { grid-column: 1 / -1; font-size: 13px; color: var(--muted);
  border: 1px dashed var(--line-2); border-radius: 10px; padding: 22px; text-align: center; }
/* ══ 상세 팝업 ══════════════════════════════════════════════════════════
   <dialog> 를 쓴다. Escape 닫기와 포커스 가둠을 브라우저가 처리하므로
   직접 만든 모달보다 접근성이 안전하다. */
dialog.sheet {
  padding: 0; border: 1px solid var(--line-2); border-radius: 12px;
  background: var(--card); color: var(--ink);
  width: min(760px, calc(100vw - 32px)); max-height: 88vh;
  box-shadow: 0 30px 90px rgba(0,0,0,.6);
}
dialog.sheet::backdrop { background: rgba(5,6,6,.78); }
.sheet-in { padding: 26px; overflow-y: auto; max-height: 88vh; }
.sheet-x { position: absolute; top: 12px; right: 14px; background: none; border: 0;
  color: var(--muted); font-size: 26px; line-height: 1; cursor: pointer; padding: 2px 6px; }
.sheet-x:hover { color: var(--ink); }
.sheet-top { display: flex; gap: 22px; align-items: flex-start; }
.sheet-top .face { flex: none; width: 132px; aspect-ratio: 1/1; border-radius: 8px;
  overflow: hidden; border: 1px solid var(--line-2); background: var(--card-2); position: relative; }
.sheet-top .face img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sheet-top .face .ini { position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; font-family: var(--mono); font-size: 30px; color: var(--muted); }
.sheet h2 { margin: 0; font-size: 21px; font-weight: 400; letter-spacing: -.2px; }
.sheet .s-ko { font-size: 14px; color: var(--ink-2); margin: 2px 0 0; }
.sheet .s-role { font-size: 12.5px; color: var(--muted); margin: 10px 0 0; }
.sheet .s-dept { font-size: 11.5px; color: var(--brand-lit); margin: 8px 0 0; }
/* 라벨-값 표. 이름·직위가 "누구인가"라면 이쪽은 "언제부터, 어디로 연락하나"라
   성격이 달라 머리글에서 떼어 한 덩어리로 묶었다. dl 을 쓴 이유는 이게 실제로
   용어와 뜻의 목록이기 때문이다 — 스크린리더가 짝으로 읽는다. */
.s-meta { display: grid; grid-template-columns: auto 1fr; gap: 9px 20px;
  margin: 0; align-items: baseline; }
.s-meta dt { font-size: 11px; color: var(--muted); letter-spacing: .3px; white-space: nowrap; }
.s-meta dd { margin: 0; font-size: 12.5px; color: var(--ink-2);
  font-family: var(--mono); min-width: 0; overflow-wrap: anywhere; }
.s-meta dd .none { color: var(--muted); }
.sheet-block { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line); }
.sheet-block .np-lbl { margin-top: 0; }
.sheet-block ul { margin: 0; padding-left: 16px; }
.sheet-block li { font-size: 12.5px; color: var(--ink-2); line-height: 1.7; margin-bottom: 3px; }
.hist li { list-style: none; margin-bottom: 8px; }
.hist { padding-left: 0 !important; }
.hist .hd { font-family: var(--mono); font-size: 10.5px; color: var(--brand-lit);
  display: inline-block; min-width: 46px; }
.hist .inst { display: block; color: var(--muted); font-size: 11.5px; padding-left: 46px; }
.sheet-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 26px; }
@media (max-width: 640px) {
  .sheet-cols { grid-template-columns: 1fr; gap: 0; }
  .sheet-top { flex-direction: column; gap: 16px; }
  .sheet-top .face { width: 110px; }
  .sheet-in { padding: 20px; }
}
/* 가안 미리보기용 토스트 — 아직 확정되지 않은 링크를 눌렀을 때 */
.toast {
  position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%) translateY(10px);
  background: var(--card); border: 1px solid var(--warn-line); border-radius: 8px;
  padding: 11px 16px; font-size: 12.5px; color: var(--ink-2); z-index: 90;
  box-shadow: 0 14px 40px rgba(0,0,0,.5); opacity: 0; pointer-events: none;
  transition: opacity .2s, transform .2s; max-width: calc(100vw - 40px);
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.toast code { font-family: var(--mono); color: var(--warn); font-size: 12px; }
/* ══ 반응형 ═════════════════════════════════════════════════════════════ */
@media (max-width: 1080px) {
  /* Stacked, so the stated row height has no job left — the panel sits
     below the figure rather than beside it, and each takes its own. */
  .net-stage { grid-template-columns: 1fr; grid-template-rows: auto; }
  .net-figure { order: 1; }
  #graph { height: auto; }
  .net-panel { order: 2; overflow-y: visible; }
}

@media (max-width: 760px) {
  /* 그래프를 목록으로 교체 */
  .net-figure { display: none; }
  .net-list { display: block; }
  .people-grid { grid-template-columns: 1fr; }
  /* 좁은 화면에서 24px 안쪽 여백은 카드를 두 번 조인다 — 페이지 거터 위에
     구역 여백이 또 얹히기 때문이다. */
  .alum-zone { padding-left: 14px; padding-right: 14px; }
  .person.lead { flex-direction: column; }
  .person.lead .face { width: 120px; }
  .brand small { display: none; }
}/* ── Blog ──────────────────────────────────────────────── */
/* Same reasoning as .news-page: the page is full-bleed by default
   (--page: none), and a column of prose that runs the width of a monitor is
   unreadable. A blog post is nothing but prose, so it is capped tighter than
   news — --measure plus the container's own gutters, and no more. */
/* The same width as the news page, written the same way: a 220px cover
   plus a text column that stops at --measure. The two pages sit next to
   each other in the same menu and should not be different widths. */
.blog-page .container {
  max-width: calc(300px + var(--measure) + 44px + 2 * var(--gutter));
}
/* A post is prose and nothing else, so it keeps the narrower column. */
.blog-single .container { max-width: calc(var(--measure) + 2 * var(--gutter)); }

.blog-list { display: grid; gap: 18px; }

.blog-card {
  position: relative;
  display: grid; grid-template-columns: 220px 1fr;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--r);
  overflow: hidden; min-height: 150px;
}
/* No cover: the text simply takes the whole card. */
.blog-card:not(:has(.blog-shot)) { grid-template-columns: 1fr; }
.blog-card:hover { border-color: var(--line-2); }

/* Whole-card link, under the text so selection still works. */
.blog-link { position: absolute; inset: 0; z-index: 1; }

.blog-shot { position: relative; background: var(--card-2); overflow: hidden; }
.blog-shot img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: center; display: block;
}

.blog-body { padding: 18px 22px; display: flex; flex-direction: column; gap: 8px;
  min-height: 0; }
.blog-meta { display: flex; align-items: center; gap: 10px;
  font-size: 12px; color: var(--muted); }
/* base.css styles bare h2 as a section label: 11px, uppercase, muted. A
   card title is a title, so every one of those has to be undone here -
   that grey was why the list read washed out next to the news page, whose
   card titles are h3 and inherit nothing. */
.blog-body h2 { margin: 0; font-size: 17px; line-height: 1.35;
  color: var(--ink); font-weight: 400; letter-spacing: -.2px;
  text-transform: none; }
.blog-excerpt { margin: 0; color: var(--ink-2); font-size: 13px; line-height: 1.65; }
.blog-more { margin-top: auto; font-size: 12.5px; color: var(--accent); }

@media (max-width: 720px) {
  .blog-card { grid-template-columns: 1fr; min-height: 0; }
  .blog-shot { height: 190px; }
}

/* ── A post ────────────────────────────────────────────── */
.post-head { margin: 0 0 26px; }
.post-head h1 { margin: 0 0 10px; line-height: 1.3; }
.post-meta { display: flex; align-items: center; gap: 12px;
  font-size: 12.5px; color: var(--muted); }
.post-note {
  margin: 14px 0 0; padding: 8px 12px; font-size: 12.5px; color: var(--muted);
  background: var(--card-2); border-radius: 5px;
}

/* The body is rendered from Markdown, so these style tags this file never
   writes by hand. Anything mistune can emit needs a rule here. */
.post-body { color: var(--ink); font-size: 15px; line-height: 1.8; }
.post-body > *:first-child { margin-top: 0; }
.post-body p { margin: 0 0 1.1em; }
/* Same reason as .blog-body h2: undo base.css section-label styling. */
.post-body h2 { margin: 1.8em 0 .6em; font-size: 20px; line-height: 1.35;
  color: var(--ink); font-weight: 400; letter-spacing: -.2px;
  text-transform: none; }
.post-body h3 { margin: 1.6em 0 .5em; font-size: 17px; }
.post-body h4 { margin: 1.4em 0 .4em; font-size: 15px; }
.post-body a { color: var(--accent); }
.post-body ul, .post-body ol { margin: 0 0 1.1em; padding-left: 1.4em; }
.post-body li { margin: .3em 0; }
.post-body blockquote {
  margin: 1.4em 0; padding: 2px 0 2px 16px;
  border-left: 2px solid var(--line-2); color: var(--ink-2);
}
.post-body code {
  background: var(--card-2); border-radius: 3px; padding: 1px 5px;
  font-size: .9em;
}
.post-body pre {
  background: var(--card-2); border: 1px solid var(--line); border-radius: 5px;
  padding: 14px 16px; overflow-x: auto;
}
.post-body pre code { background: none; padding: 0; }
.post-body table { border-collapse: collapse; margin: 1.4em 0; width: 100%; }
.post-body th, .post-body td {
  border: 1px solid var(--line); padding: 7px 10px; text-align: left;
  font-size: 13.5px;
}
.post-body hr { border: none; border-top: 1px solid var(--line); margin: 2em 0; }

/* Figures are wide and the column is narrow, so let them breathe. */
.post-body img {
  display: block; max-width: 100%; height: auto;
  margin: 1.6em auto; border-radius: 4px; background: var(--card-2);
}

/* ── Between posts ─────────────────────────────────────── */
.post-nav {
  display: flex; justify-content: space-between; gap: 20px;
  margin-top: 46px; padding-top: 22px; border-top: 1px solid var(--line);
}
.post-nav a { display: flex; flex-direction: column; gap: 4px;
  max-width: 46%; text-decoration: none; }
.post-nav-next { text-align: right; margin-left: auto; }
.post-nav-label { font-size: 12px; color: var(--muted); }
.post-nav-title { font-size: 13.5px; color: var(--ink); line-height: 1.45; }
.post-nav a:hover .post-nav-title { color: var(--accent); }
@media (max-width: 720px) {
  .post-nav { flex-direction: column; }
  .post-nav a { max-width: 100%; }
  .post-nav-next { text-align: left; margin-left: 0; }
}

/* ── Blog editor ─────────────────────────── */
/* The editor needs room, so it opts out of the narrow reading column the
   public pages use. */
.editing.blog-page .container { max-width: 1180px; }

.blog-edit-list { width: 100%; border-collapse: collapse; }
.blog-edit-list td { padding: 10px 12px; border-bottom: 1px solid var(--line);
  vertical-align: middle; }
.be-date { width: 110px; color: var(--muted); font-size: 12.5px;
  font-variant-numeric: tabular-nums; }
.be-title a { font-size: 14.5px; }
.be-slug { display: block; color: var(--muted); font-size: 11.5px; margin-top: 2px; }
.be-langs { width: 92px; text-align: right; white-space: nowrap; }
.be-tag { display: inline-block; margin-left: 4px; padding: 1px 6px;
  border: 1px solid var(--line-2); border-radius: 3px;
  font-size: 10.5px; color: var(--ink-2); }
.be-tag.is-off { color: var(--muted); opacity: .45; }
.be-go { width: 74px; text-align: right; font-size: 12.5px; }

.blog-form { display: flex; flex-direction: column; gap: 14px; }
.bf-row { display: flex; gap: 14px; flex-wrap: wrap; }
.bf-row label { flex: 1 1 220px; display: flex; flex-direction: column; gap: 5px;
  font-size: 12.5px; color: var(--muted); }
.bf-row input, .bf-row select {
  font: inherit; font-size: 14px; padding: 7px 9px; color: var(--ink);
  background: var(--card); border: 1px solid var(--line-2); border-radius: 5px;
}

.bf-tabs { display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  border-bottom: 1px solid var(--line); padding-bottom: 8px; }
.bf-tab {
  font: inherit; font-size: 13px; padding: 5px 12px; cursor: pointer;
  background: none; border: 1px solid transparent; border-radius: 5px;
  color: var(--muted);
}
.bf-tab.is-on { color: var(--ink); border-color: var(--line-2); background: var(--card); }
.bf-tools { margin-left: auto; display: flex; gap: 4px; align-items: center; }
.bf-tool {
  font: inherit; font-size: 12.5px; min-width: 30px; padding: 5px 9px;
  cursor: pointer; color: var(--ink-2);
  background: var(--card); border: 1px solid var(--line-2); border-radius: 5px;
}
.bf-tool:hover { border-color: var(--accent); color: var(--ink); }
.bf-upload { display: inline-flex; align-items: center; }

/* Writing on the left, the rendered post on the right. */
.bf-split { display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
  align-items: stretch; min-height: 460px; }
.bf-body {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px; line-height: 1.7; padding: 14px 16px; resize: vertical;
  min-height: 460px; color: var(--ink);
  background: var(--card); border: 1px solid var(--line-2); border-radius: 6px;
}
.bf-body[hidden] { display: none; }
.bf-preview {
  padding: 14px 18px; overflow-y: auto; max-height: 70vh;
  background: var(--card-2); border: 1px solid var(--line); border-radius: 6px;
  color: var(--ink); font-size: 14.5px; line-height: 1.75;
}
/* The preview holds the same HTML the post page holds, so it borrows the
   post-body rules rather than restating them. */
.bf-preview img { display: block; max-width: 100%; height: auto; margin: 1.2em auto;
  border-radius: 4px; }
.bf-preview h2 { font-size: 18px; margin: 1.4em 0 .5em; }
.bf-preview h3 { font-size: 16px; margin: 1.2em 0 .4em; }
.bf-preview p { margin: 0 0 1em; }
.bf-preview a { color: var(--accent); }
.bf-preview ul, .bf-preview ol { padding-left: 1.4em; margin: 0 0 1em; }

.bf-actions { display: flex; align-items: center; gap: 14px; }
.bf-save {
  font: inherit; font-size: 14px; padding: 9px 22px; cursor: pointer;
  color: #fff; background: var(--accent); border: none; border-radius: 6px;
}
/* Back and Cancel: quieter than Save, since neither is the thing you came
   here to do. */
.bf-top { display: flex; align-items: center; gap: 12px; }
.bf-back {
  font-size: 12.5px; color: var(--muted); text-decoration: none;
  padding: 6px 12px; border: 1px solid var(--line-2); border-radius: 5px;
}
.bf-back:hover { color: var(--ink); border-color: var(--accent); }
.bf-cancel {
  font-size: 13px; color: var(--muted); text-decoration: none;
  padding: 9px 18px; border: 1px solid var(--line-2); border-radius: 6px;
}
.bf-cancel:hover { color: var(--ink); border-color: var(--accent); }

.bf-danger { margin-top: 40px; padding-top: 18px; border-top: 1px solid var(--line); }
.bf-delete {
  font: inherit; font-size: 12.5px; padding: 6px 14px; cursor: pointer;
  color: var(--muted); background: none;
  border: 1px solid var(--line-2); border-radius: 5px;
}
.bf-delete:hover { color: #c0392b; border-color: #c0392b; }

@media (max-width: 900px) {
  .bf-split { grid-template-columns: 1fr; }
  .bf-preview { max-height: 340px; }
}/* The build guide, beside the upload button. Quiet - it is a reference, not
   an action - but present, because the person building the page usually is
   not the person at this screen. */
.dest-guide { display: inline-block; margin-left: 10px; font-size: 12px;
  color: var(--muted); text-decoration: underline; }
.dest-guide:hover { color: var(--accent); }

/* The live-service toggle, moved into the window from the row. */
.f-check { display: flex; align-items: flex-start; gap: 9px; margin-top: 14px;
  font-size: 13px; cursor: pointer; }
.f-check input { margin-top: 3px; }
.f-check .hint { display: block; font-size: 11.5px; color: var(--muted);
  margin-top: 2px; }
/* ── Light palette, scoped to the editors ─────────────────── */
html.editing, body.editing {
  --paper: #f4f5f5;      /* page */
  --card: #ffffff;       /* the table, panels, modals */
  --card-2: #ffffff;     /* inputs: white, so fields read as writable */
  --line: #e3e6e6;       /* hairlines */
  --line-2: #c9cfcf;     /* input borders — darker, so boxes are findable */

  --ink: #141a1a;        /* body text */
  --ink-2: #3d4646;      /* secondary text */
  --muted: #656e6e;      /* labels, hints — AA on both white and paper */

  /* The brand teal is a dark fill meant for white text; that still works on a
     light page. What must change is accent-ink: the pale #5fbccb is invisible
     on white, so links/emphasis use the deep teal instead. */
  --brand: #0a3d47;
  --brand-2: #10525f;
  --brand-3: #0f4a56;
  --on-brand: #ffffff;
  --brand-lit: #0f4a56;

  --accent: #0f4a56;
  --accent-ink: #0b3a44;

  --green: #2f7d67;      /* darkened: the dark-mode greens fail on white */
  --rust: #a85a2e;
  --blue: #1c6d84;

  background: var(--paper);
  color: var(--ink);
}

/* A few places hard-code dark values rather than using the variables. */
body.editing .site-header {
  background: rgba(255, 255, 255, .92);
  border-bottom: 1px solid var(--line);
}
body.editing .modal-back { background: rgba(20, 26, 26, .38); }

/* The name badge is a dark teal pill; in light mode its text must be white
   (it uses --ink, which is now near-black - dark on dark). */
body.editing .badge { background: var(--brand); color: #ffffff; border-color: var(--brand-2); }

/* Selected destination option: dark-mode fills it with the dark brand. On a
   light page that would put dark text on a dark panel, so tint it instead. */
body.editing .dest-opt:has(input[type="radio"]:checked) {
  background: #e8f1f3; border-color: var(--accent);
}
body.editing .modal { box-shadow: 0 24px 70px rgba(0, 0, 0, .18); }


/* Inputs sit on white, so they need a visible edge to read as fields. */
body.editing input,
body.editing select,
body.editing textarea {
  background: #ffffff;
  border-color: var(--line-2);
  color: var(--ink);
}
/* Native widgets (the <datalist> dropdown, autofill panel, date pickers,
   scrollbars) are painted by the BROWSER, not by our CSS - an <option> does
   not inherit the select's colours. The browser decides light or dark from
   `color-scheme`, which it reads off the ROOT element; setting it on <body>
   is not enough for popups drawn outside the body's box. So the editor
   templates set it on <html> (see base.html), and we back that up here. */
html.editing, body.editing { color-scheme: light; }
body.editing select option { background: #ffffff; color: var(--ink); }

/* Autofill paints its own background over ours; the huge inset shadow is the
   standard way to force it back. */
body.editing input:-webkit-autofill,
body.editing input:-webkit-autofill:hover,
body.editing input:-webkit-autofill:focus,
body.editing textarea:-webkit-autofill,
body.editing select:-webkit-autofill {
  -webkit-text-fill-color: var(--ink);
  -webkit-box-shadow: 0 0 0 40px #ffffff inset;
  box-shadow: 0 0 0 40px #ffffff inset;
}

/* Text selection inside the editors. */
body.editing ::selection { background: #cfe4e8; color: var(--ink); }
/* NOTE: members.css hard-codes `background: #1c2020` on
   `table.roster input:focus` - a dark hex, not a variable, so the light
   palette can't reach it. Focus styles here must therefore set the
   background explicitly, not just the border. */
body.editing input:focus,
body.editing select:focus,
body.editing textarea:focus,
body.editing table.roster input:focus,
body.editing table.roster select:focus,
body.editing table.roster textarea:focus {
  background: #ffffff;
  color: var(--ink);
  border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(15, 74, 86, .14);
}
/* Same for the hover state, which uses --card-2 (fine) but pairs with the
   dark focus above. */
body.editing table.roster input:hover,
body.editing table.roster select:hover {
  background: #ffffff;
  border-color: var(--line-2);
}

/* The other hard-coded darks, for the same reason: they name a hex rather
   than a variable, so redefining the palette doesn't reach them.
     base.css  .field input:focus                       -> #1c2020
     base.css  input[type=file]::file-selector-button:hover -> #1f2323
     home.css  .btn:hover                               -> #1f2323          */
body.editing .field input:focus,
body.editing .field select:focus,
body.editing .field textarea:focus {
  background: #ffffff; color: var(--ink); border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(15, 74, 86, .12);
}
body.editing .field input:hover,
body.editing .field select:hover,
body.editing .field textarea:hover { border-color: var(--line-2); }
body.editing .field input[type="file"]::file-selector-button {
  background: #f2f4f4; color: var(--ink); border-color: var(--line-2);
}
body.editing .field input[type="file"]::file-selector-button:hover {
  background: #e7ecec; border-color: var(--accent);
}
body.editing .btn:hover { background: #f0f4f5; }
body.editing .btn.primary:hover { background: var(--accent-ink); }
body.editing input::placeholder,
body.editing textarea::placeholder { color: #9aa2a2; }


/* Band rows (the group separators) were dark spacers; make them a light rule. */
body.editing .band-row > td {
  background: #e8ebeb; color: var(--ink-2);
  border-top: 1px solid var(--line-2); border-bottom: 1px solid var(--line-2);
}

/* Buttons: the plain ones need a border to exist on white. */
body.editing .btn { border: 1px solid var(--line-2); }
body.editing .btn.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
body.editing .cell-btn { background: #f2f4f4; border: 1px solid var(--line-2); color: var(--ink-2); }
body.editing .cell-btn:hover { background: #e7ecec; color: var(--ink); }


/* The page uses the site gutter like every other public page. It was on
   .ui-narrow, which is a 420px notice column — fine for a one-line message,
   absurd for a list of a hundred papers. */
.pub-filters { display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
               margin: 8px 0 6px; padding-bottom: 18px;
               border-bottom: 1px solid var(--line); }
.pub-filters select { width: auto; }
.pub-filters input[type="search"] { flex: 1; min-width: 200px; }
.pf-toggle { display: flex; align-items: center; gap: 6px;
             font-size: 13px; color: var(--muted); white-space: nowrap; }
.pf-clear { font-size: 12px; padding: 4px 10px; }
/* Only appears once something is filtered, so the page isn't shouting a
   count at a reader who hasn't asked a question yet. */
.pf-count { font-size: 12px; color: var(--muted); }

/* Long titles are the thing being read; keep the line length sane even
   though the page itself runs full width. */
.pub-top h3 { max-width: 68ch; }

/* Year headings: a rule that runs to the edge, the year sitting in it. */
.pub-year { display: flex; align-items: center; gap: 10px; margin: 26px 0 2px; }
.pub-year h2 { font-size: 19px; font-weight: 400; margin: 0; }
.pub-year .py-count { color: var(--muted); font-size: 12px; }
.pub-year::after { content: ""; flex: 1; height: 1px; background: var(--line); }

.pub { padding: 14px 0; border-bottom: 1px solid var(--line); }
.pub-top { display: flex; gap: 10px; align-items: flex-start; }
.pub-top h3 { font-size: 15px; font-weight: 400; line-height: 1.45; margin: 0; flex: 1; }
.pub-top h3 a { color: inherit; text-decoration: none; border-bottom: 1px solid transparent; }
.pub-top h3 a:hover { border-bottom-color: var(--accent-ink); }

.pub-class { font-size: 11px; padding: 2px 7px; border-radius: 3px;
             white-space: nowrap; color: var(--accent-ink);
             background: rgba(95, 188, 203, .12); }
.pub-wip { color: var(--muted); background: rgba(134, 141, 140, .12); }

/* Lab members are bolder than the rest of the byline. This is the one place
   the site uses weight to mean something, so it is 500 rather than bold. */

.pub-meta { font-size: 13px; color: var(--muted); margin: 3px 0 0; }
.pub-pdf { margin-left: 10px; color: var(--accent-ink); text-decoration: none;
           border-bottom: 1px solid transparent; }
.pub-pdf:hover { border-bottom-color: var(--accent-ink); }
.pub-pdf-m { color: var(--muted); }

/* Under review: present, but visibly not yet published. */
.pub-review { opacity: .82; }

.pub-legend { display: flex; gap: 18px; font-size: 12px;
              color: var(--muted); margin: 16px 0 40px; }
.pub-legend sup { color: var(--accent-ink); }

@media (max-width: 640px) {
  .pub-filters input { min-width: 0; width: 100%; }
  .pub-top { flex-wrap: wrap; }
}

/* ── The editor table: what the colours mean ────────────────
   There were six background rules on these cells - zebra striping, hover, a
   private-column tint with its own zebra and hover variants, the your-row
   highlight, and the drag state - and they fought by specificity rather than
   by meaning. Which one won was accidental, so the table read as noise.

   One rule replaces them: the background says what a row IS, never where it
   sits. Position is conveyed by lines, not fills.

     plain          nothing to say
     your row       the one you may edit
     dragging       the one you're moving
     private cols   internal, marked by a tint that survives all of the above

   No zebra striping: it colours rows by parity, which means nothing, and it
   was the loudest thing on the page. Row separation comes from the hairline
   borders instead. */

body.editing table.roster tbody tr > td {
  background: #ffffff;
  border-bottom: 1px solid var(--line);
}
body.editing table.roster thead th {
  background: #f2f5f5; color: var(--ink-2);
  border-bottom: 1px solid var(--line-2);
}

/* Hover: the faintest possible nudge, so you can follow a row across a wide
   table without it competing with the states that carry meaning. */
body.editing table.roster tbody tr:hover > td { background: #fbfcfc; }

/* Your row - the one state that matters most, so it's the strongest. */
body.editing table.roster tbody tr.mine > td {
  background: #d7eaef;
  border-top: 1px solid #93bfc8;
  border-bottom: 1px solid #93bfc8;
}
body.editing table.roster tbody tr.mine > td:first-child {
  box-shadow: inset 4px 0 0 var(--accent-ink);
}
body.editing table.roster tbody tr.mine:hover > td { background: #cfe5eb; }
/* Fields inside it stay white, so they read as fields against the band. */
body.editing tr.mine input,
body.editing tr.mine select,
body.editing tr.mine textarea { background: #fff; border-color: #8fb9c1; }

/* The lab-internal columns keep their tint in every row state - it says
   "don't show this outside", which doesn't stop being true on hover. */
body.editing table.roster th.private-col { color: var(--accent-ink); }
body.editing table.roster td.private-col { background: #f4f7f4; }
body.editing table.roster tr:hover > td.private-col { background: #eff4ef; }
body.editing table.roster tr.mine > td.private-col { background: #cfe4e2; }
body.editing table.roster td:not(.private-col) + td.private-col,
body.editing table.roster th.col-sid { border-left: 2px solid #9fc4b8; }

/* Being dragged: clearly lifted, and it outranks the rest while it lasts. */
body.editing table.roster tbody tr.dragging > td { background: #e8eff0; }

/* Rows you may not edit: no fill at all. The inputs lose their borders (see
   members.css), which is what actually says "not a form" - a background tint
   would just be another colour to decode. *//* ── The editor ──
   Inherited: body.editing the light palette, table.roster the grid,
   .band-row the separators, .f/.grid2/.wsec the popup form rows. What's
   here is column proportion and the two cells that aren't plain fields. */

/* members.css styles `table.roster input, table.roster select` but not
   textarea, because no other editor puts one in a cell — without this the
   title box falls back to ~20 characters in a wide cell. */
.edit-publications table.roster textarea {
  width: 100%; background: transparent; border: 1px solid transparent;
  border-radius: 4px; padding: 6px 8px; color: var(--ink); font: inherit;
  font-size: 13px; line-height: 1.45; resize: none;
}
.edit-publications table.roster textarea:hover { border-color: var(--line-2); background: var(--card-2); }
.edit-publications table.roster textarea:focus {
  outline: none; border-color: var(--accent); background: var(--card-2);
}

/* Titles wrap to three lines rather than running half the screen wide. A
   long single line is the hardest thing in a table to read: the eye has to
   travel, and nothing else in the row lines up beside it. */
/* table.roster is width:max-content so the roster's 18 columns can scroll.
   That makes percentage widths meaningless — the browser sizes each column
   from its content, which is why an editable row (sized by its inputs) and a
   read-only row (sized by its text) came out different widths in the same
   column. Seven columns fit, so this table is fixed instead: the widths in
   the header decide, and every row obeys them regardless of what it holds. */
/* The scroll box is 78vh on the roster, which suits a page whose only other
   content is a heading. This one also carries an intro, a toolbar of import
   buttons and a save bar, so 78vh pushed Save just past the fold — the one
   control you always need. Size the box from what's actually around it
   instead of a fixed fraction, with a floor so it doesn't collapse to
   nothing on a short screen. */
.edit-publications .roster-scroll {
  max-height: calc(100vh - 330px);
  min-height: 320px;
}

.edit-publications table.roster {
  table-layout: fixed; width: 100%; min-width: 1080px;
}
/* Summing to 100 with the delete column (admins) leaves the other 4% to be
   shared out when it's absent — which is what you want, since a member sees
   the same table minus one narrow column. */
.edit-publications .col-pub-title   { width: 25%; }
.edit-publications .col-pub-authors { width: 15%; }
.edit-publications .col-pub-where   { width: 18%; }
.edit-publications .col-pub-status  { width: 10%; }
.edit-publications .col-pub-doi     { width: 12%; }
.edit-publications .col-pub-pdf     { width: 11%; }
.edit-publications .col-pub-more    { width:  5%; }
.edit-publications .col-del         { width:  4%; }
/* Fixed layout won't grow a cell to fit, so anything long has to wrap
   rather than spill over its neighbour. */
.edit-publications table.roster td,
.edit-publications table.roster th { overflow-wrap: anywhere; }
.edit-publications .col-pub-doi input { font-size: 12px; }
/* Sits under the field, quiet until you want it. */
.doi-go { display: inline-block; margin: 3px 0 0 8px; font-size: 11px;
          color: var(--accent-ink); text-decoration: none;
          border-bottom: 1px solid transparent; }
.doi-go:hover { border-bottom-color: var(--accent-ink); }
.doi-go[hidden] { display: none; }
.edit-publications .col-pub-pdf input[type="file"] { max-width: 100%; }
/* Venue on top, year and index beneath — they answer one question together,
   and stacking them buys the title column its width back.
   NOTE specificity, not order: `table.roster select` scores (0,1,2) and sets
   width:100%, so a bare `.where-row select` at (0,1,1) loses however late it
   appears. Qualify to outrank it, or the select eats the row. */
.where-row { display: flex; gap: 5px; margin-top: 4px; }
.edit-publications table.roster .where-row input  { flex: 1 1 auto; width: auto; min-width: 0; }
.edit-publications table.roster .where-row select { flex: 0 0 84px; width: 84px; }

/* Filled in from the journal rather than chosen — worth showing, so nobody
   wonders where the value came from. */
.class-in.auto-filled { border-style: dashed; }

/* Authors: one block each, because a comma-joined line of five names has to
   be read where blocks are scanned. The whole cell is the button that opens
   the popup, so there is no small control to aim at. */
.col-pub-authors .au-open {
  display: flex; flex-direction: column; align-items: stretch; gap: 3px;
  width: 100%; padding: 4px; background: none; border: 1px solid transparent;
  border-radius: 4px; cursor: pointer; text-align: left; font: inherit;
}
.col-pub-authors .au-open:hover { border-color: var(--line-2); background: var(--card-2); }
.au-chip {
  display: block; padding: 2px 7px; border-radius: 3px; font-size: 12px;
  color: var(--ink); background: rgba(95, 188, 203, .10);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Standing shown as colour rather than as another word in the cell. */
.au-chip.professor { background: rgba(95, 188, 203, .20); }
.au-chip.member    { background: rgba(95, 188, 203, .10); }
.au-chip.alumnus   { background: rgba(95, 188, 203, .10); color: var(--muted); font-style: italic; }
.au-chip.external  { color: var(--muted); background: rgba(134, 141, 140, .10); }
.au-chip.empty     { color: var(--muted); background: none; font-style: italic; }
.au-chip sup       { color: var(--accent-ink); font-size: 10px; margin-left: 2px; }

.has-pdf { color: var(--accent-ink); margin-left: 4px; }
.edit-publications .col-pub-pdf select { margin-top: 4px; }

/* A field the public page needs and this paper hasn't got, named. */
.miss { display: inline-block; margin: 3px 4px 0 0; padding: 1px 6px;
        border-radius: 3px; font-size: 11px;
        color: var(--muted); background: rgba(134, 141, 140, .14); }

/* Queue rows read as text, not fields: nothing to edit until confirmed. */
.pq-cell { display: flex; align-items: center; gap: 12px; }
.pq-authors { flex: 1; color: var(--muted); font-size: 12px; }
.pq-acts { display: flex; gap: 6px; white-space: nowrap; }
tr.pq-accepted { opacity: .45; }
tr.pq-rejected { opacity: .3; text-decoration: line-through; }

/* Authors popup: one row per name.
   The controls match .bullet-text, which the interests and history popups
   use — and crucially carry `font: inherit`. Without it a form control
   falls back to the browser's own UI font, which is why this popup didn't
   look like the rest of the site. */
.au-row { display: flex; gap: 7px; align-items: center; margin-bottom: 6px; }
.au-row input[type="text"], .au-row .au-name,
.au-row .au-member, .au-row .au-kind {
  background: var(--card-2); border: 1px solid var(--line-2); border-radius: 5px;
  padding: 8px 10px; color: var(--ink); font: inherit; font-size: 13px;
}
/* One column, one meaning: what the author is, then which author.
   The name option carries both spellings — "Chang Saewon · 장세원" — so it
   needs the room; the standing column is four fixed words and doesn't. */
.au-row .au-kind { flex: 0 0 140px; width: 140px; }
.au-row .au-who  { flex: 1 1 auto; min-width: 0; display: flex; }
.au-row .au-name, .au-row .au-member { flex: 1 1 auto; width: 100%; min-width: 0; }
/* The flags are the same two words on every row: pin them so the name
   column keeps whatever is left rather than shrinking around them. */
.au-row .au-flag { flex: 0 0 auto; }
/* A name that came from the roster: shown, not editable — the member record
   owns that spelling. */
.au-row .au-name.from-member { color: var(--ink); background: transparent;
                               border-color: transparent; font-weight: 500; }
.au-flag { display: flex; align-items: center; gap: 5px; font-size: 12px;
           color: var(--muted); white-space: nowrap; }
.au-del { background: none; border: none; color: var(--muted);
          cursor: pointer; font-size: 16px; padding: 0 4px; }
.au-del:hover { color: var(--ink); }

/* ── Header: the brand ─────────────────────────────────── */
/* The logo holds its size. It used to shrink - the brand box has min-width: 0
   - but the <img> inside kept its intrinsic width and spilled out from under
   it, over the nav. The breakpoint below is the real answer: take the nav out
   of the row before the two can compete. max-width is only a guard for
   anything narrower than this layout anticipates. */
.brand { flex: 0 0 auto; }
.brand img { max-width: 100%; }

/* ── Header: the menu button ───────────────────────────── */
/* Hidden above the breakpoint, so on a wide screen it is not in the layout or
   the tab order at all. */
.nav-toggle { display: none; flex: 0 0 auto;
  width: 40px; height: 34px; padding: 8px 7px;
  background: none; border: 1px solid var(--line-2); border-radius: 6px;
  cursor: pointer; }
.nav-toggle span { display: block; height: 1.5px; background: var(--ink-2);
  border-radius: 1px; transition: transform .2s ease, opacity .2s ease; }
.nav-toggle span + span { margin-top: 4px; }
.nav-toggle:hover span { background: var(--ink); }
.nav-toggle:hover { border-color: var(--line-2); }

/* Open: the bars become a cross, so the button says what pressing it does. */
.nav-open .nav-toggle span:nth-child(1) { transform: translateY(5.5px) rotate(45deg); }
.nav-open .nav-toggle span:nth-child(2) { opacity: 0; }
.nav-open .nav-toggle span:nth-child(3) { transform: translateY(-5.5px) rotate(-45deg); }

/* ── Header: narrow ────────────────────────────────────── */
/* One narrower step first, which buys a little room before anything changes
   shape. */
@media (max-width: 1180px) {
  nav.main { grid-auto-columns: 84px; }
  .site-header { gap: 16px; }
}

/* The nav becomes a panel behind the button here, and here rather than at
   some phone width, because this is where the row genuinely runs out: five
   slots at 84px is 420, the actions about 200, the logo about 180, and the
   gaps another 50. The full nav is only shown when it actually fits.

   The same markup is re-laid, not duplicated - a second list of links is a
   list that goes out of date. */
@media (max-width: 1040px) {
  .nav-toggle { display: block; }
  .site-header { position: relative; }

  nav.main { display: none; }
  .nav-open nav.main {
    display: block; position: absolute; left: 0; right: 0; top: 100%;
    height: auto; padding: 6px var(--gutter) 14px;
    background: var(--paper); border-bottom: 1px solid var(--line);
    box-shadow: 0 12px 24px rgba(0, 0, 0, .35);
  }

  .nav-open nav.main > a,
  .nav-open nav.main .nav-drop { display: block; width: 100%; height: auto; }
  /* text-align as well as justify-content: the row version sets both, and
     leaving the first behind is what left some items centred and some not. */
  .nav-open nav.main > a { justify-content: flex-start; text-align: left;
    padding: 11px 0; font-size: 14px; border-bottom: 1px solid var(--line); }
  /* The underline grows from the centre of a slot; in a list it appears under
     the wrong thing. */
  .nav-open nav.main > a::after { display: none; }

  /* Teams and Posts stop being hover menus and become labelled sections:
     there is no hover here, and a menu inside a menu is a trap. The panel
     supplies the surface, so these lose the borders and shadow they have as
     popups - that leftover frame is the small white box that appeared. */
  .nav-open .nav-drop-btn { width: 100%; justify-content: flex-start;
    text-align: left; padding: 12px 0 6px; font-size: 11px;
    letter-spacing: 1.5px; text-transform: uppercase; color: var(--muted);
    cursor: default; }
  .nav-open .nav-drop-btn::after { display: none; }
  .nav-open .nav-menu {
    position: static; display: block; opacity: 1; visibility: visible;
    transform: none; pointer-events: auto;
    background: none; border: 0; box-shadow: none; border-radius: 0;
    min-width: 0; width: auto; padding: 0 0 6px 14px; margin: 0;
  }
  .nav-open .nav-menu a { display: block; text-align: left;
    padding: 9px 0; font-size: 14px; border-radius: 0; }
}/* A paper you are not an author of: present and readable, not editable.

   The text has to sit in the same box an input would. A cell inherits 15px
   from the body, while `table.roster input` is explicitly 13px inside a
   padded, bordered box — so plain text in a cell came out larger and
   differently aligned, and the row read as a different typeface. Match the
   input's metrics exactly: same size, same padding, a transparent border
   holding the same 1px, and the baselines line up down the column. */
.edit-publications tr.ro-row td {
  font-size: 13px; line-height: 1.45; color: var(--muted);
}
.edit-publications tr.ro-row td > :first-child,
.edit-publications tr.ro-row .ro-cell {
  display: block; padding: 7px 8px; border: 1px solid transparent;
}
/* The title is the thing you read, so it keeps full contrast. */
.edit-publications tr.ro-row .col-pub-title { color: var(--ink); }
/* Author blocks already carry their own padding; don't double it. */
.edit-publications tr.ro-row .col-pub-authors { padding-top: 5px; }
.ro-sub { font-size: 12px; color: var(--muted); margin-top: 3px; }
.ro-note { font-size: 11px; color: var(--muted); font-style: italic; }

/* ── The public page ──
   Search first, then the shape of the output, then the list. Chart and
   figures side by side: the trend answers "is this group active", the
   numbers answer "how much and how well cited". */

/* base.css styles `.field input`, not bare controls — so an <input> outside
   a .field falls back to the browser's own chrome, which is white with dark
   text. On a dark page that reads as a hole. Give the public page's controls
   the same treatment .field gets. */
.publications input[type="search"],
.publications input[type="text"],
.publications select {
  background: var(--card); border: 1px solid var(--line-2);
  border-radius: 7px; padding: 9px 12px; color: var(--ink);
  font: inherit; font-size: 13px;
}
.publications select { padding-right: 26px; }
.publications input::placeholder { color: var(--muted); }
.publications input:hover, .publications select:hover { border-color: var(--ink-2); }
.publications input:focus, .publications select:focus {
  outline: none; border-color: var(--brand-lit); background: var(--card-2);
}
/* The dropdown list itself is painted by the browser, not by the page. */
.publications select option { background: var(--card); color: var(--ink); }
.publications .btn { font-size: 13px; }

.pub-search { display: flex; align-items: center; gap: 10px; position: relative;
              margin: 6px 0 18px; }
.pub-search select { width: auto; }
.pub-search input[type="search"] { flex: 1; min-width: 0; padding-right: 34px; }
.ps-icon { position: absolute; right: 0; pointer-events: none; color: var(--muted); }
.pub-search .ps-icon { position: static; }
.ps-sync { font-size: 12px; color: var(--muted); white-space: nowrap; }

.pub-dash { display: grid; grid-template-columns: 1.45fr 1fr;
            gap: 12px; margin-bottom: 22px; align-items: stretch; }
.pub-card { border: 1px solid var(--line); border-radius: 12px;
            padding: 16px 18px; background: var(--card); }
.card-kicker { font-size: 10px; color: var(--muted); text-transform: uppercase;
               letter-spacing: .8px; margin: 0 0 6px; }
.chart-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 6px; }
.chart-head h2 { font-size: 19px; font-weight: 400; margin: 0; }
.chart-span { margin-left: auto; font-size: 12px; color: var(--muted); }

/* The chart is drawn by publications.js into .chart-holder. Before it runs —
   and for anyone without JavaScript — the holder contains the same figures
   as a list, which is also what a screen reader gets. */
.chart-holder { min-height: 250px; }
.chart-fallback { list-style: none; margin: 0; padding: 8px 0 0;
                  display: flex; flex-wrap: wrap; gap: 6px 18px; }
.chart-fallback li { font-size: 13px; color: var(--ink); }
.chart-fallback span { color: var(--muted); margin-right: 4px; }

.line-chart { width: 100%; height: 250px; display: block; overflow: visible; }
.lc-fill { fill: var(--accent-ink); opacity: .10; }
.lc-line { fill: none; stroke: var(--accent-ink); stroke-width: 2;
           stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.lc-dot { fill: var(--card); stroke: var(--accent-ink); stroke-width: 2;
          vector-effect: non-scaling-stroke; }
.lc-dot.last { fill: var(--warn); stroke: var(--warn); }
.lc-n { fill: var(--ink); font-size: 13px; text-anchor: middle; }
.lc-x { fill: var(--ink-2); font-size: 12px; text-anchor: middle; }

/* Four figures, two by two, beside the chart. */
.dash-figs { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
/* A filtered tile says so in its own label line, which is already there —
   an extra element would take a row out of this 2x2 and push the tiles out
   of line with the chart beside them. */
.fig.filtered { border-color: var(--brand-3); }
.fig.filtered .fig-l { color: var(--brand-lit); }
/* Each tile is a button: the number and the list it describes are the same
   thing seen two ways. Styled as a panel, not as a control — it should read
   as a figure that happens to respond. */
/* A tile is a figure, not a control. They were briefly clickable and it
   was wrong: two are totals with no subset behind them, and the two that
   did filter also reordered, so a tap looked like the page misbehaving.
   Filtering belongs to the controls below, where a reader looks for it. */
.fig { border: 1px solid var(--line); border-radius: 12px;
       padding: 16px 18px; background: var(--card); }
.fig-t { display: block; font-size: 15px; color: var(--ink);
         margin-bottom: 6px; line-height: 1.25; }
.fig-n { display: block; font-size: 30px; line-height: 1; color: var(--accent-ink); }
.fig-l { display: block; font-size: 12px; color: var(--muted); margin-top: 8px; }


/* The journals, as filters. */
.pub-venues { display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
              margin: 0 0 18px; }
.pv-kicker { font-size: 10px; color: var(--muted); text-transform: uppercase;
             letter-spacing: .8px; margin-right: 4px; }
.venue-chip { display: inline-flex; align-items: center; gap: 7px;
              font: inherit; font-size: 12px; cursor: pointer;
              padding: 5px 11px; border-radius: 99px;
              border: 1px solid var(--line-2); background: var(--card);
              color: var(--ink-2); }
.venue-chip:hover { border-color: var(--brand-3); color: var(--ink); }
.venue-chip.on { border-color: var(--brand-lit); background: var(--brand-2);
                 color: var(--brand-lit); }
.vc-n { color: var(--ink-2); }
.vc-c { font-size: 10px; padding: 0 5px; border-radius: 3px;
        color: var(--warn); background: var(--warn-soft); }
.venue-more { font: inherit; font-size: 12px; cursor: pointer;
              padding: 5px 11px; border-radius: 99px;
              border: 1px dashed var(--line-2); background: none;
              color: var(--muted); }
.venue-more:hover { border-color: var(--brand-3); color: var(--ink); }

/* The bar above the list: how many, filtered by what, in what order. */
.pub-listbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
               padding: 12px 0; border-top: 1px solid var(--line);
               border-bottom: 1px solid var(--line); margin-bottom: 4px; }
.lb-found { font-size: 14px; color: var(--ink-2); }
.lb-found b { color: var(--accent-ink); font-weight: 400; }
.lb-spacer { flex: 1; }
.lb-filters { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.lb-filters select { width: auto; }
.lb-sort { display: flex; align-items: center; gap: 6px;
           font-size: 12px; color: var(--muted); white-space: nowrap; }
.lb-sort select { width: auto; }
.pf-toggle { display: flex; align-items: center; gap: 5px;
             font-size: 12px; color: var(--muted); white-space: nowrap; }
.pf-clear { font-size: 12px; padding: 4px 10px; }

/* A paper: title, then the authors, then the record it appeared in. */
.pub { padding: 14px 0; border-bottom: 1px solid var(--line); }
/* The title carries the page; give it the weight and the full ink. */
.pub h3 { font-size: 16px; font-weight: 500; line-height: 1.45;
          margin: 0; color: var(--ink); }
.pub h3 a { color: inherit; text-decoration: none;
            border-bottom: 1px solid transparent; }
.pub h3 a:hover { border-bottom-color: var(--accent-ink); }
.pub-new { font-size: 10px; padding: 1px 6px; border-radius: 3px;
           background: var(--brand-2); color: var(--brand-lit);
           vertical-align: 2px; margin-left: 6px; }
/* Lab members stand out; everyone else recedes. The point of the byline on
   a lab page is "who here wrote this", so that is the only distinction it
   should make.

   This was briefly teal for EVERY author, with lab members differing only by
   font-weight — so the colour meant nothing (it was on all of them) and the
   distinction that does mean something was nearly invisible. If a reader has
   to ask what a colour signifies, it is decoration. */
.pub-authors { font-size: 13px; line-height: 1.6; margin: 6px 0 0;
               color: var(--muted); }
.pub-authors strong { font-weight: 500; color: var(--ink); }
/* ¹ 제1저자 · * 교신저자 — the marks carry the accent, since they are the
   only thing on the line that is a claim rather than a name. */
.pub-authors sup { font-size: 10px; color: var(--accent-ink); }
/* The record line is read, not skimmed past — a reader checking a citation
   needs the volume and the year. 13px rather than 12, and the values in the
   brighter secondary rather than the quiet one; only the field LABELS stay
   muted, because "Vol" carries less than the number after it. */
.pub-meta { font-size: 13px; color: var(--ink-2); margin: 6px 0 0;
            display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; }
.m-k { color: var(--muted); font-size: 11px; text-transform: uppercase;
       letter-spacing: .4px; }
.m-v { color: var(--ink-2); }
.m-v.venue { font-style: italic; }
.pub-class { font-size: 10px; padding: 1px 7px; border-radius: 3px;
             color: var(--warn); background: var(--warn-soft); }
.pub-wip { color: var(--muted); background: rgba(134, 141, 140, .14); }
.pub-review { opacity: .82; }
.pub-pdf { color: var(--accent-ink); text-decoration: none;
           border-bottom: 1px solid transparent; }
.pub-pdf:hover { border-bottom-color: var(--accent-ink); }
.pub-pdf-m { color: var(--muted); }

.pub-year { display: flex; align-items: center; gap: 10px; margin: 22px 0 2px; }
.pub-year h2 { font-size: 17px; font-weight: 400; margin: 0; }
.pub-year .py-count { color: var(--muted); font-size: 12px; }
.pub-year::after { content: ""; flex: 1; height: 1px; background: var(--line); }

.pub-legend { display: flex; gap: 18px; font-size: 12px;
              color: var(--muted); margin: 18px 0 40px; }
.pub-legend sup { color: var(--accent-ink); }

@media (max-width: 900px) {
  .pub-dash { grid-template-columns: 1fr; }
  .line-chart { height: 200px; }
}
@media (max-width: 640px) {
  .dash-figs { grid-template-columns: 1fr 1fr; }
  .fig-n { font-size: 24px; }
  .pub-search { flex-wrap: wrap; }
}

/* The team filter sits with the other dropdowns. Its options come from the
   roster, and a paper matches if any of its authors is on that team — so
   nothing about teams is stored per paper. */
#pf-team { max-width: 210px; }
/* ── Home-page theme ───────────────────────────────────────
   This used to redefine the palette for html.home only, leaving the rest of
   the site on a neutral black. Those values are now the site's own tokens in
   base.css, so there is nothing to override here — the homepage and every
   other page read the same palette. */
/* ── No bold anywhere in the site's chrome ─────────────────
   Every rule now asks for weight 400, but a few tags are bold by the
   browser's own default rather than by CSS: table headers, and the
   handful of <b>/<strong> marks in the page furniture. This is the
   last file in the cascade, so one rule here finishes the job.

   DELIBERATE EXCEPTIONS, added later and not oversights. All of them are
   real weights, backed by their own @font-face in base.css — none is a
   browser-synthesised smear:

     800  base.css  nav.main > a, .nav-drop-btn   the header nav
     800            .hero-center h1               the hero's lab name
     700            .ab-title                     ABOUT US
     700            .ab-eyebrow                   OUR VISION, RESEARCH SCOPE

   Everything else still holds to 400. Keep this list current — a stale
   version of it is worse than none, because the next person reads it as
   "nothing here is bold" and spends a while disbelieving the screen. */
th, b, strong { font-weight: 400; }

/* ── Header brand: two logos, university then lab ──────────
   Site-wide (the header lives in base.html), parked here rather than
   in base.css to keep that file core-maintainer-only. The header is a
   58px flex row, so the images just need heights; a hairline divider
   keeps the two marks from reading as one word. */
.brand { align-items: center; }
.brand img { height: 34px; width: auto; display: block; }


/* ── Hero: the new first screen ────────────────────────────
   One viewport tall. The ontology graph fills it as a quiet background,
   the prompt sits centred on top, agent characters stand on the graph.
   Everything the old home page said starts one scroll below. */
.hero {
  position: relative; overflow: hidden;
  /* Full-bleed: swallow the container's gutter and its top padding. */
  /* Out to the viewport edges, not just past the gutter. It used to
     cancel the padding, which was enough when the container ran the full
     width; now that the container is centred and capped there is also the
     margin either side to escape. */
  /* 100% of the container, not 100vw. A viewport unit includes the vertical
     scrollbar, so the hero came out a scrollbar wider than the visible page
     and the whole document could be nudged sideways.

     The container is full width now, so 100% is the same measurement
     without that error. The negative margin is what escapes the gutter. */
  width: calc(100% + var(--gutter) * 2);
  margin: -30px 0 0 calc(var(--gutter) * -1);
  /* Exactly one viewport, so the first screen ends on the scroll cue and
     nothing of About us shows below it. The arithmetic is exact rather than
     approximate: the header is 58px and sticky, so the hero starts at y=58;
     * { box-sizing: border-box } means this min-height already covers the
     padding and the 1px bottom border; and .hero's -30px top margin cancels
     .container's 30px top padding. Bottom edge lands on 100svh, with
     .hero-cue 18px above it. Being a viewport unit it holds at any zoom —
     "correct at 100%" is not a separate case.

     This was briefly 82svh, to let the ABOUT US title peek above the fold as
     an invitation to scroll. Reverted on request: the wanted first
     impression is the hero alone, ending on the cue. Both readings are
     defensible, so if you are about to change it again, change it knowing
     it has been both.

     min-height, not height: the answer card can need more room than one
     screen, and it should grow rather than clip. */
  min-height: calc(100svh - 58px);   /* header is 58px and sticky */
  border-bottom: 1px solid var(--line);
  /* Content sits at the vertical centre — the headline rides lower,
     roughly two-fifths down the screen. */
  display: flex; align-items: center; justify-content: center;
  padding: 44px 0 64px;
}

/* The graph is scenery that answers back: it fills the frame (crops,
   never squashes), drifts on its own physics, and reacts to the mouse —
   grab a node to drag it, hover one and its neighbourhood lights up
   while the rest falls back. */
.hero-graph { position: absolute; inset: 0; width: 100%; height: 100%; }
/* NOTE: the rule further down adds z-index and pointer-events. */
.hero-graph.focus { cursor: grab; }
/* ── The centre column: tagline, prompt, suggestion chips ──
   The hero's type ladder is now built from size and tracking alone. It used
   to be built from three faces — Space Grotesk for the lab name over Inter
   for everything under it — and a hero-scoped --font-ui existed to keep that
   swap from leaking out of the hero. One face (see base.css) carries the
   whole site now, so both the second family and the alias are gone. */
.hero-center { position: relative; z-index: 2; text-align: center;
  /* Wide enough for the answer card; the ask bar stays narrower inside. */
  width: min(880px, calc(100% - 48px)); }
.hero-center .hero-ask { max-width: 760px; margin-left: auto; margin-right: auto; }
/* The brand moment: ExtraBold, tight tracking, plain white. Colour belongs
   on the elements under the name, never in it. 800 needs the ExtraBold
   @font-face in base.css to exist — without it the browser silently fakes
   the weight by smearing 700. */
.hero-center h1 { margin: 0 0 10px; font-size: clamp(26px, 3.6vw, 44px);
  font-family: var(--font);
  /* Pure white, not --ink (#e6f1ef has a green cast): the lab name is the
     one place on the page that should read as fully neutral. */
  /* -.02em, not the -.04em this carried at weight 400: ExtraBold's stems are
     thicker, so the same negative tracking closes the counters up and the
     name starts to read as one dark block. */
  font-weight: 800; letter-spacing: -.02em; line-height: 1.18; color: #fff; }
/* The headline's second line carries the affiliation: heavier than body
   copy, well below the name in size, so the name still leads. */
.hero-center .h1-line2 { display: block; margin-top: 8px;
  font-family: var(--font);
  font-size: clamp(15px, 1.5vw, 21px); font-weight: 400;
  letter-spacing: -.025em; line-height: 1.4; color: var(--ink); }
/* One line on desktop; the media query below breaks it after
   "University," once the column gets narrow. */
.h1-break { display: none; }
.hero-sub { margin: 0 0 28px; font-family: var(--font); font-size: 15px;
  font-weight: 400; color: rgba(255, 255, 255, .62); }
/* Two marks, one of each kind — the agent gains weight, the ontology gains
   colour. Colouring both would flatten the emphasis. */
.hero-sub .hs-agent { font-weight: 400; color: var(--ink); }
.hero-sub .hs-onto { color: var(--brand-lit); }

.hero-ask { display: flex; align-items: center; gap: 8px;
  background: var(--card); border: 1px solid var(--line-2);
  border-radius: 14px; padding: 8px 8px 8px 20px;
  box-shadow: 0 18px 50px rgba(0, 0, 0, .45); }
.hero-ask:focus-within { border-color: var(--brand-lit); }
.hero-ask input { flex: 1; min-width: 0; background: none; border: none;
  outline: none; color: var(--ink); font-family: var(--font);
  font-size: 16px; font-weight: 400; }
/* Lifted off --muted, which read as greyed-out against the card. */
.hero-ask input::placeholder { color: rgba(255, 255, 255, .48); }
.hero-ask button { flex: 0 0 auto; background: var(--brand-3); color: var(--on-brand);
  border: none; border-radius: 9px; padding: 11px 24px;
  font-family: var(--font); font-size: 15px; font-weight: 400;
  letter-spacing: -.01em; cursor: pointer; }
.hero-ask button:hover { background: var(--brand-lit); color: var(--paper); }

.hero-chips { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap;
  margin-top: 14px; }
/* Clearly clickable, but a step under the Ask button: 600, not 700. */
.hero-chips button { background: none; border: 1px solid var(--line-2);
  border-radius: 999px; padding: 8px 18px; color: var(--ink-2);
  font-family: var(--font); font-size: 14px; font-weight: 400;
  line-height: 1.3; cursor: pointer; }
.hero-chips button:hover { border-color: var(--brand-lit); color: var(--ink); }
.hero-chips button.on { background: var(--brand-2); border-color: var(--brand-lit);
  color: var(--ink); }

/* Narrow screens: the affiliation splits at the comma rather than
   wrapping wherever it happens to run out of room. */
@media (max-width: 600px) {
  .h1-break { display: block; }
  /* 32px of side room instead of 48px. "A research group at Sungkyunkwan
     University," measures ~329px at 15px Inter Bold, and a 375px phone
     leaves only 327px — it missed by a hair and wrapped into three lines. */
  .hero-center { width: calc(100% - 32px); }
}

/* ── The agent's answer card ───────────────────────────────
   Renders under the chips when a question is picked: the prose on the
   left with the numbered path the agent walked, its sources on the
   right — the same exchange v3 keeps in a side panel, laid flat. */
/* The answer sits above everything: the graph is a backdrop, and a backdrop
   showing through the thing people came to read is the whole failure of the
   version this replaced. */
.hero-answer { z-index: 4; margin-top: 30px; text-align: left;
  /* 1.5× the original card: wider than the centre column it lives in,
     so it takes its own width and re-centres itself. */
  position: relative; left: 50%; transform: translateX(-50%);
  /* Narrower than it was. At 1320px it covered most of the field behind it,
     which is the thing the answer is supposed to be resolving out of —
     hiding the mass to show the answer defeats both. */
  width: min(1000px, calc(100vw - 120px));
  /* No min-height. It held 250px for the demo's three fixed answers; a real
     answer is two sentences or ten, and the box should be the size of what
     is in it. */
  align-items: start;
  display: grid; grid-template-columns: minmax(0, 1fr) 380px;
  /* Slightly translucent, with the field blurred behind it: the answer
     stays perfectly readable and the mass is still visibly there. */
  background: rgba(12, 18, 22, .84);
  backdrop-filter: blur(7px); -webkit-backdrop-filter: blur(7px); border: 1px solid var(--line-2);
  border-radius: 16px; }
.hero-answer:empty { display: none; }

.ha-head, .ha-label, .sk, .hop b {
  font-family: var(--font);
  text-transform: uppercase; }

.ha-main { padding: 28px 34px 30px; }
.ha-head { margin: 0 0 14px; font-size: 16px; letter-spacing: 2.4px;
  color: var(--muted); }
.ha-head::before { content: ""; display: inline-block; width: 9px; height: 9px;
  border-radius: 50%; background: var(--brand-lit); margin-right: 10px; }
.ha-text { margin: 0 0 20px; font-size: 14.5px; line-height: 1.65;
  color: var(--ink); text-align: justify; word-break: keep-all;
  hyphens: auto; }

/* The walked path: numbered hop pills joined by arrows. */
.ha-path { display: flex; flex-wrap: wrap; align-items: center; gap: 11px; }
.hop { background: var(--card-2); border: 1px solid var(--line-2);
  border-radius: 10px; padding: 8px 15px; font-size: 14px; color: var(--ink); }
.hop b { font-size: 11px; font-weight: 400; color: var(--brand-lit);
  margin-right: 9px; }
.arr { color: var(--muted); font-size: 14px; }

.ha-sources { border-left: 1px solid var(--line-2); }
.ha-label { margin: 0 0 6px; font-size: 16px; letter-spacing: 2.4px;
  color: var(--muted); }
.src { padding: 12px 0; border-bottom: 1px solid var(--line); }
.src:last-child { border-bottom: none; }
.sk { margin: 0 0 3px; font-size: 13px; letter-spacing: 1.8px;
  color: var(--brand-lit); }
.sv { margin: 0; font-size: 17px; color: var(--ink-2); }

@media (max-width: 720px) {
  .hero-answer { grid-template-columns: 1fr; }
  .ha-sources { border-left: none; border-top: 1px solid var(--line-2); }
}

/* ── Agents: robots that wander the stage ──────────────────
   DORMANT: the robots were taken off the stage for now (design call,
   2026-07) but the wardrobe stays — restoring them is markup + the
   wander script in git history, plus these rules. Nothing below is
   reachable while no .bot exists in the page. */
.bot { position: absolute; z-index: 2; left: 0; top: 0; width: 56px;
  display: flex; flex-direction: column; align-items: center;
  opacity: 0; transition: opacity .5s; will-change: transform; }
.bot .sprite { display: block; }
.bot .who { font-size: 11px; color: var(--muted); letter-spacing: .3px;
  margin-top: 1px; white-space: nowrap; }

/* Robot anatomy. Colours come from the site's own tokens: teal shell
   line, rust antenna tip — the accent pair base.css already owns. */
.bot .shell { fill: var(--card-2); stroke: var(--brand-lit); stroke-width: 1.3; }
.bot .leg { fill: var(--card-2); stroke: var(--line-2); stroke-width: 1; }
.bot .eyes { animation: bot-blink 4.6s infinite; }
.bot .tip { animation: tip-pulse 2.4s infinite; }
@keyframes bot-blink { 0%, 90%, 100% { opacity: 1; } 93%, 96% { opacity: .1; } }
@keyframes tip-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* Walking: the body bobs, the legs alternate. */
.bot.walking .body-g { animation: bot-bob .5s ease-in-out infinite alternate; }
.bot.walking .leg-l { animation: bot-step .32s linear infinite alternate; }
.bot.walking .leg-r { animation: bot-step .32s .16s linear infinite alternate; }
@keyframes bot-bob { from { transform: translateY(0); } to { transform: translateY(-2px); } }
@keyframes bot-step { from { transform: translateY(0); } to { transform: translateY(-3.5px); } }

/* The status bubble: a small task card, like the twin's tooltips.
   Title colour says which kind of agent is talking. */
.bot .bubble { position: absolute; bottom: calc(100% + 8px); left: 50%;
  transform: translate(-50%, 6px); width: max-content; max-width: 200px;
  background: rgba(19, 21, 21, .94); border: 1px solid var(--line-2);
  border-radius: 10px; padding: 8px 12px; font-size: 12px; line-height: 1.5;
  color: var(--ink-2); text-align: center; opacity: 0; pointer-events: none;
  transition: opacity .25s, transform .25s; }
.bot .bubble.on { opacity: 1; transform: translate(-50%, 0); }
.bot .bubble .bt { display: block; font-size: 12px; font-weight: 400;
  letter-spacing: .6px; margin-bottom: 1px; }
.bot.b1 .bt { color: var(--green); }
.bot.b2 .bt { color: var(--brand-lit); }
.bot.b3 .bt { color: var(--rust); }

@media (prefers-reduced-motion: reduce) {
  .bot .eyes, .bot .tip, .bot.walking .body-g,
  .bot.walking .leg-l, .bot.walking .leg-r { animation: none; }
}

/* ── Scroll cue ───────────────────────────────────────────── */
/* It was 11px in --muted at the bottom of a full-height hero, which on a busy
   background read as a caption rather than as something to press. Bigger,
   brighter, and the arrow moves - motion is what makes it register as an
   invitation. */
/* Put the highlight back. Bottom right of the answer card, out of the way of
   the text - it is a repair, not something to reach for. .hero-answer is
   already position: relative, so it is the anchor. */
.ha-relight {
  position: absolute; right: 10px; bottom: 8px; z-index: 3;
  width: 26px; height: 26px; padding: 0; line-height: 1;
  font-size: 13px; cursor: pointer;
  color: var(--muted); background: none;
  border: 1px solid var(--line-2); border-radius: 50%;
  transition: color .2s ease, border-color .2s ease;
}
.ha-relight:hover { color: var(--brand-lit); border-color: var(--brand-lit); }

.hero-cue { position: absolute; z-index: 2; bottom: 22px; left: 50%;
  transform: translateX(-50%); font-size: 13.5px; letter-spacing: 2px;
  text-transform: uppercase; text-decoration: none;
  /* The site's teal, the same one links and graph nodes use - so it reads as
     something to press rather than as another piece of grey furniture. */
  color: var(--brand-lit);
  padding: 8px 14px 6px;            /* a real target, not just the glyph */
  transition: color .2s ease, opacity .2s ease; }
.hero-cue:hover { color: #fff; text-decoration: none; }
.hero-cue::after { content: "↓"; display: block; text-align: center;
  font-size: 18px; line-height: 1; margin-top: 4px;
  animation: cue-bob 1.9s ease-in-out infinite; }
.hero-cue:hover::after { animation-play-state: paused; }

/* Small and slow on purpose: enough to catch the eye at the edge of vision,
   not enough to compete with the headline. */
@keyframes cue-bob {
  0%, 100% { transform: translateY(0);   opacity: .75; }
  50%      { transform: translateY(5px); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .hero-cue::after { animation: none; }
}

/* Once a question is asked the answer is the point; the cue steps back rather
   than bobbing away underneath it. */
.hero.asking .hero-cue { opacity: 0; pointer-events: none; }

/* ── About: vision, research scope, promise ────────────────
   The old three-paragraph lede, rebuilt: a highlighted vision line,
   four scope cards (sensor→city), and three numbered commitments. */
/* inline-block so the rule under the label stops at the end of the text
   rather than running the width of the section. */
/* Bold (700), the real weight — base.css declares a NanumSquare @font-face
   for it, so this is not a synthesised smear. Both OUR VISION and RESEARCH
   SCOPE wear this class, and it is used nowhere but those two. */
.ab-eyebrow { display: inline-block; margin: 0 0 18px; padding-bottom: 8px;
  border-bottom: 1px solid var(--line-2);
  font-size: 22px; font-weight: 700;
  letter-spacing: 3px; text-transform: uppercase; color: var(--brand-lit);
  font-family: var(--font); }
/* The section's display title — ABOUT US, big and white, with the teal
   eyebrow (OUR VISION) sitting under it. */
.ab-title { margin: 0 0 12px; font-size: 44px; font-weight: 700;
  letter-spacing: 3px; text-transform: uppercase; color: var(--ink);
  font-family: var(--font); }

/* One column by default — the copy, then the figure under it. The two-column
   form is the wide-screen case below. */
.about { padding: 54px 0 6px; display: grid; gap: 44px; }
.ab-vision { margin: 0; max-width: none; font-size: clamp(22px, 2.7vw, 34px);
  font-weight: 400; line-height: 1.5; letter-spacing: -.2px; color: var(--ink);
  /* The site's global h2 rule uppercases headings; uppercase glyphs are
     wider and were forcing wraps before the intended <br> breaks. */
  text-transform: none; }
.ab-vision em { font-style: normal; color: var(--brand-lit); }

/* The figure links to itself at full resolution. Its labels are baked-in
   pixels, and at column width they land around 10px — readable enough to
   see there are four levels, not to read the three lines under each. The
   click is the answer to that, so it needs to look like one. */
.ab-figure { display: block; border-radius: 12px;
  transition: transform .3s, opacity .3s; }
.ab-figure img { display: block; width: 100%; height: auto; }
.ab-figure:hover { transform: scale(1.015); }
.ab-figure:focus-visible { outline: 2px solid var(--brand-lit);
  outline-offset: 6px; }

@media (min-width: 1101px) {
  /* Copy left, figure right, each on half the row and vertically centred on
     the other — the figure is 16:9 and ends up the taller of the two. */
  .about { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: center; gap: 48px; }
  /* The vision's line breaks are hand-placed <br>s, and they were placed for
     the full-width container. Measured in the real face at 34px, the longest
     line is 923px in English and 1227px in Korean — Korean is what binds.
     Half of this row is ~900px, so the <br>s would survive and the browser
     would add its OWN wraps on top of them, breaking the shape into ragged
     five- and six-line blocks. Dropping them here hands the line breaking to
     the column width, which is the only thing that knows how wide it is.
     Below this breakpoint the section is one column at full width again and
     the <br>s come back, which is where they were right all along. */
  .ab-vision br { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .ab-figure { transition: none; }
  .ab-figure:hover { transform: none; }
}

/* Bottom padding closes the About block now that the promises are gone;
   the pattern's own top rule on the Work header takes over from there. */
.scope { padding: 46px 0 52px; }
/* Stacked, not a split row: the note reads as a subtitle under the
   RESEARCH SCOPE eyebrow rather than a caption off to the right. */
.scope-head { display: flex; flex-direction: column;
  align-items: flex-start; gap: 8px; margin: 0 0 16px; }
.scope-head .ab-eyebrow { margin: 0; }
.scope-note { margin: 0; font-size: 13px; color: var(--muted); }
.scope-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 16px; }
.scope-card { background: var(--card); border: 1px solid var(--line-2);
  border-radius: 12px; padding: 18px 20px 16px;
  display: flex; flex-direction: column; }
/* Only the tag lives here now. The "Sensor-level" / "System-level" labels
   that sat opposite it are gone — they restated the tag beside them in a
   second wording, and being hardcoded English they stayed English on /ko.
   space-between is harmless with one child and is what the row goes back to
   if anything is ever added on the right. */
.sc-head { display: flex; justify-content: space-between;
  align-items: baseline; margin: 0 0 12px; }
.sc-tag, .pr-num {
  font-family: var(--font); }
.sc-tag { font-size: 16.5px; font-weight: 400; letter-spacing: 2px;
  text-transform: uppercase; color: var(--brand-lit); }
.sc-claim { margin: 0 0 16px; font-size: 16px; font-weight: 400;
  line-height: 1.55; color: var(--ink); }
/* The keyword list sits at the card's floor, whatever the claim's length. */
.sc-list { list-style: none; margin: auto 0 0; padding: 14px 0 0;
  border-top: 1px solid var(--line); }
.sc-list li { padding: 3px 0; font-size: 12.5px; color: var(--ink-2); }
/* Each keyword links to the lab's paper on it. Deliberately NOT the site's
   link colour: --blue on twelve rows of small type would turn the four cards
   into a wall of blue and pull attention off the claim above them. They read
   as the list they already were, and answer to the cursor.
   The teal arrow is what says "this leaves the page" — without it a colour
   change on hover alone is easy to miss on a row of 12.5px text. */
.sc-list a { color: inherit; text-decoration: none;
  display: inline-flex; align-items: baseline; gap: 5px; }
.sc-list a::after { content: "↗"; font-size: 10px; color: var(--muted);
  transition: color .18s; }
.sc-list a:hover { color: var(--brand-lit); text-decoration: underline;
  text-underline-offset: 2px; }
.sc-list a:hover::after { color: var(--brand-lit); }

@media (max-width: 1100px) {
  .scope-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
  .scope-grid { grid-template-columns: 1fr; }
}

/* ── Research Scope: one-shot fan-out ──────────────────────
   Sensor stays put in column 1. System / Building / City start
   stacked behind Sensor (translated to overlap, opacity 0) and
   fan out to their natural columns, staggered so the reveal
   cascades left-to-right rather than moving in unison.

   This was a scroll-driven view() timeline, where scroll position
   WAS the playhead: the cards scrubbed back and forth with the
   wheel and froze whenever you stopped, which read as dragging
   them by hand rather than watching them open. Now the section
   entering the viewport is only a TRIGGER — the animation runs on
   its own clock afterwards, so one scroll plays the whole fan.

   The trigger is an IntersectionObserver in home.html, which adds
   .fan-armed (the collapsed start state) on load and .fan-in
   (play) once the grid is in view. Both classes come from JS, so
   with JS off nothing is ever hidden — the plain four-column grid
   renders as-is. No @supports gate needed any more: this is
   ordinary time-based CSS animation, so Firefox gets it too.

   Only applied at the 4-col breakpoint; narrower layouts stack
   into rows and would look wrong slid horizontally.
   prefers-reduced-motion opts out (checked in the JS as well). */
@media (min-width: 1101px) and (prefers-reduced-motion: no-preference) {
  /* Start offset = stacked on column 1: one card-width per column
     gone, plus the 16px grid gap between each. Declared outside the
     .fan-armed rule so the @keyframes can read it too. */
  .scope-card:nth-child(2) { --from-x: calc(-100% - 16px); }
  .scope-card:nth-child(3) { --from-x: calc(-200% - 32px); }
  .scope-card:nth-child(4) { --from-x: calc(-300% - 48px); }

  /* Collapsed and invisible until the trigger fires. */
  .scope-grid.fan-armed .scope-card:nth-child(n+2) {
    opacity: 0; transform: translateX(var(--from-x));
  }
  /* `both` fill matters: it holds the collapsed frame through the
     stagger delay, then holds the open frame forever after — without
     it card 4 would flash into place before its delay elapsed, and
     every card would snap back to .fan-armed when the run ended. */
  .scope-grid.fan-in .scope-card:nth-child(n+2) {
    animation: scope-fan-x 820ms cubic-bezier(.16, .84, .34, 1) both;
  }
  .scope-grid.fan-in .scope-card:nth-child(3) { animation-delay: 130ms; }
  .scope-grid.fan-in .scope-card:nth-child(4) { animation-delay: 260ms; }

  @keyframes scope-fan-x {
    from { transform: translateX(var(--from-x)); opacity: 0; }
    to   { transform: translateX(0); opacity: 1; }
  }
}

/* Small screens: the stage narrows to prompt-first. Agents and their
   chatter would crowd a phone, so they wait for the wide screen. */
@media (max-width: 720px) {
  .hero { min-height: 480px; height: calc(100svh - 58px); }
  .bot { display: none; }
  }

/* While a path is lit, everything else steps back. */
.hero-graph.v3 .agent-head { display: flex; align-items: center; gap: 10px;
  justify-content: space-between; padding: 15px 20px;
  border-bottom: 1px solid var(--line); }
.v3 .agent-title { font-size: 14px; font-weight: 400; color: var(--ink);
  display: flex; align-items: center; gap: 8px; }
.v3 .agent-title::before { content: ""; width: 8px; height: 8px;
  border-radius: 50%; background: var(--brand-lit); }
.v3 .agent-tag { font-size: 9.5px; letter-spacing: 1.4px; color: var(--muted); }

/* The exchange: question on the right, the grounded answer below. */
.v3 .agent-log { flex: 1; min-height: 0; overflow-y: auto; padding: 18px 20px;
  display: flex; flex-direction: column; gap: 12px; }
.v3 .q-bubble { align-self: flex-end; max-width: 92%;
  background: var(--card-2); border: 1px solid var(--line-2);
  border-radius: 11px; padding: 10px 14px; font-size: 13.5px; color: var(--ink); }
.v3 .a-card { background: var(--card-2); border: 1px solid var(--line-2);
  border-radius: 12px; padding: 14px 16px; font-size: 13.5px; line-height: 1.6;
  color: var(--ink-2); }
.v3 .from { margin-top: 12px; border-top: 1px solid var(--line-2);
  padding-top: 10px; }
.v3 .from-label { margin: 0 0 6px; font-size: 10px; letter-spacing: 2px;
  color: var(--muted); }
.v3 .from-row { display: flex; gap: 12px; padding: 3px 0; font-size: 12.5px; }
.v3 .fk { flex: 0 0 88px; padding-top: 1px; font-size: 10px;
  letter-spacing: 1.2px; color: var(--muted); }
.v3 .fv { color: var(--brand-lit); }

/* Try-asking: a stack of full-width questions; the active one is lit. */
.v3 .try { padding: 4px 20px 10px; }
.v3 .try-label { margin: 0 0 8px; font-size: 10px; letter-spacing: 2px;
  color: var(--muted); }
.v3 .try button { display: block; width: 100%; text-align: left;
  background: none; border: 1px solid var(--line-2); border-radius: 12px;
  padding: 11px 14px; margin-bottom: 8px; font-size: 13px; font-family: inherit;
  color: var(--ink-2); cursor: pointer; }
.v3 .try button:hover { border-color: var(--brand-lit); color: var(--ink); }
.v3 .try button.on { background: var(--brand-2); border-color: var(--brand-lit);
  color: var(--ink); }

.v3 .agent-ask { margin: 0 20px 18px; box-shadow: none;
  padding: 5px 5px 5px 14px; }
.v3 .agent-ask button { padding: 9px 18px; }

/* Narrow screens: stack — the stage keeps its copy, the agent follows. */
@media (max-width: 940px) {
  .v3 .hero3 { grid-template-columns: 1fr; height: auto; }
  .v3 .stage { min-height: 440px; }
  .v3 .stage-copy { padding: 32px 24px 0; }
  .v3 .stage-stats { left: 24px; }
  .v3 .agent { border-left: none; border-top: 1px solid var(--line); }
  .v3 .agent-log { max-height: 300px; }
}

/* Nodes that appeared because an answer cited them. Brighter than the
   resting graph, so what the answer rests on reads at a glance. */
/* Sources, grouped by kind: the heading said once, then its links. Listing
   the kind against every row repeated "publication" five times and made the
   labels harder to read than the answer above them. */
.ha-sources { display: flex; flex-wrap: wrap; gap: 22px 40px; }
.src-group { min-width: 0; }
.src-group .sk { margin: 0 0 6px; font-size: 10px; letter-spacing: 1px;
                 text-transform: uppercase; color: var(--muted); }
.src-group .sv { display: block; font-size: 13px; line-height: 1.5;
                 color: var(--ink-2); text-decoration: none; max-width: 42ch; }
.src-group .sv + .sv { margin-top: 4px; }
.src-group .sv:hover { color: var(--brand-lit); }

/* The answer itself. The box is up to 1320px wide, so prose set across the
   whole of it runs to 200 characters a line and reads as text dumped into a
   container. The measure is capped instead; the box stays wide because the
   sources sit beside it. */
.ha-card { padding: 26px 30px; min-width: 0; }
.ha-tag { margin: 0 0 14px; font-size: 11px; letter-spacing: 2.2px;
          text-transform: uppercase; color: var(--brand-lit);
          font-family: var(--font); }
.ha-card .ha-text { max-width: 62ch; }
.ha-card .ha-text:last-child { margin-bottom: 0; }
.ha-sources { padding: 26px 26px; }

/* Sources sit in one column beside the answer, not spread across a row —
   the panel is 380px wide, so wrapping them made two cramped columns. */
.ha-sources { display: block; align-content: start; }
.ha-sources .src-group + .src-group { margin-top: 20px; }

/* A structural node goes somewhere; the cursor should say so. */
/* Research areas: drawn when an answer names them, gone at the next
   question. Brighter than the structure, because they are the part of the
   picture that changes and the part the answer is actually about. */
/* ── The research-area cloud ─────────────────────────────────────
   A region rather than a node: empty until an answer names some areas, then
   a small word cloud, then empty again. The words are not links — there is
   no page about a research area, which is exactly why they are drawn this
   way instead of as nodes. */
.pathing /* ── Nodes with some depth ───────────────────────────────────────
   The graph was fifteen identical grey discs. Size now says what a thing is
   — a section of the site, one team, one person — and a radial fill with the
   highlight off-centre reads as a sphere rather than a dot. */
/* The professor. Gold rather than the lab's teal, because he is the one
   node that is a person and not a part of the site. */
/* Lit by an answer. */
/* A hub's label carries more weight than a child's, so the eye finds the
   six parts of the site before the twelve things inside them. */


/* ── The halo ────────────────────────────────────────────────────
   One point per research area, turning slowly around the lab. Four hundred
   terms are a quantity, not a list — drawn as a field they say how much
   there is at a glance, and naming a chosen dozen would say something
   false. Only the few an answer picks out get a name. */
.og-mote { fill: var(--brand-lit); opacity: .34;
           animation: og-breathe 9s ease-in-out infinite;
           transition: opacity .5s, r .5s; }
@keyframes og-breathe { 0%,100% { opacity: .2; } 50% { opacity: .5; } }
.answering .og-mote { opacity: .24; animation: none; }
.og-mote.near { opacity: .6; animation: none; }
.og-mote.named { opacity: 1 !important; animation: none; r: 3.4;
                 filter: drop-shadow(0 0 7px rgba(63, 224, 176, .8)); }
.og-area { fill: var(--brand-lit); font-size: 12.5px; letter-spacing: .2px;
           transition: opacity .55s ease; }

/* ── Instances ───────────────────────────────────────────────────
   What an answer cited, as a node of its own hanging off its class and
   linking to its page. Lighting "Teams" because the answer was about Urban
   Building Informatics says nothing; a node that IS that team can be
   clicked through to it. This is the part that makes it an ontology rather
   than a picture of one. */
.og-inst { transition: opacity .45s ease; }
.og-idot { fill: var(--brand-lit); opacity: .95; }
.og-iname { fill: var(--ink); font-size: 11.5px; }
.og-inst:hover .og-idot { fill: #fff; }
.og-inst:hover .og-iname { fill: var(--brand-lit); }
.og-stem { stroke: var(--brand-lit); opacity: .45; stroke-dasharray: 2 3; }



/* ── Asking ──────────────────────────────────────────────────────
   At rest the headline and the prompt own the middle. Once a question is
   asked the headline has done its job: it goes entirely — not dimmed, gone,
   and its height with it, because fading alone leaves a hole — and the
   conversation slides into a column on the left while the ontology gathers
   in what is left. */
.hero h1, .hero-sub {
  transition: opacity .4s ease, transform .4s ease,
              margin .45s ease, max-height .45s ease;
  overflow: hidden; max-height: 320px;
}
.hero.asking h1, .hero.asking .hero-sub {
  opacity: 0; transform: translateY(-10px);
  max-height: 0; margin-top: 0; margin-bottom: 0;
  padding-top: 0; padding-bottom: 0; border: 0; visibility: hidden;
}

.hero { transition: justify-content .45s ease, padding-left .45s ease; }
.hero-center { transition: width .45s ease; }

/* Below 1180 there is no room for two columns, so the card stays centred and
   sits over the graph instead. The same 1180 and the same column width are
   in hero-graph.js, which needs to know where the conversation ends — change
   one and change the other. */
@media (min-width: 1180px) {
  /* These two decide where the answer column ends, and hero-graph.js works
     out where to put the graph from them (ASK_PAD and ASK_COL near the
     bottom of that file). It computes rather than measures, because this
     transitions over 450ms and anything measured mid-flight is a frame of
     the animation. Change these and change those. */
  .hero.asking { justify-content: flex-start; padding-left: 4vw; }
  .hero.asking .hero-center { width: min(560px, 38vw); text-align: left; }
  .hero.asking .hero-ask { max-width: none; margin-top: 0; }
  .hero.asking .hero-chips { justify-content: flex-start; }
  /* The card stops breaking out of the centre column and fills it instead. */
  .hero.asking .hero-answer {
    left: 0; transform: none; width: 100%; margin-top: 22px;
    grid-template-columns: 1fr;      /* sources below the answer, not beside */
  }
  .hero.asking .ha-sources { border-left: 0; border-top: 1px solid var(--line-2); }
  .hero.asking .ha-card .ha-text { max-width: none; }
}

/* ── The ontology ────────────────────────────────────────────────
   The lab's whole ontology as a slowly turning globe: every paper, every
   person, every topic worked on more than once, and the relations between
   them. Around 230 nodes. The spheres are WebGL; these are the labels, HTML
   on top and moved each frame from the projected coordinates.

   Labels are rationed. Two hundred at once is a wall of text, so only the
   lab, the teams, the professor and the heaviest topics and people carry
   one at rest — everything else names itself on hover. The nodes are the
   picture; the labels are the key. */
.hero-graph { position: absolute; inset: 0; z-index: 0; }
.hero-graph canvas { display: block; }

/* The canvas takes pointer events - hover names a node, drag turns the scene,
   click lights a neighbourhood - so it cannot be pointer-events: none the way
   the old graph was. Instead the centre column stops swallowing them: it is a
   transparent block 880px wide sitting on top, and without this it would eat
   every gesture aimed at the graph behind it. Its own controls opt back in. */
.hero-center { pointer-events: none; }
.hero-center .hero-ask,
.hero-center .hero-chips,
.hero-center .hero-answer,
.hero-center a,
.hero-center button,
.hero-center input { pointer-events: auto; }

/* Labels are DOM over the canvas rather than drawn into the scene: text in a
   WebGL scene either scales with distance until it is unreadable, or is
   redrawn every frame. These are positioned each tick and never hit-tested. */
.hero-graph__labels { position: absolute; inset: 0; pointer-events: none; }
.hero-graph__label {
  position: absolute; top: 0; left: 0; pointer-events: none;
  white-space: nowrap; line-height: 1;
  font-size: 12px; font-weight: 400;
  color: #b9c6d8;
  /* The scene is dark but not uniformly so; a label crossing a bright cluster
     needs its own shadow to stay legible. */
  text-shadow: 0 1px 8px rgba(0,0,0,.95), 0 0 3px rgba(0,0,0,.9);
  transition: opacity 180ms ease, color 180ms ease;
  visibility: hidden;   /* shown once positioned, so none flash at 0,0 */
}
.hero-graph__label-name { display: block; }
.hero-graph__label-detail {
  display: none; margin-top: 4px; max-width: 30ch; white-space: normal;
  font-size: 11px; line-height: 1.4; color: #8f9fb3;
}
.hero-graph__label.is-open { text-align: center; }
.hero-graph__label.is-open .hero-graph__label-detail { display: block; }

/* One colour per kind, so the picture is readable without a legend. */
.hero-graph__label--root { color: #fff; font-size: 20px; font-weight: 800;
  letter-spacing: .12em; }
.hero-graph__label--hub { color: #fff; font-size: 15px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; }
.hero-graph__label--group { color: #ffc0a8; font-size: 13px; font-weight: 700; }
.hero-graph__label--venue { color: #a8e0ff; font-size: 11px; }
.hero-graph__label--project { color: #b6e559; font-size: 13px; font-weight: 600; }
.hero-graph__label--contact { color: #9fb4c8; font-size: 12px; }
.hero-graph__label--link { color: #7de3e0; font-size: 12px; font-weight: 600; }
.hero-graph__label--team { color: #62e0b8; font-size: 13px; font-weight: 600; }
.hero-graph__label--member { color: #ffc978; }
.hero-graph__label--member.is-faculty { color: #ff9d2e; font-weight: 700; }
.hero-graph__label--topic { color: #c9a6e8; }

/* The camera-distance readout is a tuning aid, not part of the page. It stays
   in the markup so the numbers can be re-read when the layout is next
   adjusted; showing it to visitors would be noise. */
.hero-graph__zoom { display: none; }