/* =========================================================================
   Zhanlin (Kevin) Liu — personal site
   "Research console" aesthetic: deep canvas, cyan→violet accent,
   monospace metadata, knowledge-graph signature.
   ========================================================================= */

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

:root {
  --font-display: "Bricolage Grotesque", "Segoe UI", system-ui, sans-serif;
  --font-body: "Hanken Grotesk", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: "Spline Sans Mono", ui-monospace, "SF Mono", Menlo, monospace;

  /* Dark (default) — a dimmed charcoal-blue, not black. #090b11 and then #12151d
     both still read as pure black on most screens; extreme contrast against
     near-white text is what makes a dark theme tiring to read. This is the
     comfortable-dark range (comparable to GitHub's dark-dimmed).

     Two invariants hold, both measured rather than assumed:
       - --bg-2 stays DARKER than --panel, so recessed surfaces still recede
         (the answer panel inside a project card depends on it).
       - text > muted > faint in contrast, each ≥4.5:1 against the surface it
         actually lands on. Lifting the page compresses that, so the secondary
         levels are raised in step: --faint has now been retuned three times
         (#5d6675 failed AA outright, #7a8494 and #828c9c each fell below 4.5
         as the page got lighter). Measured here: 12.3 / 6.3 / 5.0. */
  /* The page ground and the field's ground are the same colour now. They used to
     differ -- #1e232b for the page, #0a0812 for the constellation's box -- and once the
     About panel carried the darker one, the difference read as a framed card floating on
     a lighter page: 42px of page colour down each side and a 14px radius drawing the
     corner. The universe is the page, so there is one ground. */
  --bg: #0a0812;
  --bg-2: #191e25;
  --panel: #252b35;
  --elevated: #2d3540;
  --text: #eaeff6;
  --muted: #a3aebd;
  --faint: #8f9aaa;
  --border: #39424f;
  --border-strong: #495463;

  --accent-1: #6ee7ff;
  --accent-2: #a78bfa;
  --accent-grad: linear-gradient(120deg, var(--accent-1), var(--accent-2));
  --accent-soft: rgba(110, 231, 255, 0.12);

  /* Topic accents. Previously inlined in the retired card grid's per-group rules
     and again in a second block under a different variable name, so nothing else
     could reuse them. The work areas, the scope row and the canvas backdrop all
     key off these now. */
  --topic-retrieval: var(--accent-1);
  --topic-agentic: var(--accent-2);
  --topic-evaluation: var(--ok);
  --topic-other: var(--warn);

  --ok: #4ade80;
  --warn: #fbbf24;

  /* ---- The About constellation ----
     Its own palette, in two modes. An earlier version of this idea kept one night
     sky in both themes on the grounds that a starfield needs a dark ground; the
     user asked for two, and they were right -- the site stamps data-theme
     explicitly and defaults to light, so a permanent dark box is the version most
     readers would have seen, pasted onto a pale page.

     The light mode is a real redefinition, not the dark one at lower opacity. That
     distinction is the whole difficulty: every mark on this canvas is drawn as the
     token colour at a low alpha over the ground, so on a dark ground "faint" means
     a pale colour fading toward black, and on a light ground it has to mean a dark
     colour fading toward white. Reusing the dark tokens at any opacity would make
     the stars, the orbit rings and the link curves lighter than the page and they
     would disappear. So --map-faint, --map-line and the four dot hues all flip
     from pale to deep between the two blocks.

     Declared on :root, and read off document.documentElement by qmap.js, which is
     what keeps that module from needing to know which element it draws inside.

     Contrast measured against both the ground and the lifted nebula centre, since
     text sits over both. Dark: ink 14.16, dim ink 10.03, keyword 7.80, dots 6.95
     to 11.13. Light: ink 12.79, dim ink 8.23, keyword 5.96, dots 3.57 to 5.00.
     Text is held to 4.5:1 and the dots, being non-text marks, to 3:1. */
  --map-ground: #0a0812;
  --map-nebula: #241a44;
  --map-ink: #f2effa;
  --map-ink-dim: #cfc9e2;
  --map-key: #c4a7f5;
  --map-faint: #8d84ad;
  --map-line: #6f6690;
  --map-topic-retrieval: #6ee7ff;
  --map-topic-agentic: #b49bff;
  --map-topic-evaluation: #62e58f;
  --map-topic-other: #fcc93f;

  /* ---- The company-logo chip ----
     One near-white ground for every real logo, identical in both themes, and
     deliberately NOT re-declared in the light block below.

     Measured across the eight marks the user supplied: five arrived with a baked-in
     opaque light background (astrumu, docugami, microsoft, prorata, wyze_labs --
     background luminance 192 to 255), one is dark ink on transparency (meta, ink
     luminance 82), and two are transparent with mid-tone ink (alibaba, wmg). No
     single treatment of the bare image works for that set: placed directly on the
     dark panel the five read as hard white rectangles, and meta's dark ink
     disappears entirely.

     A light chip fixes both at once -- the five baked backgrounds blend into it
     instead of ending in a ragged edge, and meta's dark ink sits on light ground in
     both themes. It is also the only way eight marks from eight sources sit together
     as one row of objects.

     What was rejected: flood-filling the baked backgrounds away (unreliable, and it
     eats any part of a mark that shares its background's colour), and inverting or
     brightness-filtering in dark mode (that turns a brand mark into something the
     company does not use, and wrecks the multicolour ones -- Microsoft's four
     squares and Alibaba's gradient).

     The edge is translucent ink rather than a fixed grey, which is what lets one
     value work on both grounds: on the dark panel it barely shows against the light
     chip, and on the light theme's white panel it is the hairline that keeps a
     near-white chip from dissolving into the page. */
  --logo-chip: #f4f6f8;
  --logo-chip-edge: rgba(11, 16, 32, 0.3);

  --neutral: #94a3b8;

  --radius: 14px;
  --radius-sm: 9px;
  --shadow: 0 18px 50px -18px rgba(0, 0, 0, 0.75);
  --maxw: 1180px;
  --head-h: 66px;
}

:root[data-theme="light"] {
  --bg: #f6f8fc;
  --bg-2: #eef1f6;
  --panel: #ffffff;
  --elevated: #f2f5f9;
  --text: #131720;
  --muted: #5a6675;
  /* Was #525c69, which was DARKER than --muted (#5a6675) — so "faint" text
     rendered more prominent than "muted" text, inverting the hierarchy in light
     mode only. #666f7d measures 4.73:1 against --muted's 5.45:1: still AA, and
     now correctly the quieter of the two in both themes. */
  --faint: #666f7d;
  --border: #e6e9ef;
  --border-strong: #d7dce4;
  --accent-1: #0e7490;
  --accent-2: #7c53f0;
  --accent-soft: rgba(14, 116, 144, 0.12);

  /* Status colors used for badge text (Recognition tab) need to be darkened
     for AA contrast on white — the dark-theme values are tuned to sit on a
     near-black panel and are far too light to read on white. */
  --ok: #15803d;
  --warn: #b45309;
  --neutral: #475569;

  /* Same indirection as dark: the operands are what change per theme. */
  --topic-retrieval: var(--accent-1);
  --topic-agentic: var(--accent-2);
  --topic-evaluation: var(--ok);
  --topic-other: var(--warn);

  /* Soft, diffused, low-opacity — a hairline contact shadow plus a broad,
     gentle falloff, so cards/dialog/sticky-detail feel lifted rather than
     smudged. */
  --shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 20px 40px -16px rgba(16, 24, 40, 0.12);

  /* ---- The About constellation, in daylight ----
     Not the night sky on a pale page, and not the pale page either: a lavender
     daylight sky, with the same structure drawn in ink instead of light. Every
     value here is a genuine redefinition of its dark counterpart rather than an
     opacity of it -- the ground lightens, and in exchange the ink, the keywords,
     the stars, the orbit rings, the link curves and all four dot hues darken,
     because "faint" on a light ground means a dark colour at low alpha. Left as
     opacities of the dark set, every one of those marks would have been lighter
     than the page and invisible.

     The nebula keeps its violet, deepened, so the centre still reads as the lit
     part of the field rather than a grey smudge.

     The four dot hues are the light theme's own topic accents pushed a little
     further down, since they land on a lavender ground rather than on white:
     measured 3.57 to 5.00 against ground and nebula, above the 3:1 a non-text
     mark needs. */
  /* f8, not f4, in the green channel. The panel carries this colour now rather than
     just the constellation's box, so the hero's mono greeting sits on it -- and green
     carries ~72% of the luminance weight, so f4 dropped that pairing from 4.50:1 to
     4.43:1 and under AA. f8 restores it to 4.54:1 with headroom, and is visually
     indistinguishable from f4 at this lightness. */
  --map-ground: #f6f8fc;
  --map-nebula: #ddd4f2;
  --map-ink: #16122b;
  --map-ink-dim: #3a3358;
  --map-key: #5b2fb0;
  --map-faint: #7d719e;
  --map-line: #9c93b8;
  --map-topic-retrieval: #0b7285;
  --map-topic-agentic: #6d28d9;
  --map-topic-evaluation: #157f3d;
  --map-topic-other: #a05a00;
}

/* Knowledge-graph canvas: on a near-white page the same node/edge alpha used
   for the dark canvas reads as a hazy smudge, so dim the whole layer. */
:root[data-theme="light"] .graph-bg { opacity: 0.4; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--text);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  min-height: 100vh;
}

a { color: inherit; text-decoration: none; }
h1, h2, h3, h4 { font-family: var(--font-display); font-weight: 600; line-height: 1.15; margin: 0; }
img { max-width: 100%; }
::selection { background: var(--accent-soft); }

/* ---------- Knowledge-graph background ---------- */
.graph-bg {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.55;
  -webkit-mask-image: radial-gradient(120% 90% at 70% 0%, #000 30%, transparent 78%);
          mask-image: radial-gradient(120% 90% at 70% 0%, #000 30%, transparent 78%);
}
.graph-bg canvas { width: 100%; height: 100%; display: block; }

/* ---------- Header ---------- */
.site-head {
  position: sticky;
  top: 0;
  z-index: 30;
  height: var(--head-h);
  display: flex;
  align-items: center;
  gap: 1.25rem;
  padding: 0 clamp(1rem, 2.5vw, 2.75rem);
  background: color-mix(in srgb, var(--bg) 80%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--border);
}

.wordmark {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.01em;
}
.wordmark-mark {
  font-size: 1.15rem;
  background: var(--accent-grad);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.wordmark-slash { color: var(--accent-2); font-family: var(--font-mono); font-weight: 500; }
.wordmark-name { font-size: 0.98rem; color: var(--text); }

.tabs {
  display: flex;
  gap: 0.25rem;
  margin: 0 auto;
  padding: 0.25rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 999px;
}
.tab {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--muted);
  font-family: var(--font-body);
  font-size: 0.9rem;
  font-weight: 500;
  padding: 0.5rem 1.05rem;
  border-radius: 999px;
  cursor: pointer;
  transition: color 0.18s, background 0.18s;
}
.tab:hover { color: var(--text); }
/* The visible label shortens on a phone so six tabs still fit one row; each
   button carries aria-label with the full name, so the accessible name is
   unaffected (and WCAG 2.5.3 holds: "Exp" is contained in "Experience"). */
.tab-abbr { display: none; }
.tab[aria-selected="true"] {
  color: #06121a;
  background: var(--accent-grad);
  font-weight: 600;
}
:root[data-theme="light"] .tab[aria-selected="true"] { color: #fff; }

.head-actions { display: flex; align-items: center; gap: 0.6rem; }

.icon-btn {
  appearance: none;
  width: 38px; height: 38px;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
  font-size: 1.05rem;
  cursor: pointer;
  transition: border-color 0.18s, transform 0.18s;
}
.icon-btn:hover { border-color: var(--accent-1); transform: translateY(-1px); }

.btn-ghost {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-family: var(--font-mono);
  font-size: 0.8rem;
  letter-spacing: 0.02em;
  padding: 0.5rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--muted);
  transition: border-color 0.18s, color 0.18s;
  white-space: nowrap;
}
.btn-ghost:hover { border-color: var(--accent-1); color: var(--text); }

/* ---------- Icons ---------- */
.ic { width: 18px; height: 18px; flex-shrink: 0; display: inline-block; vertical-align: middle; }
.btn-ghost .ic { width: 15px; height: 15px; }
.chat-launcher .ic { width: 15px; height: 15px; }
.ic-brand { width: 16px; height: 16px; }
/* Theme toggle: show the icon of the current theme. */
.ic-sun { display: none; }
:root[data-theme="light"] .ic-moon { display: none; }
:root[data-theme="light"] .ic-sun { display: inline-block; }

.btn-primary {
  appearance: none;
  border: 0;
  cursor: pointer;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 0.95rem;
  color: #06121a;
  background: var(--accent-grad);
  padding: 0.75rem 1.35rem;
  border-radius: 999px;
  transition: transform 0.18s, box-shadow 0.18s;
  box-shadow: 0 10px 30px -12px var(--accent-1);
}
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 16px 38px -12px var(--accent-1); }
:root[data-theme="light"] .btn-primary { color: #fff; }

/* ---------- Stage / panels ---------- */
.stage {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 100%;
  margin: 0;
  min-height: calc(100dvh - var(--head-h));
  display: flex;
  flex-direction: column;
  padding: clamp(1.5rem, 3vw, 3rem) clamp(1rem, 2.5vw, 2.75rem) clamp(2rem, 4vw, 3.5rem);
}

.panel { display: none; }
/* Active panel is a flex item; margin-block:auto centers it when the content is
   shorter than the viewport, and yields to normal top-aligned scrolling when
   it's taller — so every tab fills the screen height without a dead band. */
.panel.is-active { display: block; width: 100%; margin-block: auto; animation: panel-in 0.45s cubic-bezier(0.22, 1, 0.36, 1); }
@keyframes panel-in {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: translateY(0); }
}

.eyebrow {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  letter-spacing: 0.06em;
  color: var(--accent-1);
  margin: 0 0 1rem;
  text-transform: uppercase;
}

.section-h {
  font-family: var(--font-mono);
  font-size: 0.82rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--faint);
  margin: 2.4rem 0 1rem;
}

.panel-title { font-size: clamp(1.9rem, 4vw, 2.6rem); letter-spacing: -0.02em; }
.panel-sub { color: var(--muted); margin: 0.5rem 0 2.2rem; max-width: 62ch; }

/* ---------- Personal hero (single column, no metrics card) ---------- */
.about-hero { max-width: 92ch; margin-bottom: 3.5rem; }
.hero-title {
  font-size: clamp(2.6rem, 7vw, 4.6rem);
  letter-spacing: -0.03em;
  font-weight: 700;
  margin-bottom: 1.25rem;
}
.hero-lead { font-size: clamp(1.05rem, 2.1vw, 1.35rem); color: var(--muted); margin: 0 0 1.75rem; max-width: 92ch; }

/* ---------- What I work on ---------- */
.about-work { margin-bottom: 3rem; }
/* Shared with .domain-grid on Projects. The two card sets are one component
   family: same grid, same card shell, same entrance. What differs is what the
   card holds -- About shows one example question and a link onward, Projects
   lists every question in the area -- so the two keep their own classes and
   their own behaviour, and share only the look. Every rule below that both use
   names both selectors; nothing on Projects ever carried the .work-card class, so no
   About behaviour (the .wq-* list, the [data-project] delegation, the analytics
   selectors keyed on .about-work) can reach it. */
.domain-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 1.1rem;
}
/* Four cards across need 1,093px of content, i.e. a ~1,151px viewport. Below that
   auto-fit gives three and the fourth sits alone -- measured 3+1 from roughly 859
   to 1151px, which includes iPad landscape at 1024. Two-up instead. Lowering the
   min track to 240px only narrows the band, it does not close it. */
@media (max-width: 1150px) {
  .domain-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .domain-grid { grid-template-columns: minmax(0, 1fr); }
}
.domain-card {
  position: relative;
  overflow: hidden;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.6rem 1.5rem;
  transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s;
}
.domain-card::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 2px;
  background: var(--accent-grad);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.3s ease;
}
/* The hover treatment -- lift, topic-hued border and the accent sweep -- lives
   in the (hover: hover) block near the end of this file, since iOS leaves the
   last tapped element in :hover and would strand a tapped card lifted. */
/* The four About cards are the legend for the constellation below them, so each one
   has to resolve to the SAME custom property its area's nodes do -- not to a
   different property that happens to hold a similar colour today.

   That is why these point at --map-topic-* rather than at the site's --topic-*.
   The constellation has its own palette per theme, and the two families genuinely
   differ: in daylight --topic-agentic is #7c53f0 while --map-topic-agentic is
   #6d28d9, tuned for a lavender ground rather than for the page. A card claiming
   #7c53f0 beside a node drawn in #6d28d9 is a legend that misinforms, which is
   worse than no legend, and no amount of keeping the two "in sync" by hand would
   stay true through the next palette edit. One property, both places.

   Projects' cards are not a legend and keep the site palette. This is the one place
   the two card sets deliberately resolve differently; everything about how the
   colour is *used* -- border, hover, glyph tile, question rule -- is still shared,
   because all of it reads --topic and neither rule knows where the value came
   from. */
.domain-card[data-group="retrieval"]  { --topic: var(--topic-retrieval); }
.domain-card[data-group="agentic"]    { --topic: var(--topic-agentic); }
.domain-card[data-group="evaluation"] { --topic: var(--topic-evaluation); }
.domain-card[data-group="other"]      { --topic: var(--topic-other); }
/* The glyph tile carries the card's own topic hue rather than the site accent. On
   About that is what makes the card a swatch a reader can match to a node in the
   constellation; on Projects it restores the topic-tinted tile the published panel
   has. Both read --topic, so neither rule needs to know which palette supplied it.
   The fallback keeps a tile that has no data-group looking like it used to. */
.domain-ic {
  display: inline-grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 10px;
  background: color-mix(in srgb, var(--topic, var(--accent-1)) 15%, transparent);
  margin-bottom: 0.9rem;
}
.domain-ic svg { width: 20px; height: 20px; color: var(--topic, var(--accent-1)); }
/* The two card sets share everything about the card except the type scale, which
   is where they genuinely differ and where the published site is the authority:
   About's title is 13.6px mono with letter-spacing over a 21.6px display lead,
   and Projects' area name is a 15.68px display heading with no lead under it at
   all. Matching About's typography here would change sizes zhanlinliu.com already
   serves, so only the margin -- the spacing rhythm -- is shared, and each keeps
   its own size, weight, family and colour. */
.domain-name { margin: 0 0 0.8rem; }
/* 0.98rem/1.3/600 is 15.68px/20.384px on a 16px root: the published size. The
   family is the display face the <h3> already inherits, and the colour is --text,
   also inherited -- neither is restated, so neither can drift from the heading
   scale the rest of the site uses. */
.domain-name { font-weight: 600; font-size: 0.98rem; line-height: 1.3; }

/* ---------- The question row ----------
   The questions the area's projects answer. About used to carry one example question
   per card in a .wq-link, styled by this same rule; the four cards became corner nodes
   in the constellation and the example went with them, so in practice this now styles
   Projects' .dq alone. The .wq-link selector is kept on the rule rather than deleted:
   it is the single definition of what a question row looks like on this site, and if
   About ever grows one back it should not be re-invented.

   The sharing was always done by naming both classes rather than by selecting `a` or
   `button`, so no rule here could be reached by an element merely because of its tag --
   About's row was an <a> that navigated and Projects' is a <button> that selects. */
/* .wq-list is no longer in the markup: About's cards carry no question list. The
   rule is Projects' now. */
.domain-questions {
  list-style: none;
  margin: 1rem 0 0;
  padding: 0;
  display: grid;
  gap: 0.5rem;
}
.wq-link,
.dq {
  display: grid;
  gap: 0.55rem;
  align-items: start;
  width: 100%;
  text-align: left;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  border: 0;
  border-left: 2px solid color-mix(in srgb, var(--topic, var(--accent-1)) 45%, transparent);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: color-mix(in srgb, var(--topic, var(--accent-1)) 6%, transparent);
  padding: 0.5rem 0.7rem;
  color: var(--text);
  font-family: var(--font-body);
  font-size: 0.92rem;
  line-height: 1.5;
  text-decoration: none;
  /* The smallest target a finger can be expected to hit, on both. */
  min-height: 44px;
}
/* question | arrow. The arrow column is fixed so the text starts and ends on the
   same line in every row, whatever its length. */
.wq-link { grid-template-columns: minmax(0, 1fr) 0.9rem; }
/* marker | question | arrow: the extra column is the selection state.
   0.9rem/1.45 is 14.4px/20.88px on a 16px root -- the published size. About's row
   is 14.72px, so the two look the same at a glance while each keeps the size its
   own panel already serves; the size is the one thing the shared rule above does
   not get to decide. */
.dq {
  grid-template-columns: 0.5rem minmax(0, 1fr) 0.9rem;
  font-size: 0.9rem;
  line-height: 1.45;
}

/* The arrow. A real span on Projects, because projects.js builds that row and can
   mark it aria-hidden; generated content on About, because its row is authored
   markup whose entire text content is the question -- a child element there would
   have to be excluded from every assertion that the visible question matches the
   project's own. Same treatment either way. It earns its place only once the row
   is live: always-on arrows on eleven rows read as clutter. */
.wq-link::after,
.dq-chev {
  color: var(--topic, var(--accent-1));
  font-size: 0.85rem;
  line-height: 1.55;
  opacity: 0;
}
.wq-link::after { content: "\2192"; }
.wq-link:hover::after,
.wq-link:focus-visible::after,
.dq:hover .dq-chev,
.dq[aria-current="true"] .dq-chev { opacity: 1; }

/* Colour only, so it does not need the (hover: hover) gate: a tapped row left in
   :hover on iOS just stays slightly tinted, which reads as a selection. The
   translateX nudge that goes with it is gated, and lives in the hover block. */
.wq-link:hover,
.dq:hover { background: color-mix(in srgb, var(--topic, var(--accent-1)) 14%, transparent); }
/* One focus ring for both, and it must survive the hover tint above -- an outline
   is drawn outside the box, so it does. */
.wq-link:focus-visible,
.dq:focus-visible { outline: 2px solid var(--accent-1); outline-offset: 2px; }

/* .wq-more, the per-card "Explore more" link, is gone with the example question.
   About's route into Projects is the section head's "See all 11 projects" link,
   which was already there; the cards themselves navigate nowhere now, which is
   consistent with the constellation under them doing the same. */

/* ---------- Beyond work ---------- */

/* ---------- Elsewhere / links ---------- */
.about-links .section-h:first-child { margin-top: 0; }
.links { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.55rem; }
.links a { display: inline-flex; align-items: center; gap: 0.6rem; color: var(--text); font-size: 0.95rem; transition: color 0.18s; }
.links a:hover .ic-brand { color: var(--accent-1); }
.links a:hover { color: var(--accent-1); }
.side-note { font-family: var(--font-mono); font-size: 0.78rem; color: var(--faint); margin-top: 1.1rem; }

/* ---------- Cards (Projects / Research) ---------- */
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.1rem; }
.cards-tight { grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); margin-bottom: 1rem; }

.card {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.4rem 1.4rem 1.5rem;
  transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s;
  overflow: hidden;
}
.card::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 2px;
  background: var(--accent-grad);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.28s ease;
}
.card:hover { transform: translateY(-4px); border-color: var(--border-strong); box-shadow: var(--shadow); }
.card:hover::after { transform: scaleX(1); }

.card-title { font-size: 1.15rem; margin-bottom: 0.6rem; }
.card-body { color: var(--muted); font-size: 0.92rem; margin: 0; }
.card-meta { font-family: var(--font-mono); font-size: 0.76rem; color: var(--faint); margin: 0.5rem 0 0.3rem; }
.card-date { font-family: var(--font-mono); font-size: 0.76rem; color: var(--faint); margin: 0.15rem 0 0; }
.card-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem;
  margin: 0.85rem 0 0;
}
.card-links a {
  font-family: var(--font-mono);
  font-size: 0.76rem;
  color: var(--accent-1);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color 0.18s, transform 0.18s;
}
.card-links a:hover { border-bottom-color: var(--accent-1); }
.card-links a:focus-visible { outline: 2px solid var(--accent-1); outline-offset: 3px; border-radius: 2px; }
.card-authors { color: var(--muted); font-size: 0.85rem; margin: 0; }

.tags { list-style: none; margin: 1rem 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.4rem; }
.tags li { font-family: var(--font-mono); font-size: 0.72rem; padding: 0.25rem 0.55rem; border-radius: var(--radius-sm); background: var(--accent-soft); color: var(--accent-1); }

.badge {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  padding: 0.25rem 0.6rem;
  border-radius: 999px;
  margin-bottom: 0.75rem;
  border: 1px solid transparent;
}
.badge-granted { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 45%, transparent); background: color-mix(in srgb, var(--ok) 12%, transparent); }
.badge-submitted { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 45%, transparent); background: color-mix(in srgb, var(--warn) 12%, transparent); }
.badge-draft { color: var(--neutral); border-color: color-mix(in srgb, var(--neutral) 40%, transparent); background: color-mix(in srgb, var(--neutral) 12%, transparent); }
.badge-award { color: var(--accent-2); border-color: color-mix(in srgb, var(--accent-2) 45%, transparent); background: color-mix(in srgb, var(--accent-2) 12%, transparent); }
.badge-review { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 45%, transparent); background: color-mix(in srgb, var(--warn) 12%, transparent); }

/* Publications */
.pub-note { color: var(--muted); font-size: 0.9rem; margin: -0.4rem 0 1.4rem; max-width: 62ch; }
.pub-note code { font-family: var(--font-mono); font-size: 0.82em; color: var(--faint); }
.pub-note a { color: var(--accent-1); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--accent-1) 45%, transparent); }

.pub-year {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  color: var(--accent-1);
  margin: 1.8rem 0 0.8rem;
}
.pub-groups .pub-year:first-child { margin-top: 0; }

.pub-list { list-style: none; margin: 0 0 1rem; padding: 0; display: grid; gap: 1rem; }

.pub-row {
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 0.9rem 1.2rem;
  padding: 1.1rem 1.3rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel);
  transition: border-color 0.2s;
}
.pub-row:hover { border-color: var(--border-strong); }

.pub-ic {
  flex-shrink: 0;
  width: 34px;
  height: 34px;
  padding: 8px;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent-1);
}

.pub-info { flex: 1 1 260px; min-width: 0; display: grid; gap: 0.3rem; }
.pub-title { font-family: var(--font-display); font-weight: 600; font-size: 1.02rem; margin: 0; }
.pub-authors { color: var(--muted); font-size: 0.9rem; margin: 0; }
.pub-venue { font-family: var(--font-mono); font-size: 0.76rem; color: var(--faint); margin: 0; }

.pub-side {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.65rem;
  margin-left: auto;
}
.pub-side .badge { margin-bottom: 0; }

.pub-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.5rem; }
.pub-actions .btn-ghost { padding: 0.4rem 0.75rem; }

@media (max-width: 560px) {
  .pub-row { flex-direction: column; align-items: stretch; }
  .pub-side { align-items: flex-start; margin-left: 0; width: 100%; }
  .pub-actions { justify-content: flex-start; }
}

/* =========================================================================
   CONTACT — info column + mailto form
   ========================================================================= */
.contact-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
  gap: clamp(1.25rem, 3vw, 2.5rem);
  align-items: start;
  max-width: 1080px;
}

.contact-info {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.2rem;
}

.contact-row {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 0.65rem;
  border-radius: var(--radius-sm);
  color: var(--text);
  transition: background 0.18s, color 0.18s;
}
a.contact-row:hover { background: var(--accent-soft); }

.contact-ic {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent-1);
}
.contact-ic .ic { width: 18px; height: 18px; }

.contact-row-text { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.contact-row-label {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--faint);
}
.contact-row-value { font-size: 0.95rem; word-break: break-word; }

.contact-form {
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.5rem;
}

.field { display: flex; flex-direction: column; gap: 0.4rem; }
.field label {
  font-family: var(--font-mono);
  font-size: 0.76rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}
.field input,
.field textarea {
  width: 100%;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 0.95rem;
  padding: 0.7rem 0.85rem;
  outline: none;
  transition: border-color 0.18s, box-shadow 0.18s;
}
.field textarea { resize: vertical; min-height: 6.5rem; }
.field input:focus,
.field textarea:focus { border-color: var(--accent-1); box-shadow: 0 0 0 3px var(--accent-soft); }

.contact-send { display: inline-flex; align-items: center; gap: 0.55rem; align-self: flex-start; }
.contact-send .ic { width: 16px; height: 16px; }

.contact-status { font-family: var(--font-mono); font-size: 0.8rem; color: var(--accent-1); min-height: 1.2em; margin: 0; }
.contact-note { font-size: 0.85rem; color: var(--muted); margin: 0; }
.contact-note a { color: var(--accent-1); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--accent-1) 45%, transparent); }

/* The honeypot. `display: none` rather than an off-screen position on purpose: some
   bots skip fields they can tell are invisible, but the ones this catches fill the
   form from the DOM without rendering it at all, and display:none is the variant that
   no browser will ever autofill. */
.hp-field { display: none; }

@media (max-width: 780px) {
  .contact-body { grid-template-columns: 1fr; }
}

/* =========================================================================
   EXPERIENCE — pinned education, work/internship select bar + timeline
   ========================================================================= */

/* ---------- Education strip (always visible, static) ---------- */
.exp-edu-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 0.85rem;
  margin: 1.5rem 0 2.2rem;
}
/* Education sits under the timeline on its own tab now. */
.exp-edu { margin-top: clamp(2rem, 4vw, 3rem); }
.exp-resume { margin-bottom: 1.6rem; }
.exp-edu-card {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 1rem 1.1rem;
  transition: border-color 0.2s;
}
.exp-edu-card:hover { border-color: var(--border-strong); }
.exp-edu-ic {
  flex-shrink: 0;
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--accent-soft);
}
.exp-edu-ic .exp-ic { color: var(--accent-1); }
.exp-edu-info { min-width: 0; }
.exp-edu-degree { font-family: var(--font-display); font-weight: 600; font-size: 1.05rem; line-height: 1.3; margin: 0; color: var(--text); }
.exp-edu-school { font-size: 0.86rem; color: var(--muted); margin: 0.3rem 0 0; }
.exp-edu-meta { font-family: var(--font-mono); font-size: 0.74rem; color: var(--faint); margin: 0.35rem 0 0; }

/* ---------- Two-column body: timeline + sticky detail ---------- */
.exp-body {
  display: grid;
  grid-template-columns: minmax(260px, 380px) 1fr;
  align-items: start;
  gap: clamp(1.25rem, 3vw, 2.5rem);
  opacity: 0;
  transform: translateY(12px);
}
.exp-body.is-revealed {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.5s cubic-bezier(0.22, 1, 0.36, 1), transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}

.exp-timeline-col { min-width: 0; }

/* Segmented select bar (Work Experience / Internships) */
.exp-segbar,
.rec-segbar {
  display: inline-flex;
  gap: 0.2rem;
  padding: 0.25rem;
  margin-bottom: 1.4rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--panel);
}
.exp-seg,
.proj-seg {
  appearance: none;
  border: 0;
  cursor: pointer;
  background: transparent;
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.01em;
  padding: 0.5rem 1.05rem;
  border-radius: 999px;
  transition: color 0.18s, background 0.18s;
}
.exp-seg:hover,
.proj-seg:hover { color: var(--text); }
/* Both remaining consumers are real tablists on aria-selected: .exp-seg switches
   Experience panels and .proj-seg is reused by the Recognition tabs. The
   aria-pressed variants belonged to the retired Projects filter pills, which the
   problem-area cards replaced, so they went with them. */
.exp-seg[aria-selected="true"],
.proj-seg[aria-selected="true"] {
  color: #06121a;
  background: var(--accent-grad);
  font-weight: 500;
}
:root[data-theme="light"] .exp-seg[aria-selected="true"],
:root[data-theme="light"] .proj-seg[aria-selected="true"] { color: #fff; }

/* Vertical timeline */
.exp-vlist {
  position: relative;
  list-style: none;
  margin: 0;
  padding: 0;
}
.exp-vlist::before {
  content: "";
  position: absolute;
  left: 22px;
  top: 36.5px;
  bottom: 36.5px;
  width: 2px;
  border-radius: 2px;
  background: linear-gradient(var(--accent-2), color-mix(in srgb, var(--accent-2) 15%, transparent));
  opacity: 0.35;
}
.exp-node { position: relative; }
.exp-node-btn {
  position: relative;
  z-index: 1;
  appearance: none;
  border: 0;
  width: 100%;
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: 0.65rem 0.6rem;
  background: transparent;
  border-radius: var(--radius-sm);
  color: inherit;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: background 0.18s;
}
.exp-node-btn:hover { background: var(--accent-soft); }
.exp-dot {
  flex-shrink: 0;
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--bg);
  border: 2px solid var(--border-strong);
  transition: transform 0.2s, box-shadow 0.2s, background 0.2s, border-color 0.2s;
}
.exp-ic {
  width: 16px;
  height: 16px;
  color: var(--muted);
  transition: color 0.2s;
}
.exp-node-info { min-width: 0; display: flex; flex-direction: column; gap: 0.15rem; }
.exp-node-name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.94rem;
  color: var(--text);
  word-break: break-word;
}
.exp-node-role { font-size: 0.8rem; line-height: 1.25; color: var(--muted); word-break: break-word; }
.exp-node-dates { font-family: var(--font-mono); font-size: 0.72rem; color: var(--faint); }

.exp-node.is-active .exp-node-btn { background: var(--accent-soft); }
.exp-node.is-active .exp-dot {
  background: var(--accent-1);
  border-color: var(--accent-1);
  box-shadow: 0 0 0 5px var(--accent-soft), 0 0 14px 1px color-mix(in srgb, var(--accent-1) 50%, transparent);
}
.exp-node.is-active .exp-ic { color: var(--bg); }

/* ---------- Sticky detail panel ---------- */
.exp-detail {
  position: sticky;
  top: calc(var(--head-h) + 1rem);
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.5rem clamp(1.1rem, 2.5vw, 1.8rem);
  box-shadow: var(--shadow);
  min-height: 280px;
  max-height: calc(100dvh - var(--head-h) - 2rem);
  overflow-y: auto;
  transition: opacity 0.18s ease;
}
.exp-detail.is-fading { opacity: 0; }
.exp-detail-head { display: flex; align-items: center; gap: 0.65rem; }
.exp-detail-ic-wrap {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--accent-soft);
}
.exp-detail-ic-wrap .exp-ic { width: 18px; height: 18px; color: var(--accent-1); }
.exp-detail-company {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.25rem, 3vw, 1.55rem);
  letter-spacing: -0.01em;
  margin: 0;
  background: var(--accent-grad);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.exp-detail-role { font-size: 1rem; color: var(--text); margin: 0.85rem 0 0; font-weight: 500; }
.exp-detail-meta { font-family: var(--font-mono); font-size: 0.78rem; color: var(--muted); margin: 0.25rem 0 0; }

.exp-detail-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem 1.6rem;
  margin: 1.3rem 0 0;
}
.exp-detail-block { margin-top: 1.3rem; }
.exp-detail-grid .exp-detail-block { margin-top: 0; }
.exp-detail-tag {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--accent-1);
  margin: 0 0 0.4rem;
}
.exp-detail-text { color: var(--muted); font-size: 0.92rem; margin: 0; }
.exp-highlights { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.55rem; }
.exp-highlights li { position: relative; padding-left: 1.2rem; color: var(--text); opacity: 0.92; font-size: 0.92rem; }
.exp-highlights li::before { content: "▹"; position: absolute; left: 0; color: var(--accent-2); }
.exp-detail-text strong { font-weight: 600; color: var(--text); }
.exp-highlights strong { font-weight: 600; color: var(--text); opacity: 1; }

/* Mobile: stack education, segbar, full-width timeline, then detail below. */
@media (max-width: 820px) {
  .exp-body { grid-template-columns: 1fr; }
  .exp-detail { position: static; top: auto; margin-top: 1.5rem; max-height: none; overflow-y: visible; }
  .exp-detail-grid { grid-template-columns: 1fr; gap: 0; }
  .exp-detail-grid .exp-detail-block { margin-top: 1.3rem; }
  .exp-detail-grid .exp-detail-block:first-child { margin-top: 0; }
}

/* =========================================================================
   PROJECTS — scope filters and the authored detail content. The card grid and
   the detail dialog it opened are gone; see WORK at the end of this file for the
   list/detail block that replaced them.
   ========================================================================= */

/* ---------- Recognition tabs (.proj-seg, shares .exp-seg styling above) ----------
   The Projects panel's own pill row retired with .proj-segbar; the Recognition
   panel still marks its three tabs up as .proj-seg, so these rules stay.

   Stretching the three tabs to a third of the panel each was tried and reverted:
   the user's line was that full width "does not improve the UI". So this is the
   published treatment -- three pills at their natural width -- and every rule here
   is the one zhanlinliu.com serves. */
.rec-segbar { margin-bottom: 1.5rem; }
.proj-seg:focus-visible { outline: 2px solid var(--accent-1); outline-offset: 2px; }
/* Labels this long overflow a phone viewport in one row, so wrap them and
   square off the pill, which reads better than a two-row lozenge. */
@media (max-width: 620px) {
  .rec-segbar {
    display: flex;
    flex-wrap: wrap;
    max-width: 100%;
    border-radius: var(--radius);
  }
}
.proj-group-sub {
  color: var(--muted);
  font-size: 0.92rem;
  margin: 0 0 1.3rem;
}

/* The glyph tile takes its hue from the project's topic, so the four scopes read
   as distinct sections rather than one filtered list. Reuses existing palette
   tokens rather than inventing colours, so light theme stays in step. */
.proj-ic {
  flex: none;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-sm);
  border: 1px solid color-mix(in srgb, var(--proj-ic, var(--accent-1)) 42%, transparent);
  background:
    linear-gradient(135deg, color-mix(in srgb, var(--proj-ic, var(--accent-1)) 18%, transparent), transparent 82%),
    var(--elevated);
}
/* Tint follows the project's own topic: --topic is the house token for that (see
   .work-detail[data-group] just below), --proj-ic the narrower override. */
.proj-ic-svg { width: 17px; height: 17px; color: var(--proj-ic, var(--topic, var(--accent-1))); }
/* The topic accents the retired card grid carried per group. The authored
   .work-row is hidden, so they ride on the detail panel, and are inert until
   projects.js copies data-group across. The question buttons need no rule of
   their own: they inherit --topic from the problem card they sit in. */
.work-detail[data-group="retrieval"]  { --topic: var(--topic-retrieval); }
.work-detail[data-group="agentic"]    { --topic: var(--topic-agentic); }
.work-detail[data-group="evaluation"] { --topic: var(--topic-evaluation); }
.work-detail[data-group="other"]      { --topic: var(--topic-other); }

/* The kind glyph in front of a resource link. buildResourceList() clones this
   from TAB_ICONS into every .work-resource, so it is the one survivor of the
   retired chip strip. */
.proj-chip-ic {
  width: 13px;
  height: 13px;
  flex: none;
  opacity: 0.85;
}

/* The answer, revealed. Leads the dialog above the detailed description. */
.project-answer {
  margin: 0 0 1.1rem;
  padding: 0.8rem 1rem;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  color: var(--text);
  font-size: 1rem;
  line-height: 1.55;
}
/* Detail content shared between the authored source templates and the rendered
   detail panel: org / title / overview / impact / tags. */
.project-org { font-family: var(--font-mono); font-size: 0.76rem; letter-spacing: 0.04em; text-transform: uppercase; color: var(--accent-1); margin: 0 0 0.4rem; }
.project-overview { color: var(--text); opacity: 0.92; font-size: 1rem; max-width: 62ch; margin: 0 0 1rem; }
.impact { font-family: var(--font-mono); font-size: 0.85rem; color: var(--accent-2); margin: 0 0 1rem; }

/* ---------- Resource links: authored source, rendered into the detail ----------
   The chip strip and the tablist that used to present these are gone; the rules
   that styled them went with the markup. What is left is the authored <ul>,
   which stays out of sight — projects.js reads it and emits .work-resource. */
.proj-tab-links { display: none; }

/* Revealed detail: location/dates, then one block per workstream. The dialog
   already scrolls (max-height + overflow-y), which this content relies on. */
.project-meta {
  margin: 0 0 1.4rem;
  font-family: var(--font-mono);
  font-size: 0.76rem;
  color: var(--faint);
}
.project-block { margin: 0 0 1.5rem; }
.project-block:last-of-type { margin-bottom: 1.2rem; }
.project-block-h {
  margin: 0 0 0.65rem;
  font-size: 0.95rem;
  color: var(--accent-1);
  letter-spacing: -0.005em;
}
.project-points { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.55rem; }
.project-points li {
  position: relative;
  padding-left: 1rem;
  color: var(--muted);
  font-size: 0.93rem;
  line-height: 1.62;
}
.project-points li::before {
  content: "";
  position: absolute;
  left: 0; top: 0.62em;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--accent-1) 55%, transparent);
}
.project-points strong { color: var(--text); font-weight: 600; }

/* =========================================================================
   BLOG — markdown post list + detail
   ========================================================================= */
.blog-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1.1rem;
}

.blog-card {
  appearance: none;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  padding: 1.4rem 1.4rem 1.5rem;
  text-align: left;
  cursor: pointer;
  color: inherit;
  font-family: inherit;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s;
}
.blog-card:hover, .blog-card:focus-visible {
  transform: translateY(-4px);
  border-color: var(--border-strong);
  box-shadow: var(--shadow);
}
.blog-card-title { font-family: var(--font-display); font-size: 1.1rem; margin: 0; }
.blog-card-date { font-family: var(--font-mono); font-size: 0.76rem; color: var(--faint); margin: 0; }
.blog-card-summary { color: var(--muted); font-size: 0.92rem; margin: 0; }

/* Empty state (no posts yet). */
.blog-empty {
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  background: var(--panel);
  padding: 2rem 1.4rem;
  text-align: center;
  max-width: 56ch;
  margin: 0 auto;
}
.blog-empty-title { font-family: var(--font-display); font-size: 1.1rem; margin: 0 0 0.5rem; }
.blog-empty-hint { color: var(--muted); font-size: 0.9rem; margin: 0; }
.blog-empty-hint code {
  font-family: var(--font-mono);
  font-size: 0.85em;
  background: var(--accent-soft);
  color: var(--accent-1);
  padding: 0.1rem 0.35rem;
  border-radius: 4px;
}

/* Post detail view. */
.blog-post { max-width: 100%; }
.blog-post-body {
  max-width: 72ch;
  margin: 1.5rem auto 0;
  color: var(--text);
}
.blog-post-body h1, .blog-post-body h2, .blog-post-body h3 {
  font-family: var(--font-display);
  letter-spacing: -0.01em;
  margin: 1.6rem 0 0.8rem;
}
.blog-post-body h1:first-child, .blog-post-body h2:first-child, .blog-post-body h3:first-child { margin-top: 0; }
.blog-post-body p { color: var(--text); opacity: 0.92; font-size: 1rem; margin: 0 0 1.1rem; }
.blog-post-body ul, .blog-post-body ol { margin: 0 0 1.1rem; padding-left: 1.4rem; color: var(--text); opacity: 0.92; }
.blog-post-body li { margin-bottom: 0.4rem; }
.blog-post-body blockquote {
  margin: 0 0 1.1rem;
  padding: 0.2rem 1.1rem;
  border-left: 3px solid var(--accent-2);
  color: var(--muted);
  font-style: italic;
}
.blog-post-body pre {
  margin: 0 0 1.1rem;
  padding: 1rem 1.1rem;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow-x: auto;
  max-width: 100%;
}
.blog-post-body pre code {
  font-family: var(--font-mono);
  font-size: 0.85rem;
  color: var(--text);
  background: none;
  padding: 0;
  white-space: pre;
}
.blog-post-body code {
  font-family: var(--font-mono);
  font-size: 0.88em;
  background: var(--accent-soft);
  color: var(--accent-1);
  padding: 0.1rem 0.35rem;
  border-radius: 4px;
}
.blog-post-body a { color: var(--accent-1); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--accent-1) 45%, transparent); }
.blog-post-body a:hover { color: var(--accent-2); }
.blog-post-body img { max-width: 100%; height: auto; border-radius: var(--radius-sm); display: block; margin: 0 0 1.1rem; }
.blog-post-body hr { border: 0; border-top: 1px solid var(--border); margin: 1.6rem 0; }

/* =========================================================================
   AI ASSISTANT — collapsible
   ========================================================================= */
.chat-launcher {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.55rem 1.05rem;
  border: 0;
  border-radius: 999px;
  cursor: pointer;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 0.9rem;
  color: #06121a;
  background: var(--accent-grad);
  box-shadow: 0 8px 24px -10px var(--accent-1);
  transition: transform 0.2s, box-shadow 0.2s, opacity 0.2s;
  white-space: nowrap;
}
:root[data-theme="light"] .chat-launcher { color: #fff; }
.chat-launcher:hover { transform: translateY(-1px); box-shadow: 0 12px 30px -10px var(--accent-1); }
.launcher-dot { width: 8px; height: 8px; border-radius: 50%; background: #06121a; opacity: 0.85; animation: pulse 2.4s ease-in-out infinite; }
:root[data-theme="light"] .launcher-dot { background: #fff; }
@keyframes pulse { 0%, 100% { transform: scale(1); opacity: 0.9; } 50% { transform: scale(1.6); opacity: 0.35; } }

.chat {
  position: fixed;
  top: calc(var(--head-h) + 0.75rem);
  right: clamp(1rem, 3vw, 2rem);
  z-index: 41;
  width: min(400px, calc(100vw - 2rem));
  height: min(640px, calc(100vh - var(--head-h) - 2rem));
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: 18px;
  box-shadow: var(--shadow);
  overflow: hidden;
  transform-origin: top right;
  opacity: 0;
  transform: translateY(-12px) scale(0.97);
  pointer-events: none;
  transition: opacity 0.24s ease, transform 0.24s cubic-bezier(0.22, 1, 0.36, 1);
}
body.chat-open .chat { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }

.chat-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.9rem 1rem;
  border-bottom: 1px solid var(--border);
  background: linear-gradient(180deg, var(--elevated), var(--panel));
}
.chat-title { display: flex; align-items: center; gap: 0.7rem; }
.chat-badge {
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border-radius: 10px;
  font-family: var(--font-mono);
  font-size: 0.8rem;
  font-weight: 500;
  color: #06121a;
  background: var(--accent-grad);
}
:root[data-theme="light"] .chat-badge { color: #fff; }
.chat-name { margin: 0; font-family: var(--font-display); font-weight: 600; font-size: 0.98rem; }
.chat-sub { margin: 0; font-size: 0.76rem; color: var(--muted); }

.chat-log { flex: 1; min-height: 0; overflow-y: auto; padding: 1.1rem; display: flex; flex-direction: column; gap: 0.8rem; }
.msg { max-width: 88%; padding: 0.7rem 0.95rem; border-radius: 14px; font-size: 0.92rem; line-height: 1.5; white-space: pre-wrap; word-wrap: break-word; }
.msg.user { align-self: flex-end; background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--accent-1) 35%, transparent); color: var(--text); border-bottom-right-radius: 4px; }
.msg.assistant { align-self: flex-start; background: var(--elevated); border: 1px solid var(--border); border-bottom-left-radius: 4px; }
.msg.intro { color: var(--muted); }

.msg.typing { display: inline-flex; gap: 5px; align-items: center; }
.msg.typing span { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); animation: typing-pulse 1.3s infinite ease-out; }
.msg.typing span:nth-child(2) { animation-delay: 0.18s; }
.msg.typing span:nth-child(3) { animation-delay: 0.36s; }
@keyframes typing-pulse { 0%, 70%, 100% { opacity: 0.35; transform: scale(0.85); } 35% { opacity: 1; transform: scale(1); } }

.starters { display: flex; flex-wrap: wrap; gap: 0.4rem; padding: 0 1.1rem 0.6rem; }
.starter-chip {
  appearance: none;
  cursor: pointer;
  font-family: var(--font-body);
  font-size: 0.78rem;
  padding: 0.4rem 0.7rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--muted);
  transition: border-color 0.18s, color 0.18s;
}
.starter-chip:hover { border-color: var(--accent-1); color: var(--text); }

.chat-form { display: flex; gap: 0.5rem; padding: 0.85rem 1rem; border-top: 1px solid var(--border); background: var(--panel); }
#chat-input {
  flex: 1;
  min-width: 0;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: 10px;
  color: var(--text);
  font-family: var(--font-body);
  font-size: 0.92rem;
  padding: 0.65rem 0.8rem;
  outline: none;
  transition: border-color 0.18s;
}
#chat-input:focus { border-color: var(--accent-1); }
#send-btn {
  appearance: none;
  border: 0;
  cursor: pointer;
  display: grid;
  place-items: center;
  border-radius: 10px;
  width: 42px;
  color: #06121a;
  background: var(--accent-grad);
  transition: transform 0.15s, opacity 0.15s;
}
:root[data-theme="light"] #send-btn { color: #fff; }
#send-btn:hover:not(:disabled) { transform: translateY(-1px); }
#send-btn:disabled, #chat-input:disabled { opacity: 0.55; cursor: default; }

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

/* ---------- Responsive ---------- */
/* 1020px, not 860: the tab bar grew from five tabs to six when Experience got
   its own tab, taking it from 441px to 551px. The header stopped fitting on one
   row somewhere around 1010px, so between 880 and 1000 the actions ran off the
   right edge and the page scrolled sideways — measured 114px of overflow at
   900px. Wrapping earlier costs a header row on a small laptop; overflowing
   costs a horizontal scrollbar, which is worse. */
@media (max-width: 1020px) {
  .site-head { flex-wrap: wrap; height: auto; gap: 0.75rem; padding: 0.7rem 1rem; }
  .wordmark { order: 1; }
  .head-actions { order: 2; margin-left: auto; }
  .tabs { order: 3; margin: 0; width: 100%; overflow-x: auto; justify-content: flex-start; }
}

@media (max-width: 560px) {
  /* Full-screen sheet. `inset: 0` is load-bearing: the desktop rule sets `top`,
     and a fixed box with top + bottom + a set height is over-constrained, so
     `bottom` loses and the panel hangs its own header height below the screen —
     which put the text input off-screen and made the assistant untypable on a
     phone. Clearing top/height here is what keeps the input reachable. */
  .chat {
    inset: 0;
    width: auto;
    height: auto;
    border-radius: 0;
    border: 0;
    transform-origin: center bottom;
  }
  /* The send row is the last thing above the home indicator. */
  .chat-form { padding-bottom: calc(0.85rem + env(safe-area-inset-bottom)); }
}

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; scroll-behavior: auto !important; }
  .graph-bg { opacity: 0.3; }
}

/* Cold-start notice under the typing indicator (see chat.js). */
.chat-waking {
  margin: 0.35rem 0 0.2rem;
  padding: 0 0.2rem;
  color: var(--faint);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  line-height: 1.5;
}

/* The agent narrating itself. Same dim register as .chat-waking -- it is
   scaffolding, not content, and it is gone the moment the answer starts. */
.chat-status {
  margin: 0.35rem 0 0;
  font-size: 0.82rem;
  line-height: 1.4;
  /* 64%, not 58%: measured 4.39:1 on the light ground, under the 4.5:1 AA
     floor for text this size. 64% reads as 5.19:1 light and 12.5:1 dark and
     is still plainly quieter than the answer it sits above. */
  color: color-mix(in srgb, var(--text) 64%, transparent);
  font-style: italic;
}

.chat-suggestions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0.5rem 0 0.25rem;
}

/* =========================================================================
   ABOUT — condensed single page: contact line, hero, work, timeline, skills,
   education. Section headings share .about-h so the five read as peers.
   ========================================================================= */
/* ---- the field behind the whole panel ----
   The ground moves here from .aq-stage. That single move is what dissolves the seam:
   before it, the constellation sat on --map-ground inside a bordered rectangle while
   the hero and the journey sat on the page's own colour, so the map read as a lit
   card on a flat page. Now the panel is the field and the constellation is simply the
   part of it that has a ring in it. */
#panel-about {
  /* No ground and no radius. The body carries the field now, so a background here would
     be a second, identical rectangle -- and the radius was what drew the card's corner.
     position: relative stays, because the panel's children are layered above the
     page-level canvas by the rule below. */
  position: relative;
}
.site-cosmos {
  /* Fixed rather than absolute, and the viewport rather than a panel.

     Fixed is what makes the field a place the page moves through instead of a texture
     printed on it: the stars hold still while the content scrolls over them, which is
     the whole difference between a background image and depth. It is also the cheaper
     of the two -- a fixed canvas never re-rasterises on scroll.

     Behind everything and never a click target. */
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 0;
  pointer-events: none;
}
/* The page's own chrome sits above the field. .stage already carries z-index: 1, so the
   panels are above the canvas by construction; the header needs saying because it is a
   sibling of the canvas rather than a descendant of the stage. */
.site-head { position: relative; z-index: 2; }

/* Hero left, contact icons pinned top-right. Not in the site header: measured
   spare room there is only 9px at 980px wide, and four 36px tiles need ~160px,
   so they would break the 980-1150px range or have to vanish on a laptop. */
.about-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
}
.about-top .about-hero { flex: 1 1 28rem; }

/* Icon-only contact links. No dot separators — with glyphs they read as noise.
   Scoped by descendant selector rather than a new class: .contact-ic already
   exists in the Contact panel and would have collided. */
.about-contact {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  flex: 0 0 auto;
}
.about-contact a {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--elevated);
  color: var(--muted);
  transition: color 0.18s, border-color 0.18s, background 0.18s, transform 0.18s;
}
.about-contact a:hover {
  color: var(--accent-1);
  border-color: var(--accent-1);
  background: var(--accent-soft);
  transform: translateY(-1px);
}
.about-contact a:focus-visible { outline: 2px solid var(--accent-1); outline-offset: 3px; }
.about-contact svg { width: 17px; height: 17px; }

/* =========================================================================
   The About constellation — eleven questions, one of them centred

   Full width, following the intro. Two grounds, dark and daylight, both declared
   as --map-* tokens at the top of this file; nothing here names a colour.

   The geometry is percentages of .aq-stage, so one aspect ratio fixes it at every
   width the panel can take. Every satellite carries its whole question: the dot
   sits on the ring point and the label runs outward from it, away from the centre,
   which is what makes eleven readable labels fit a box this shape.
   ========================================================================= */
.about-qmap { margin: 0 0 3.2rem; }
.aq-stage {
  position: relative;
  /* 1448:500 -- 1368x472 at a 1440px window, which is the whole graph inside the fold.
     The graph's top is 426, so 472 is what fits, and 500/1448 of the panel's width is
     that height.

     The history matters, because 775 was called a floor here: 1448:886 with no area
     blocks, 1448:1010 when they arrived, 1448:775 when the user asked for the cloud to
     land at first impression. 775 really was a floor for THAT composition -- the ring at
     30/30 centred at 50/45, area blocks pinned in the corners -- and it held 65 percent
     of the graph above the fold.

     What was wrong was the composition, not the number. Moving the blocks inward on
     their own makes the collision worse, since inward is into the ring's diagonals; the
     whole thing had to be re-solved at once. It was: the ring is 40/40 at cx 42 (see
     RING in about.js), the area blocks scale with the box instead of holding a fixed
     text size, and the four of them come 92px in off the sides. Measured at that shape,
     the tightest gap between any two of the eleven labels is 50.9px -- more room than
     the tall box ever had, in 64 percent of the height. Held as a ratio rather than a
     height so the percentage geometry means the same thing at any width. */
  /* 1448:620, up from 1448:500 -- "the question universe graph can also have a bit
     bigger length". A taller box is not free: this ratio IS the cqw basis, so every label
     width and font size resolves against it, and the ring's 40/40 radii are percentages
     of it. Taller means the vertical radius covers more pixels, which spreads the
     satellites apart rather than crowding them -- so the risk was the opposite one, of
     the field going loose and the corner blocks drifting away from the ring they
     annotate. Re-measured after the change at four widths: the tightest gap between any
     two of the eleven labels is reported in the commit, and it did not fall below the
     50.9px the flat box held. */
  aspect-ratio: 1448 / 620;
  /* The centred label's width is a share of this box, and a percentage could not
     express it: the label sits in normal flow inside a button that the label
     itself sizes, so a percentage width would be circular. */
  container-type: inline-size;
  /* The ground moved to #panel-about: the constellation is now just the part of the
     panel's own field that has a ring in it, not a lit rectangle on top of it. The
     corner blocks are pinned by their nearest corner and grow inward (see the comment
     near their positions), the keyword halo sits well inside the ring's radii, and the
     ring canvas is inset: 0 -- nothing here can spill, so overflow: hidden is gone too. */
}
.aq-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }

/* ---- the four problem areas, inset from the corners ----
   These were four cards above the graph. Merged into it at the user's request, they
   are the graph's legend and its topology: every question is joined to its own area
   by an edge on the canvas, in that area's colour.

   They sat in the corners proper until the user asked for them "a bit closer to the
   center so that all content on the graph can be displayed in one page". They are 92px
   in off the sides now; how far in is solved in about.js, where the positions live.

   Below the breakpoint the graph is removed but these are NOT -- each area's sentence
   is authored only here, so hiding them would delete four sentences from the site. So
   the default layout is a plain stacked list, and only the wide-screen rule at the end
   of this block lifts them into the corners. One DOM source, two layouts. */
.aq-topics {
  display: grid;
  gap: 0.9rem;
  padding: 1.1rem;
}
.aq-topic { display: grid; gap: 0.3rem; }
/* The flip trigger. A real button, so Enter and Space arrive for free and there is no
   key handling to get wrong; everything below is undoing the UA's button styling, not
   adding any. It is the whole card in the wide layout (see the min-width rule), which is
   how a 44px target comes out of a 26px icon. */
.aq-topic-btn {
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  margin: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  text-align: inherit;
  min-height: 44px;
  display: grid;
  align-content: center;
  cursor: pointer;
}
.aq-topic-face { display: grid; gap: 0.3rem; }
.aq-topic-btn:focus-visible {
  outline: 2px solid var(--aq-topic, var(--map-ink));
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}
/* Collapsed, in the stacked layout the phone gets: a plain disclosure, so the sentence
   leaves the flow and leaves the accessibility tree with it. The wide layout overrides
   this, because a card cannot flip to a face that is display: none. */
.aq-topic[data-open="false"] .aq-topic-lead { display: none; }
.aq-topic[data-group="retrieval"]  { --aq-topic: var(--map-topic-retrieval); }
.aq-topic[data-group="agentic"]    { --aq-topic: var(--map-topic-agentic); }
.aq-topic[data-group="evaluation"] { --aq-topic: var(--map-topic-evaluation); }
.aq-topic[data-group="other"]      { --aq-topic: var(--map-topic-other); }
.aq-topic-ic {
  display: inline-grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 7px;
  /* The same token the area's questions and its edges use, so the tile, the name, the
     dots and the edge are one colour with one source. */
  background: color-mix(in srgb, var(--aq-topic) 16%, transparent);
  color: var(--aq-topic);
}
.aq-topic-ic .ic { width: 15px; height: 15px; }
.aq-topic-name {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 0.76rem;
  letter-spacing: 0.03em;
  color: var(--aq-topic);
}
/* The only affordance the card carries: it is deliberately not a plate or a chevron,
   since the point of the flip was "this will make the graph less noisy". The tile lifts
   and the sentence's ink warms on hover, and aria-expanded carries the same state to a
   reader who cannot see either. */
.aq-topic-btn:hover .aq-topic-ic { background: color-mix(in srgb, var(--aq-topic) 30%, transparent); }
.aq-topic-btn:hover .aq-topic-name { color: var(--map-ink); }
.aq-topic-lead {
  margin: 0;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--map-ink-dim);
  text-wrap: pretty;
}

/* ---- the keyword halo ----
   Each project's own authored tags, 2 or 3 of them, scattered near the centre. It
   duplicates the chips the Projects detail panel shows, so it stays out of the
   accessibility tree, and pointer-events: none so a word can never take a click
   meant for a question. */
.aq-halo { position: absolute; inset: 0; pointer-events: none; }
.aq-key {
  position: absolute;
  transform: translate(-50%, -50%);
  font-family: var(--font-mono);
  font-size: clamp(0.6rem, 0.95cqw, 0.76rem);
  letter-spacing: 0.06em;
  color: var(--map-key);
  white-space: nowrap;
  opacity: 0.9;
}

/* ---- the questions ---- */
/* pointer-events: none on the layer, auto on the buttons. The layer is inset: 0, so it
   covers the whole stage, and it is authored after .aq-topics -- which means it painted
   over the four area cards and swallowed every click aimed at them the moment they
   became controls. Measured before the fix: Playwright reported ".aq-nodes intercepts
   pointer events" on all four cards at every width. The eleven questions are unaffected;
   they are the buttons, not the layer. */
.aq-nodes { position: absolute; inset: 0; list-style: none; margin: 0; padding: 0; pointer-events: none; }
/* Moved by inline left/top only, never reordered. translateY centres the node on
   its ring point vertically while the dot stays at that point horizontally, so the
   curves the canvas draws arrive at the dot rather than at the middle of a label. */
.aq-node { position: absolute; transform: translateY(-50%); }
.aq-node[data-group="retrieval"]  { --aq-topic: var(--map-topic-retrieval); }
.aq-node[data-group="agentic"]    { --aq-topic: var(--map-topic-agentic); }
.aq-node[data-group="evaluation"] { --aq-topic: var(--map-topic-evaluation); }
.aq-node[data-group="other"]      { --aq-topic: var(--map-topic-other); }

.aq-btn {
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  margin: 0;
  /* The dot is 9px, but the control is at least 44px tall: the smallest target a
     finger can be expected to hit. The label makes it wider than that on its own. */
  min-height: 44px;
  /* The layer above is pointer-events: none, so each question takes its own clicks
     back. Without the pair, the node layer -- inset: 0 over the whole stage -- ate every
     click aimed at the four area cards behind it. */
  pointer-events: auto;
  padding: 0.25rem 0.4rem 0.25rem 0;
  display: flex;
  align-items: center;
  gap: 0.55rem;
  text-align: left;
  cursor: pointer;
  /* Was --map-ink-dim, which is the token for the map's secondary marks rather than
     for eleven strings a reader is meant to read. Mixed most of the way to full ink:
     the ring stays quieter than the centre without being faint. */
  color: color-mix(in srgb, var(--map-ink) 82%, var(--map-ink-dim));
  /* Serif, matching the reference. Georgia is a system face -- no font request is
     added, and the page already fails to fetch one web font as it is. */
  font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
  font-size: clamp(0.72rem, 1.02cqw, 0.92rem);
  /* 500, not the default 400. These sit over a starfield that several link curves
     cross, and at 400 in a serif at ~13px they measured as barely present -- "we can
     make the question in question cloud more visible". */
  font-weight: 500;
  line-height: 1.42;
}
/* Left of the label, on every node. */
.aq-dot {
  flex: none;
  width: 9px;
  height: 9px;
  border-radius: 999px;
  background: var(--aq-topic, var(--map-faint));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--aq-topic, var(--map-faint)) 24%, transparent);
}
.aq-label { display: block; width: clamp(9.5rem, 14.2cqw, 13rem); }
.aq-btn:hover { color: var(--map-ink); }
.aq-btn:focus-visible {
  outline: 2px solid var(--aq-topic, var(--map-ink));
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}

/* Every label reads left to right from its own dot, on both sides of the ring. The
   reference has the dot before the words on all of them, and mirroring the left five
   was tried while flattening the box to fit the fold: it frees the centre band, but it
   points those five labels straight into the left-hand corner blocks, and the sweep
   went from 0 failures to 46 -- retrieval~label9 and agentic~label2 at every selection.
   The corners and the ellipse's diagonals are the same place once the box is flat, so
   outward-radiating labels and corner blocks cannot both have that space. Rightward for
   all, which is what passes. */

/* ---- the centred question ----
   The only one set large, and the only one whose dot is dropped: the canvas paints
   a bright core and the nebula behind this point, and a dot under the words would
   be the one mark the text could collide with. */
.aq-node.is-current {
  transform: translate(-50%, -50%);
  z-index: 2;
}
.aq-node.is-current .aq-dot { display: none; }
.aq-node.is-current .aq-btn {
  /* It navigates now: this is the constellation's one route into Projects, since the
     separate solution link was removed. A satellite only re-centres, so the two must
     not read alike -- see the :hover/:focus-visible rule below for how they differ. */
  cursor: pointer;
  color: var(--map-ink);
  font-size: clamp(1rem, 1.9cqw, 1.6rem);
  /* Ahead of the ring on both axes -- size and weight -- so raising the satellites
     cannot flatten the hierarchy the field depends on. */
  font-weight: 600;
  line-height: 1.32;
  padding: 0.8rem 1rem;
  text-align: center;
  border-radius: 999px;
  /* A scrim, because the orbit rings and several link curves cross the middle of
     the box and would otherwise read straight through the words. It fades to
     nothing at its edge, so the field does not gain a visible plate. */
  background: radial-gradient(
    closest-side,
    color-mix(in srgb, var(--map-nebula) 88%, transparent),
    color-mix(in srgb, var(--map-nebula) 55%, transparent) 60%,
    transparent
  );
}
/* The centred question lights up rather than gaining an underline.

   An underline was tried first and rejected: a rule drawn under one line of a serif
   question, inside a field of stars and orbit curves, reads as a flaw in the type
   rather than as a route -- there is no other straight horizontal line anywhere in
   this composition for it to belong to.

   What this does instead is what the field already does to the point it is centred on:
   it brightens. The halo is the question's own area colour, the same --aq-topic the
   node's dot and its edge to the corner block use, so the cue says which area it
   belongs to at the same time as saying it is live. The scrim under the words comes up
   with it, which is what stops the glow reading as a blur.

   Nothing here moves the text. A translate would fight the canvas, which is drawing a
   glow and an orbit set positioned on this exact point -- the words would slide out of
   their own light. */
.aq-node.is-current .aq-btn:hover,
.aq-node.is-current .aq-btn:focus-visible {
  color: #fff;
  /* Three shadows, deliberately far past tasteful-and-invisible. A first pass at
     0.6em/1.6em was measured against the resting state in a screenshot and could not
     be told apart from it -- an affordance nobody can see is not an affordance. The
     tight one thickens the strokes so the type gains weight without a font-weight
     jump; the middle one is the glow; the wide one is the bloom that separates the
     words from the starfield. */
  text-shadow:
    0 0 0.28em color-mix(in srgb, var(--aq-topic, var(--map-ink)) 95%, transparent),
    0 0 0.9em color-mix(in srgb, var(--aq-topic, var(--map-ink)) 75%, transparent),
    0 0 2.4em color-mix(in srgb, var(--aq-topic, var(--map-ink)) 45%, transparent);
  /* The scrim lifts a little with the type, and no further. A stronger plate plus a
     rimmed box-shadow was tried and rejected on sight: at border-radius 999px around
     three or four wrapped lines it resolved into a lumpy pill, and the resting rule
     three declarations up exists precisely so "the field does not gain a visible
     plate". The glow above is what carries the state; this only keeps the words off
     the orbit curves while it does. */
  background: radial-gradient(
    closest-side,
    color-mix(in srgb, var(--map-nebula) 97%, transparent),
    color-mix(in srgb, var(--map-nebula) 72%, transparent) 60%,
    transparent
  );
}
/* The light theme's ground is pale, so a white-hot centre would vanish into it. There
   the question darkens to full ink and the halo does the work on its own. */
:root[data-theme="light"] .aq-node.is-current .aq-btn:hover,
:root[data-theme="light"] .aq-node.is-current .aq-btn:focus-visible {
  color: var(--map-ink);
}
.aq-node.is-current .aq-label { width: clamp(11rem, 23cqw, 22rem); }

/* The constellation is decoration and it links to nothing, so below the width at
   which eleven labels can be read it is removed rather than restacked -- every
   question it holds is on the Projects panel in full. about.js gates on the same
   1200px, so under it no 2d context is created and no animation frame is ever
   requested: an invisible animating canvas is battery drain. display:none, so the
   eleven buttons leave the tab order too. */
/* Wide enough for the field: the ring, the eleven labels and the four area blocks. Held
   at 1200 and re-measured for the flat box rather than assumed -- see MAP_QUERY in
   about.js for the clearance at each width. Above 1200 every part of this composition is
   sized in cqw, so it scales as one piece and one solved geometry holds at any width;
   1200 is where the type's rem floors take over and that stops being true.

   This is the only place the inset layout is declared, which is what makes the stacked
   list the fallback rather than an override: below this width the rule simply does not
   apply and .aq-topics is the grid it is by default. */
@media (min-width: 1200px) {
  .aq-topics {
    position: absolute;
    inset: 0;
    display: block;
    padding: 0;
    /* Decoration-free container: it only positions. Clicks pass through to the canvas
       and the questions; the four flip buttons inside take their own pointer events
       back, and nothing else in here takes any. */
    pointer-events: none;
  }
  /* ---- the flip ----
     One fixed box per area, holding both faces: icon plus name on the front, the
     sentence on the back. FIXED is the whole safety argument -- the footprint is
     identical open and closed, so no state of these cards can produce a collision the
     sweep has not already measured. The box is also SHORTER than the block it replaces
     (38px against 67px at a 1440px window), because it no longer has to stack a name
     and a sentence; a smaller footprint can only add clearance.

     The button does NOT rotate. Only its inner .aq-topic-face does, and the sentence
     rotates opposite it about the same axis, so the pair reads as two sides of one
     card while the control itself stays flat, front-facing and hit-testable at every
     angle -- which is what keeps its focus ring visible and rectangular. A rotated
     button would take its focus ring with it and, backface hidden, would stop being
     clickable at exactly the moment the reader wanted to flip back. */
  .aq-topic {
    position: absolute;
    /* --aq-x/--aq-y come from about.js, which is also what hands the canvas the hub
       points, so the block and the edge that reaches it share one source of position. */
    /* 21.9cqw is 300px at a 1440px window. Wider is SHORTER for the sentence face: at
       205 three of the four sentences wrapped to three lines, at 300 all four take
       two. */
    width: clamp(15rem, 21.9cqw, 23rem);
    /* Tall enough for the taller of the two faces at every width, which is the sentence.
       Measured, the sentence needs 38px at 1440 (two lines at 13px) and 55px at 1200,
       where the block is 249px wide and the longest of them takes three lines. The floor
       covers that case, so the box is 3.5rem up to a 2038px container and grows with the
       box above it. Either way it is under the 55-67px the always-open block occupied, at
       every width -- see the sweep. */
    height: clamp(3.5rem, 2.75cqw, 4.4rem);
    perspective: 900px;
    display: block;
  }
  /* The card is the target: the whole fixed box, so 44px comes for free. */
  .aq-topic-btn {
    position: absolute;
    inset: 0;
    display: block;
    min-height: 0;
    perspective: 900px;
    /* .aq-topics is pointer-events: none so the field behind it stays clickable; the
       four controls inside it are the exception. */
    pointer-events: auto;
  }
  .aq-topic-face {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    gap: 0.33cqw;
    backface-visibility: hidden;
  }
  .aq-topic-lead {
    position: absolute;
    inset: 0;
    display: grid;
    align-content: center;
    /* Clicks fall through to the button underneath, which is the whole box: with the
       sentence showing, clicking it is what flips the card back. */
    pointer-events: none;
    backface-visibility: hidden;
  }
  /* Closed: the sentence is turned away rather than removed, or it could not flip back
     into view. about.js is what takes it out of the accessibility tree while it is
     turned away, and puts it back when it faces the reader. */
  .aq-topic[data-open="false"] .aq-topic-lead { display: grid; transform: rotateY(-180deg); }
  /* Open: the name face turns away instead. Keyed on :not([data-open="false"]) rather
     than on [data-open="true"], so markup with no attribute at all -- which is what a
     visitor sees if about.js never runs -- shows the sentence. */
  .aq-topic:not([data-open="false"]) .aq-topic-face { transform: rotateY(180deg); }
  /* On the right-hand pair the icon goes after the name, so both blocks read outward
     from the field's centre. */
  .aq-topic[data-corner$="-right"] .aq-topic-face { flex-direction: row-reverse; }
  /* Everything inside the block is sized in cqw here, so the block scales with the box
     exactly as the question labels do. That is what makes one measured geometry hold at
     every width: with a fixed 12.5px sentence the block kept its height while the box
     shrank, and at 1200px it was 22 percent of the height instead of 14.

     The floors are placed where the graph stops being shown (a 1128px container at the
     1200px breakpoint), so inside this media query the sizes are purely proportional;
     the caps engage above a 1582px container, where the type would otherwise outgrow the
     body text, and a capped block is a shorter block, so it only ever gains clearance. */
  .aq-topic-ic {
    width: clamp(1.3rem, 1.9cqw, 1.75rem);
    height: clamp(1.3rem, 1.9cqw, 1.75rem);
  }
  .aq-topic-ic .ic {
    width: clamp(0.75rem, 1.1cqw, 1rem);
    height: clamp(0.75rem, 1.1cqw, 1rem);
  }
  .aq-topic-name { font-size: clamp(0.72rem, 0.89cqw, 0.9rem); }
  /* 12.48px at the floor, 13px at 1440, capped at 15.04px -- up from 10.88/12.43, and the
     reason the flip is worth having beyond the noise. The old size was not a choice about
     reading, it was the largest that fitted while all four sentences were on screen at
     once beside eleven labels: a floor of 11.04px used to rewrap the longest of them to
     three lines in a 249px block and put it 8.3px through a question label. Hidden by
     default and flipping inside a fixed box, the rewrap costs nothing -- the box is sized
     for the three-line case at 1200 and never changes -- so the sentence can be set to
     read. */
  .aq-topic-lead { font-size: clamp(0.78rem, 0.95cqw, 0.94rem); }
  /* Pinned by its nearest corner, so a block grows inward and can never grow off the
     box however the sentence wraps. */
  .aq-topic[data-corner="top-left"]     { left: var(--aq-x); top: var(--aq-y); }
  .aq-topic[data-corner="top-right"]    { right: calc(100% - var(--aq-x)); top: var(--aq-y); }
  .aq-topic[data-corner="bottom-left"]  { left: var(--aq-x); bottom: calc(100% - var(--aq-y)); }
  .aq-topic[data-corner="bottom-right"] { right: calc(100% - var(--aq-x)); bottom: calc(100% - var(--aq-y)); }
  .aq-topic[data-corner$="-right"] { text-align: right; justify-items: end; }
}

/* The constellation is decoration and it links to nothing, so below the width at which
   eleven labels can be read it is removed rather than restacked -- every question it
   holds is on the Projects panel in full. about.js gates on the same 1200px, so under
   it no 2d context is created and no animation frame is ever requested.

   The four problem areas used to be the exception here and stay visible below this
   width in the sense that they are never deleted -- but as of the phone's topic
   cards (see .topic-cards below) their name and sentence render there instead, read
   out of .aq-topic-lead rather than duplicated. So .aq-topics is hidden here too now:
   leaving it on screen at the same width as the cards that read its own copy would
   put every area's sentence in front of the reader twice. The panel itself carries
   the ground now, so the block still reads as one section without the stage's own
   box; the stage only drops the aspect ratio it needed for the ring. */
/* ---- the phone's topic cards ----
   Below 1200px the ring above is hidden -- eleven labels cannot be read in a narrow
   viewport -- and .aq-topics is hidden with it now that these cards carry the same
   four areas. Hidden by default and shown only below the breakpoint, so the desktop
   never renders both treatments at once: exactly one of the ring's corner nodes or
   this deck exists at any width. topiccards.js removes [hidden] once it builds the
   four cards, so a browser with no JavaScript sees none of this and none of the
   corner nodes either -- the trade this phone treatment makes for cards built as
   real controls rather than a static, always-present list.

   Swipe is scroll-snap, not JavaScript: the browser's own scrolling carries momentum
   and rubber-banding for free, and keeps moving even if topiccards.js throws after
   the cards are in the DOM. */
.topic-cards { display: none; }

@media (max-width: 1199px) {
  .aq-canvas,
  .aq-halo,
  .aq-nodes,
  /* Hidden only once the cards have actually been built. The class is set by
     topiccards.js after it appends them, not by this media query alone -- with JS off,
     or if the module throws, the stacked .aq-topics list is the whole of the four areas
     on a narrow screen, and hiding it unconditionally would delete four sentences from
     the phone rather than replace them. Progressive enhancement: the fallback goes away
     when, and only when, something better exists. */
  .about-qmap[data-cards="ready"] .aq-topics { display: none; }

  /* The instruction and its control describe the ring: "bring it to the centre, then
     click the centred one". There is no centre below this width -- the cards are a
     swipeable row with arrows, dots and a count of their own -- so the control would
     offer to explain an interaction that is not on screen. The invitation stays; only
     the how goes, because the cards already answer it. */
  .aq-how,
  .aq-instruction { display: none; }
  .aq-stage { aspect-ratio: auto; container-type: normal; }

  .topic-cards:not([hidden]) { display: block; margin: 0 0 1.6rem; }

  .tc-track {
    display: flex;
    gap: 0.9rem;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    /* No visible scrollbar, but the element still scrolls -- and still answers the
       keyboard, which is why it is focusable below. */
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    padding: 0.2rem 0.1rem;
  }
  .tc-track::-webkit-scrollbar { display: none; }
  .tc-track:focus-visible {
    outline: 2px solid var(--map-ink);
    outline-offset: 3px;
    border-radius: var(--radius);
  }

  /* 87%, so the next card's edge peeks past the viewport -- visible proof that more
     than one exists, which is the whole point of a deck over a single static block. */
  .tc-card {
    scroll-snap-align: start;
    flex: 0 0 87%;
    display: flex;
    flex-direction: column;
    gap: 0.7rem;
    padding: 1.1rem 1.2rem;
    border-radius: var(--radius);
    border: 1px solid color-mix(in srgb, var(--aq-topic, var(--map-faint)) 30%, transparent);
    /* The card's own colour comes from its area, the same --aq-topic the ring's dot
       and edge use for it, so a card says which area it belongs to the same way. */
    background: color-mix(in srgb, var(--map-nebula) 45%, transparent);
  }
  .tc-card[data-group="retrieval"]  { --aq-topic: var(--map-topic-retrieval); }
  .tc-card[data-group="agentic"]    { --aq-topic: var(--map-topic-agentic); }
  .tc-card[data-group="evaluation"] { --aq-topic: var(--map-topic-evaluation); }
  .tc-card[data-group="other"]      { --aq-topic: var(--map-topic-other); }

  .tc-name {
    margin: 0;
    font-family: var(--font-mono);
    font-size: 0.8rem;
    letter-spacing: 0.03em;
    color: var(--aq-topic, var(--map-ink));
  }
  .tc-lead {
    margin: 0;
    font-size: 0.92rem;
    line-height: 1.5;
    color: var(--map-ink-dim);
  }
  .tc-questions { display: grid; gap: 0.55rem; margin: 0; }
  .tc-q {
    display: grid;
    align-items: center;
    min-height: 44px;
    text-align: left;
    appearance: none;
    -webkit-appearance: none;
    cursor: pointer;
    border: 0;
    border-left: 2px solid color-mix(in srgb, var(--aq-topic, var(--map-faint)) 55%, transparent);
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    background: color-mix(in srgb, var(--aq-topic, var(--map-faint)) 10%, transparent);
    padding: 0.5rem 0.7rem;
    color: var(--map-ink);
    font-family: var(--font-body);
    font-size: 0.88rem;
    line-height: 1.4;
  }
  .tc-q:focus-visible { outline: 2px solid var(--aq-topic, var(--map-ink)); outline-offset: 2px; }

  .tc-more {
    justify-self: start;
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 0.9rem;
    margin-top: 0.1rem;
    border-radius: 999px;
    border: 1px solid color-mix(in srgb, var(--aq-topic, var(--map-ink-dim)) 45%, transparent);
    color: var(--aq-topic, var(--map-ink));
    text-decoration: none;
    font-family: var(--font-mono);
    font-size: 0.78rem;
  }
  .tc-more:focus-visible { outline: 2px solid var(--aq-topic, var(--map-ink)); outline-offset: 2px; }

  .tc-nav { display: flex; align-items: center; justify-content: center; gap: 1rem; margin-top: 0.8rem; }
  .tc-count { font-family: var(--font-mono); font-size: 0.8rem; color: var(--muted); margin: 0; min-width: 4.5em; text-align: center; }
  .tc-arrow {
    min-width: 44px;
    min-height: 44px;
    border-radius: 999px;
    border: 1px solid color-mix(in srgb, var(--map-ink-dim) 35%, transparent);
    background: none;
    color: var(--map-ink);
    cursor: pointer;
    font-size: 1rem;
  }
  .tc-arrow:disabled { opacity: 0.35; cursor: default; }
  .tc-arrow:focus-visible { outline: 2px solid var(--map-ink); outline-offset: 3px; }

  .tc-dots { display: flex; gap: 0.35rem; justify-content: center; margin: 0.7rem 0 0; }
  .tc-dots span {
    width: 6px;
    height: 6px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--map-ink-dim) 40%, transparent);
  }
  .tc-dots span.is-on { background: var(--map-ink); }
}

@media (prefers-reduced-motion: reduce) {
  .tc-track { scroll-behavior: auto; }
}

.about-hero { margin-bottom: 3.2rem; }
/* Quiet on purpose: the greeting sets the tone, the name carries the weight. */
.hero-greet {
  margin: 0 0 0.2rem;
  font-family: var(--font-mono);
  font-size: 0.9rem;
  letter-spacing: 0.06em;
  color: var(--accent-2);
}
.hero-name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(2.6rem, 8vw, 4.6rem);
  line-height: 1.02;
  letter-spacing: -0.03em;
  margin: 0.1rem 0 0.9rem;
  background: var(--accent-grad);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.hero-tagline {
  /* font-family/font-weight kept from the previous rule: the tagline has always
     been display-face at 500, and dropping them would have handed it the body
     face at 400 as a side effect of resizing it. */
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(1.15rem, 2.6vw, 1.6rem);
  line-height: 1.35;
  color: var(--text);
  margin: 0 0 1.4rem;
  max-width: 34ch;
}

.invite-em {
  /* 300% wide so there is somewhere for the gradient to travel to; at 100% the
     animation would have no distance to cover and the words would just cycle hue. */
  background: linear-gradient(
    100deg,
    var(--topic-retrieval),
    var(--topic-agentic) 30%,
    var(--topic-other) 55%,
    var(--topic-agentic) 80%,
    var(--topic-retrieval)
  );
  background-size: 300% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* The .hero-tags chips and their li rule were deleted here: the four focus-area
   chips are gone from the markup, so both rules matched nothing. Checked the whole
   file and both panels first -- .hero-tags appeared only in that one <ul>, unlike
   .proj-seg, which reads dead and is not. */

/* One heading treatment for all five sections, replacing the small mono eyebrow. */
.about-h {
  font-size: clamp(1.35rem, 3vw, 1.75rem);
  letter-spacing: -0.015em;
  margin: 0 0 1.4rem;
  padding-bottom: 0.7rem;
  border-bottom: 1px solid var(--border);
}
/* The heading labels the constellation that follows it, so it sits close to it.
   Was 3.2rem here plus 1.4rem under .about-h -- 74px of gap, measured -- which was
   tuned when this section held four cards of its own and the graph was a separate
   block below. With the cards merged into the graph the heading has nothing else to
   be spaced from, and 74px left it floating between two sections instead of
   introducing one. 16px total now, and the underline does the separating. */
.about-work { margin-bottom: 0; }
.about-work .about-h { margin-bottom: 1rem; }

@media (max-width: 760px) {
  /* Below this the hero needs the full width, so the icons wrap under it. */
  .about-top { flex-direction: column; }
  .about-contact { order: 2; }
  /* Flipping the axis turns the hero's 28rem flex-basis into a *height*, which
     reserved 448px for ~350px of copy and opened a 215px hole above the contact
     icons. In a column the hero should just be as tall as its content. */
  .about-top .about-hero { flex: 0 0 auto; }
  /* 3.2rem was tuned as desktop breathing room between the hero and the first
     section; on a phone it reads as another gap. In a column the contact icons
     become the last thing in this block, so the separation from "What I work
     on" moves onto the block itself and matches the 51px section rhythm. */
  .about-hero { margin-bottom: 0.75rem; }
  .about-top { margin-bottom: 3rem; }
}

/* =========================================================================
   MOTION — every rule below is inside prefers-reduced-motion: no-preference,
   so a reader who has asked for less movement gets none of it.
   ========================================================================= */
@media (prefers-reduced-motion: no-preference) {
  /* Which area is lit changes on a click, so it transitions rather than animates -- and
     briefly, since the field has already moved the questions by the time it settles.
     Folded into this block rather than opening a second no-preference at-rule: a test
     asserts there is exactly one, because a duplicate earlier in the file silently
     becomes the block that other tests inspect. */
  .aq-topic { transition: background 0.35s ease-out; }
  .aq-topic .aq-topic-name { transition: text-shadow 0.35s ease-out; }
  .aq-topic .aq-topic-ic { transition: background 0.35s ease-out, box-shadow 0.35s ease-out; }

  @keyframes invite-drift {
    from { background-position: 0% 50%; }
    to { background-position: 200% 50%; }
  }
  /* Declared only in here, which is the whole reduced-motion story: with this block
     inactive the gradient is simply static and the words keep every bit of their colour.
     Nothing is lost by switching motion off -- only the travel.

     Folded into this block rather than opening a second
     @media (prefers-reduced-motion: no-preference): a test locates "the" no-preference
     block by its first occurrence, so a duplicate at-rule earlier in the file silently
     became the one it inspected. One block per media query avoids that whole class of
     fragility. */
  .invite-em { animation: invite-drift 9s linear infinite; }

  /* The glyph tile animates only where something moves it; the card entrance
     stagger and the card hover-lift left with the grid. */
  .proj-ic { transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1), border-color 0.25s, background 0.25s; }
  /* Filter pills settle rather than snap. */
  .proj-seg { transition: color 0.18s, background 0.18s, transform 0.18s; }
  .proj-seg:active { transform: scale(0.97); }

  /* The three areas arrive in a short stagger so the section reads as one idea
     unfolding rather than three boxes appearing at once. --i is set per card in
     the markup, since there are exactly three and they never reorder. */
  .domain-card { animation: work-card-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) backwards; animation-delay: calc(var(--i, 0) * 90ms); }
  @keyframes work-card-in {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: none; }
  }
  .domain-card { transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1), border-color 0.22s, box-shadow 0.22s; }
  .domain-ic { transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1); }
  /* Deliberately not extended to .domain-card:hover: the Projects card is a
     section, not a control, and glyph motion under the pointer would promise a
     click it does not answer. The entrance stagger above is the motion the two
     card sets share; the hover treatments stay with the cards that are links.
     The transform transition on .domain-card is still wanted -- .is-chosen
     raises its own shadow, and that should settle rather than snap. */

  /* Re-centring is the constellation's only motion: all eleven nodes ease to their
     new slots at once, behind a per-node delay about.js sets from the slot index,
     so the field sweeps round rather than swinging as one rigid body.

     Declared only in here, which is the whole reduced-motion story: with this block
     inactive there is no transition and no delay on any of it, so the nodes are
     simply already in their new places. A jump, not a slide -- and qmap.js draws a
     single frame and starts no loop, so nothing on the canvas moves either. */
  .aq-node {
    transition:
      left 0.55s cubic-bezier(0.22, 1, 0.36, 1) var(--aq-delay, 0ms),
      top 0.55s cubic-bezier(0.22, 1, 0.36, 1) var(--aq-delay, 0ms);
  }
  /* text-shadow and background carry the centred question's hover glow, so it swells
     in rather than snapping on. Both are inside this block, which means reduced motion
     gets the lit state instantly and completely -- the cue is never lost, only its
     travel. */
  .aq-btn {
    transition:
      color 0.2s,
      text-shadow 0.28s ease-out,
      background 0.28s ease-out,
      box-shadow 0.28s ease-out,
      font-size 0.4s cubic-bezier(0.22, 1, 0.36, 1);
  }
  /* Once lit, the centre breathes -- the same slow swell qmap.js gives the glow it
     paints behind this point, so the markup layer and the canvas layer pulse as one
     object rather than two. Opacity only: nothing here may move the words, because the
     canvas is drawing that glow and the orbit set on this exact coordinate and the
     text would slide out of its own light. */
  @keyframes aq-breathe {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.82; }
  }
  .aq-node.is-current .aq-btn:hover,
  .aq-node.is-current .aq-btn:focus-visible {
    animation: aq-breathe 2.4s ease-in-out infinite;
  }
  .aq-dot { transition: transform 0.2s, box-shadow 0.25s; }
  .aq-btn:hover .aq-dot { transform: scale(1.3); }
  /* The old set of keywords leaves before the new set arrives; two vocabularies
     crossfading over each other is unreadable. about.js holds the gap. */
  .aq-key { transition: opacity 0.3s; }
  .aq-key.is-going { opacity: 0; }
  /* The area cards' flip, and the ONLY thing that makes it a flip rather than a swap:
     the rotations themselves are in the base rules, so a reader who has asked for less
     movement gets the two faces exchanged instantly, with no rotation to watch and no
     frame scheduled. */
  .aq-topic-face,
  .aq-topic-lead { transition: transform 0.55s cubic-bezier(0.22, 1, 0.36, 1); }
  .aq-topic-ic { transition: background 0.2s; }
}

/* =========================================================================
   MOBILE — everything below is phone-only polish. Three classes of problem
   were measured on a 390x664 viewport before these rules existed:

   1. The header wrapped to three rows and ate 160px (24%) of the screen,
      while --head-h still claimed 66px, so every box sized from that token
      was 94px out.
   2. Controls were sized for a mouse: 30-38px tall against the 44px minimum
      a finger needs (WCAG 2.5.8 / the iOS and Material guidance).
   3. Hover states have nowhere to go on a touch screen: iOS leaves the last
      tapped element in :hover, so a card stayed lifted after being tapped.
   ========================================================================= */

/* Hover-only affordances, gated on a device that actually has a pointer. */
@media (hover: none) {
  .chat-launcher:hover { transform: none; box-shadow: 0 8px 24px -10px var(--accent-1); }
  .about-contact a:hover { transform: none; }
  #send-btn:hover:not(:disabled) { transform: none; }
}

@media (max-width: 1020px) {
  /* Same breakpoint as the header wrap above, deliberately: this rule exists
     because the header is `height: auto` there, so --head-h no longer describes
     it and the stage's viewport-height reservation just adds a phantom scroll.
     If one breakpoint moves, both move. */
  .stage { min-height: 0; }
}

@media (max-width: 560px) {
  /* --- Header: three rows down to two --- */
  /* Dropping the name from the wordmark frees ~90px, which is the difference
     between the actions wrapping onto their own row and sitting beside it. The
     mark stays, so the site is still identified, and the <h1> on About carries
     the full name two lines further down. */
  .wordmark-name { display: none; }
  .site-head { gap: 0.5rem 0.75rem; padding: 0.5rem 0.9rem; }

  /* Tighter tabs so all six fit one row: measured 0px hidden at 360, 375, 390,
     412, 430 and 560. At 320px about 26px is still hidden — the bar scrolls to
     reach it and the page itself never scrolls sideways, so nothing is lost. */
  .tabs { padding: 0.2rem; gap: 0.1rem; }
  .tab {
    font-size: 0.84rem;
    padding: 0.5rem 0.42rem;
    min-height: 40px;
  }
  .tab-full { display: none; }
  .tab-abbr { display: inline; }

  /* --- Touch targets --- */
  .btn-ghost, .chat-launcher, .starter-chip { min-height: 44px; }
  .icon-btn { min-width: 44px; min-height: 44px; }
  /* Resource links are 18px of text; padding and a min-height are what make
     them tappable without changing how they read. */
  .work-resource {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }
  .proj-seg { min-height: 40px; }
  .about-sec-more { min-height: 40px; display: inline-flex; align-items: center; }


  /* --- Inputs --- */
  /* Under 16px, iOS Safari zooms the page in when the field takes focus and
     leaves it zoomed, which is half of why typing here felt broken. */
  #chat-input,
  .field input,
  .field textarea { font-size: 16px; }
}

/* Held sideways, the cutout is on the long edge, so the 1rem gutter is not
   enough to clear it. Scoped to small landscape screens so the desktop gutters
   keep their clamp() sizing. */
@media (max-width: 900px) and (orientation: landscape) {
  .site-head,
  .stage {
    padding-left: max(1rem, env(safe-area-inset-left));
    padding-right: max(1rem, env(safe-area-inset-right));
  }
}

/* =========================================================================
   ABOUT — landing sections. Added on top of the condensed About page; the
   hero and "what I work on" blocks above are unchanged.
   ========================================================================= */

/* Section heading with an optional "see all" on the same baseline. */
.about-sec-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
/* The work section's head is its heading and its link onward -- the focus-area chips
   that used to sit between them are deleted. align-items: center is kept from that
   arrangement: with two items of very different type sizes (a 28px heading, a 12px mono
   link) centring reads level, where a shared baseline sits the link on the heading's
   and leaves it looking dropped. The row, not the heading, carries the rule and the
   gap to the graph below. */
.about-work .about-sec-head { align-items: center; }
.about-work .about-sec-head .about-h { margin-bottom: 0; }
/* The underline moves to the row, so it still separates the section from the graph
   while spanning the chips too. */
.about-work .about-sec-head {
  padding-bottom: 0.6rem;
  border-bottom: 1px solid var(--border);
  margin-bottom: 1rem;
}
.about-work .about-sec-head .about-h { padding-bottom: 0; border-bottom: 0; }
.about-sec-more {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: var(--accent-1);
  text-decoration: none;
  white-space: nowrap;
}
.about-sec-more:hover { text-decoration: underline; }
/* This is the only route from About into Projects now that the per-card "Explore
   more" links are gone with the example questions, so it has to be findable by
   keyboard. It had no focus style at all and fell back to the UA's, which the rest
   of this file overrides everywhere else. */
.about-sec-more:focus-visible { outline: 2px solid var(--accent-1); outline-offset: 3px; border-radius: 4px; }

/* ---------- About: journey ---------- */
/* Horizontal on a wide screen, one column on a phone. Deliberately lighter than
   the Experience timeline: this is a summary, that is the record. */
.jrn {
  list-style: none;
  margin: 1.2rem 0 1rem;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 1.1rem;
}
.jrn-step {
  padding-top: 0.9rem;
  /* Dashed and drawn from the map's tokens, so the timeline reads as one more thread in
     the same sky rather than as a rule on a page. It stays a real CSS border rather than
     moving to the canvas: the timeline is content, and content must not depend on a 2d
     context being available. */
  border-top: 1px dashed color-mix(in srgb, var(--map-line) 55%, transparent);
  position: relative;
}
.jrn-step::before {
  content: "";
  position: absolute;
  top: -5px; left: 0;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--map-ink-dim) 70%, transparent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--map-faint) 22%, transparent);
}
.jrn-step.is-now { border-top-color: var(--accent-1); }
.jrn-step.is-now::before { background: var(--accent-1); box-shadow: 0 0 0 3px var(--accent-soft); }
.jrn-year { font-family: var(--font-mono); font-size: 0.76rem; color: var(--faint); margin: 0; }
/* ---------- Company logo slot ----------
   Beside every company name, in all three places one appears: the About journey,
   the Experience timeline rows and the Experience detail head. One rule for all
   three; the box is sized in em so it tracks whichever type it sits in.

   No logo files are committed and none are drawn here -- a hand-authored Meta or
   Microsoft mark would be a fabricated trademark. What renders today is the
   monogram fallback, so it has to look deliberate rather than broken: a rounded
   tile in the theme's own --elevated / --border / --muted tokens, with no brand
   colour and no brand shape anywhere in it. logos.js swaps in <slug>.svg for any
   company whose file exists.

   vertical-align: middle and line-height: 1 keep the slot inside the line box it
   joins, so adding it does not grow a row: measured, all five journey steps, all
   eight timeline rows and the detail head are unchanged to the pixel. */
.co-logo {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  /* A fixed box, not an em size: all eight marks have to align as one set, and the
     three places a company appears set three different type sizes.

     21px is the largest that leaves every row height untouched. Measured: the About
     journey step is 113.938px with a 21px chip and 113.938px with none, against
     113.984px at 22px -- at 22 the chip's bottom edge lands exactly on the line
     box's and pushes .jrn-co from 25.088 to 25.141. Sub-pixel, but the brief asked
     for nothing to reflow, and 21 costs nothing. */
  width: 21px;
  height: 21px;
  margin-right: 0.42em;
  vertical-align: middle;
  line-height: 1;
  border-radius: 5px;
  overflow: hidden;
  border: 1px solid var(--border);
  background: var(--elevated);
  /* The detail head paints its company name with a gradient clipped to the text
     and a transparent colour. Without these three resets the monogram inside it
     would be clipped away too -- invisible, with nothing to show it had gone. */
  -webkit-background-clip: border-box;
  background-clip: border-box;
  color: var(--muted);
}
.co-logo-fb {
  font-family: var(--font-mono);
  font-size: 0.52em;
  font-weight: 500;
  letter-spacing: 0.02em;
  line-height: 1;
  color: var(--muted);
  /* Same reason as above: inside the gradient-clipped company line this text
     would otherwise inherit a transparent colour. */
  -webkit-text-fill-color: var(--muted);
}
/* A real asset turns the tile into the light chip and the monogram steps aside.
   object-fit: contain, so a wide mark keeps its proportions -- alibaba is 64x41 and
   meta 64x36 after downscaling, and stretching either to a square would deform a
   trademark. The padding keeps a full-bleed mark off the chip's rounded corners. */
.co-logo img { width: 100%; height: 100%; object-fit: contain; display: block; }
.co-logo.has-logo {
  background: var(--logo-chip);
  border-color: var(--logo-chip-edge);
  padding: 2px;
  /* The hairline is what separates a near-white chip from a near-white panel: on the
     light theme the chip's own ground measures 1.01:1 against the page, so the ground
     cannot do it and no near-white value could. The border over that ground measures
     1.98:1, and the soft shadow under it adds the depth cue a single hairline lacks.
     On the dark theme none of this is load-bearing -- the chip is already 14.57:1
     against the panel -- so the shadow is kept faint enough not to read as a halo
     there. */
  box-shadow: 0 1px 2px rgba(11, 16, 32, 0.16);
}
.co-logo.has-logo .co-logo-fb { display: none; }
/* The monogram keeps the neutral --elevated tile instead: it is text, and --muted
   ink on a near-white chip would fail contrast in the dark theme. Two treatments,
   each suited to what it holds -- a placeholder should not pretend to be a logo. */

/* The timeline row is the one place with no vertical slack: .exp-node-name sets
   0.94rem in an 18px line box, and a 1.15em inline tile grew every row by 0.8px.
   Taking the slot out of flow there keeps the tile the same size as the other two
   and the row height exactly what it was -- padding-left reserves the space, and
   padding does not affect a line box. Measured: rows back to 73.5/89.5 to the
   pixel. Scoped here rather than applied everywhere, because the journey step and
   the detail head have room and read better with the slot inline. */
.exp-node-name { position: relative; padding-left: 27px; }
.exp-node-name .co-logo {
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  margin-right: 0;
}

.jrn-co { font-weight: 600; font-size: 0.98rem; margin: 0.2rem 0 0.3rem; }
/* Holds the role title since R1b, read from the authored data-role. */
.jrn-role { color: var(--muted); font-size: 0.88rem; line-height: 1.6; margin: 0; }

/* The journey steps 5 -> 3 -> 2 -> 1. Explicit counts, not auto-fit: there are
   exactly five, and auto-fit's natural progression puts four on one row and
   orphans the fifth -- measured 4+1 from 820 to 1000px. The by-the-numbers grid
   shared these three breakpoints until R1b removed that section. */
@media (max-width: 1080px) {
  .jrn { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 700px) {
  .jrn { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 460px) {
  .jrn { grid-template-columns: minmax(0, 1fr); }
}

/* The closing CTA block is removed at the user's request. Its three rules went with
   it -- the section wrapper, the copy wrapper and the tagline -- each having had
   exactly one consumer, that block. Two classes that looked equally dead were kept
   after checking: the section-heading class still has three other users, and the
   primary-button class is what the contact form's submit button carries, so deleting
   that one as "dead" would have broken the contact panel. Class names are described
   here rather than written, because a test scans this file for selectors that should
   no longer exist. */

/* One line plain, one carrying the accent ramp — the reference mockup's idea at
   a size that still wraps to two lines on a phone rather than four. Built from
   the shared tokens so it cannot drift from the wordmark or the figures. */
.hero-em {
  background: linear-gradient(100deg, var(--topic-retrieval), var(--topic-agentic) 55%, var(--topic-other));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* Focal cards lift into their own topic hue. Behind (hover: hover) because iOS
   leaves the last tapped element in :hover, which would strand a tapped card in
   the lifted state. Retargeted in R1b: the featured and hub cards this was
   written for are gone, and the three work areas are now the focal cards. */
@media (hover: hover) {
  /* The problem card itself no longer responds to hover: it is a section, and a
     border that lights up would promise a click that does nothing. The questions
     inside it are the controls, so the feedback belongs there -- colour only, no
     transform, so a hovered question does not shift the card's other lines. */
  .work-sibling:hover { color: var(--text); background: color-mix(in srgb, var(--topic, var(--accent-1)) 8%, transparent); }
  /* The question row's hover tint is ungated and lives with the shared rule --
     colour alone is safe on a touch screen. What is gated is the nudge, and it is
     shared too, so a question moves the same way on both panels. The 8% override
     that used to sit here is gone: it came after the shared 14% and quietly won,
     which made the Projects row a paler thing than the About row it is meant to
     match. */
  .wq-link,
  .dq { transition: color 0.18s, background 0.18s, border-color 0.18s, transform 0.18s; }
  .wq-link::after,
  .dq-chev { transition: opacity 0.18s; }
  .wq-link:hover,
  .dq:hover { transform: translateX(2px); }
  .dq:hover .dq-mark { border-color: var(--topic, var(--accent-1)); }
}

/* A second, slower layer beneath the canvas -- z-index: -1 is what puts it
   there, since an absolutely-positioned pseudo-element otherwise paints after
   the canvas's in-flow content. 40s so it never draws attention; the global
   reduced-motion rule collapses animation-duration, parking it at one frame. */
.graph-bg::before {
  content: "";
  position: absolute;
  inset: -20%;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(40% 40% at 20% 20%, color-mix(in srgb, var(--accent-2) 18%, transparent), transparent 70%),
    radial-gradient(35% 35% at 80% 30%, color-mix(in srgb, var(--accent-1) 14%, transparent), transparent 70%);
  animation: wash 40s ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes wash {
  from { transform: translate3d(-3%, -2%, 0) scale(1); }
  to   { transform: translate3d(3%, 2%, 0) scale(1.08); }
}

/* =========================================================================
   WORK — four problem cards carrying their own questions, then one full-width
   detail panel below them. Replaces the list column that used to sit beside the
   detail: the questions are the list now, and they live where the problem is
   described. Treatment is typography and colour only — no diagram, no media
   slot, nothing that could read as a placeholder.
   ========================================================================= */
.work-step {
  font-family: var(--font-mono);
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--faint);
  margin: clamp(1.6rem, 4vw, 2.4rem) 0 0.7rem;
}
/* One panel, full width: the list column it used to share this row with is gone.
   Kept as a wrapper rather than deleted, since the step label above it and the
   aria-live region inside it are addressed as a unit. */
.work-body { display: block; }
.work-detail {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  padding: clamp(1.2rem, 3vw, 2rem);
  /* Was 420px to hold the height of the list beside it; at full width the panel
     is taller than that anyway, and a floor would only pad a short project. */
  min-height: 0;
  /* Selecting a question brings this panel up. The header is sticky, so without
     this the answer's heading would arrive underneath it. Declared here rather
     than as an offset computed in JS, so the one number the layout already has
     -- --head-h -- is the one that positions it. */
  scroll-margin-top: calc(var(--head-h) + 0.9rem);
}
/* The heading is the focus target for that jump, and it takes tabindex="-1" to be
   one. A ring on :focus alone would flash on every mouse click; :focus-visible is
   what a keyboard selection gets, and it should be visible -- that reader has no
   pointer to tell them where they landed. */
.work-detail h3:focus { outline: none; }
.work-detail h3:focus-visible { outline: 2px solid var(--accent-1); outline-offset: 4px; border-radius: 4px; }
/* The detail heading kept the dialog's display size; that rule died with the
   dialog, so the project name is restated here rather than falling back to the
   base h3 size. */
.work-detail h3 { font-size: clamp(1.4rem, 3vw, 1.8rem); letter-spacing: -0.01em; margin: 0 0 0.9rem; }

/* Identity left, substance right: the title and keywords are what you scan to
   confirm which project you are looking at, and the blocks are what you read. */
.work-detail-cols {
  display: grid;
  grid-template-columns: minmax(0, 300px) minmax(0, 1fr);
  gap: clamp(1.2rem, 3vw, 2.4rem);
  align-items: start;
}
/* The rail lists the area's other projects by title. Quiet by design: it is
   navigation between neighbours, not the thing being read. */
.work-siblings { align-self: start; }
.work-siblings-head {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--topic, var(--accent-1));
  margin: 0 0 0.5rem;
}
.work-siblings-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.15rem; }
.work-siblings-lead {
  color: var(--muted);
  font-size: 0.82rem;
  line-height: 1.55;
  margin: 0 0 0.9rem;
}
.work-siblings-count {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  color: var(--faint);
  margin: 0.7rem 0 0;
  padding-left: 0.6rem;
}
.work-sibling-title { display: block; font-weight: 600; }
/* Two lines at most: the rail is navigation, and a four-line question in a 300px
   column would push the fourth project below the fold on a laptop. */
.work-sibling-q {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  color: var(--muted);
  font-size: 0.8rem;
  line-height: 1.4;
  margin-top: 0.2rem;
}
.work-sibling[aria-current="true"] .work-sibling-q { color: var(--text); }
/* Who the work was for, under each title: it is the fastest way to tell two
   projects apart when their names are both abbreviations. */
.work-sibling-org {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.02em;
  color: var(--faint);
  margin-top: 0.15rem;
}
.work-sibling[aria-current="true"] .work-sibling-org { color: var(--muted); }
.work-sibling {
  appearance: none;
  cursor: pointer;
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  border-left: 2px solid transparent;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  padding: 0.55rem 0.6rem;
  color: var(--muted);
  font-family: var(--font-body);
  font-size: 0.88rem;
  line-height: 1.35;
  min-height: 44px;
  transition: color 0.18s, background 0.18s, border-color 0.18s;
}
.work-sibling[aria-current="true"] {
  color: var(--text);
  font-weight: 600;
  background: color-mix(in srgb, var(--topic, var(--accent-1)) 12%, transparent);
  border-left-color: var(--topic, var(--accent-1));
}
.work-sibling:focus-visible { outline: 2px solid var(--accent-1); outline-offset: 2px; }
.work-detail-body { display: grid; gap: 0.9rem; min-width: 0; }

/* One column on a phone: a 260px identity rail beside body text does not fit,
   and the title reads better directly above what it describes. The rule is still
   needed with the rail hidden below -- the 300px track is explicit, so an empty
   first column still grows to its 300px growth limit and would squeeze the body
   into what is left of a 390px screen. */
@media (max-width: 760px) {
  .work-detail-cols { grid-template-columns: minmax(0, 1fr); }
  /* The cards above are already the navigation; stacked, the rail repeats them:
     its questions land directly beneath the same questions in the card above.
     Beside a long detail on desktop it is a persistent index and earns its
     place; in one column it is the same list twice. */
  .work-siblings { display: none; }
}

.work-resources { display: flex; flex-wrap: wrap; gap: 0.9rem; margin-top: 1.2rem; }
.work-resource {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: var(--accent-1);
  text-decoration: none;
}
.work-resource:hover { text-decoration: underline; }
.work-resource.is-soon { color: var(--faint); }
/* The kind in words, in front of the authored label: the glyph reinforces which
   sort of resource this is, but on its own it never said it. flex: none so the
   pill keeps its own width inside the inline-flex row. */
.work-resource-kind {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 0.1rem 0.4rem;
  border: 1px solid color-mix(in srgb, var(--accent-1) 40%, transparent);
  border-radius: 999px;
  color: var(--accent-1);
  flex: none;
}

/* ---------- Work: four problem cards, each holding its questions ---------- */
/* Replaces the pill filter row: the user asked for cards that describe the
   problem, not just name a topic. The questions moved inside them, so all eleven
   are on screen at once -- nothing filters, and the card is a section rather
   than a control.

   The shell, the grid, the glyph tile and the entrance stagger were shared with
   About's .work-card. Those four cards are corner nodes in the constellation now, so
   these rules are Projects' alone -- they were left in the About section of this file,
   where they were declared, rather than moved, since moving them would make the diff
   for that change unreadable. Only what is particular to this panel is here: the
   grid's own bottom margin, the chosen-card mark and the question list. */
.domain-grid { margin-bottom: clamp(1.6rem, 4vw, 2.4rem); }
/* Grid layout, gap and align-content come from the .domain-grid rule up in the About
   section; the card shell is a block, so the inner rhythm is set by the children's own
   margins. */
/* The chosen card is the one holding the chosen question. Set by projects.js
   rather than with :has(), which nothing else in this file relies on. The mark is
   the same top-edge accent bar About sweeps in on hover -- here it is selection,
   not the pointer, that reveals it, since this card is a section and does not
   answer a click. */
.domain-card.is-chosen {
  border-color: color-mix(in srgb, var(--topic, var(--accent-1)) 55%, transparent);
  box-shadow: 0 12px 34px -24px var(--topic, var(--accent-1));
}
.domain-card.is-chosen::before { transform: scaleX(1); }
/* The area sentence and the "3 projects" count are gone from these cards at the
   user's request: a card is now its icon, its area name and its questions. The
   sentence still exists once, on the constellation's .aq-topic-lead -- it moved there
   with the four cards -- and the count restated something the list underneath it
   already showed. */

/* The questions themselves. The row -- plate, left rule, type, padding, arrow,
   hover and focus ring -- is the one shared with About's .wq-link, up in the About
   section of this file. What is left here is the part About has no use for: the
   marker that fills in on the selected question, and the stronger plate that goes
   with it. Colour and type carry the state, since the user asked for no graphics
   here.

   The list's own rule is shared as well, so .domain-questions is not redeclared:
   it would only have restated the same four properties and could then drift. */
/* Hollow ring, filled when this question is the one on screen. Sized in em so it
   tracks the question's own type rather than needing its own breakpoint. */
.dq-mark {
  width: 0.5rem;
  height: 0.5rem;
  margin-top: 0.5em;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--topic, var(--accent-1)) 55%, transparent);
  background: none;
  transition: background 0.18s, border-color 0.18s, box-shadow 0.18s;
}
/* The selected question. Stronger than the shared plate rather than different from
   it: the same tint further up and the left rule at full strength, so it reads as
   one of a set that happens to be current.

   No font-weight change, deliberately. A 600 here was measured at 600 against the
   published 400 for .domain-questions .dq -- the state has to be carried by colour
   and the rule alone, because the type scale on this panel is the published one. */
.dq[aria-current="true"] {
  color: var(--text);
  background: color-mix(in srgb, var(--topic, var(--accent-1)) 18%, transparent);
  border-left-color: var(--topic, var(--accent-1));
}
.dq[aria-current="true"] .dq-mark {
  background: var(--topic, var(--accent-1));
  border-color: var(--topic, var(--accent-1));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--topic, var(--accent-1)) 18%, transparent);
}
/* The arrow reveal on the current row and the focus ring are both in the shared
   rule up in the About section; restating them here would be two places to
   change one thing. */

/* The questions are visible at every width. A per-card phone disclosure lived
   here for a day and was reverted: About shows one example question per area with
   a link onward, so Projects is the page you land on to browse the questions --
   hiding them behind four toggles fought what the page is for. */

/* The 1020px/520px column steps this block used to declare are gone: .domain-grid's
   steps (1150px to two columns, 560px to one) are the measured ones -- 1150 is where
   four cards across stop fitting and leave a single card orphaned on a second row.
   Two sets of breakpoints on the same grid contradicted each other between 1020 and
   1150. */

/* Per-role skill keywords. Deliberately quieter than the project keyword chips:
   no fill, muted text, so they annotate the role rather than competing with the
   title above them. Restores the .tl-tags treatment that was deleted with the
   old two-column timeline. .tags already supplies list-style/display/margin/
   padding, so only the deltas live here. */
.jrn-tags,
.exp-skills {
  gap: 0.3rem 0.35rem;
  margin-top: 0.6rem;
}
.jrn-tags li,
.exp-skills li {
  background: none;
  border: 1px solid var(--border);
  /* 999px, not --radius-sm: the version being restored used full pills, and
     .tags li's squarer radius made the chips read as a different component. */
  border-radius: 999px;
  color: var(--faint);
  font-size: 0.68rem;
  letter-spacing: 0.02em;
  white-space: nowrap;
  padding: 0.15rem 0.5rem;
}
/* The Journey's chips lift with their step. The detail panel's do not need an
   active state at all -- it only ever shows the selected role -- so they sit at
   the stronger treatment permanently rather than behind a selector that could
   never match: .exp-detail is a sibling of .exp-vlist, so .exp-skills is never
   a descendant of .exp-node. */
.exp-skills li { border-color: var(--border-strong); color: var(--muted); }
.jrn-step:hover .jrn-tags li { border-color: var(--border-strong); color: var(--muted); }

/* Journey skills are collapsed by default. One control for the whole row rather
   than one per step: the steps sit side by side in a grid, so revealing them
   individually would leave the row ragged, and three chips wrap to two or three
   lines on the longer roles. Collapsed, every step is the same height. */
.jrn-head-actions { display: flex; align-items: baseline; gap: 1rem; flex-wrap: wrap; }
.jrn-skills-btn {
  appearance: none;
  cursor: pointer;
  background: none;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  padding: 0.25rem 0.7rem;
  /* WCAG 2.5.8 wants 24x24 minimum; the padding alone landed on exactly 24,
     which is one rounding error from failing. 28 clears it without making a
     secondary control shout. The mobile block takes it to 40. */
  min-height: 28px;
  transition: color 0.18s, border-color 0.18s;
}
.jrn-skills-btn:hover { color: var(--text); border-color: var(--border-strong); }
.jrn-skills-btn:focus-visible { outline: 2px solid var(--accent-1); outline-offset: 2px; }
.jrn-tags { display: none; }
.about-journey[data-skills="shown"] .jrn-tags { display: flex; }

@media (max-width: 560px) {
  .jrn-skills-btn { min-height: 40px; }
}


/* ---- the lead above the constellation ----
   The invitation and the way in, on one line. This row replaced a heading and a "See All
   Projects" link: the heading duplicated what the invitation says, and the link was a
   shortcut to a panel the tab bar already reaches.

   The instruction is not here. It sits below, hidden, because it answers a question only
   a first-time visitor has -- and asking everyone else to read it on every visit is how a
   page starts feeling like a manual. */
.aq-lead {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.4rem 0.9rem;
  margin: 0 0 0.5rem;
}
.aq-invite {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(1.05rem, 2.1vw, 1.35rem);
  letter-spacing: -0.01em;
}
/* Quiet beside the invitation: it is a way in, not the invitation itself. Sized and
   coloured like the other small controls on the page rather than like a link, because it
   reveals text in place and goes nowhere. */
.aq-how {
  appearance: none;
  background: none;
  border: 1px solid color-mix(in srgb, var(--map-ink-dim) 30%, transparent);
  border-radius: 999px;
  padding: 0.25rem 0.7rem;
  min-height: 32px;
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.03em;
  cursor: pointer;
}
.aq-how:hover { color: var(--text); border-color: color-mix(in srgb, var(--map-ink-dim) 60%, transparent); }
.aq-how:focus-visible { outline: 2px solid var(--map-ink); outline-offset: 3px; }

/* Revealed by .aq-how. [hidden] does the hiding, so this only styles the shown state. */
.aq-instruction {
  margin: 0 0 1rem;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.02em;
  color: var(--muted);
  max-width: 66ch;
}

/* ---- the area that owns the centred question ----
   Replaces the curves qmap.js drew from each question to its corner block. An edge made
   the reader trace a line across the field to learn one thing: which of four areas the
   question belongs to. This states it instead.

   The first attempt at this gave the lit block an opaque fill and a 1px border, and it
   was wrong for the same reason the centred question's pill was wrong: at rest these
   blocks are nothing but an icon and a word -- no box, no ground -- so a filled,
   bordered rectangle did not make one of them brighter, it made one of them a different
   kind of object, a UI panel dropped onto a starfield. The field does not gain a plate.

   What is left is light. The name already carries its area's colour at rest, so the lit
   state cannot be "colour it" -- it has to be a different axis. Three, none of which
   draws an edge:

     a scrim behind the block that fades to nothing well inside its own box, the same
     device the centred question uses;
     a halo on the name, in its own colour, like the centred question's;
     the icon chip coming up from a 16 percent tint to a lit one.

   Nothing here has a border, a hard shadow, or a rectangular boundary of any kind. */
.aq-topic.is-lit {
  /* closest-side, so the wash dies inside the block's own bounds rather than at them --
     that is the difference between a lit area and a card. */
  background: radial-gradient(
    closest-side at 30% 50%,
    color-mix(in srgb, var(--aq-topic, var(--map-ink)) 22%, transparent),
    color-mix(in srgb, var(--aq-topic, var(--map-ink)) 7%, transparent) 55%,
    transparent
  );
}
.aq-topic.is-lit .aq-topic-name {
  /* Two stops: a tight one thickening the strokes so mono type gains presence without a
     weight jump, and a wide one that is the glow. Same construction as the centred
     question's, at lower intensity -- this is an annotation, not the subject. */
  text-shadow:
    0 0 0.35em color-mix(in srgb, var(--aq-topic, transparent) 80%, transparent),
    0 0 1.1em color-mix(in srgb, var(--aq-topic, transparent) 45%, transparent);
}
.aq-topic.is-lit .aq-topic-ic {
  background: color-mix(in srgb, var(--aq-topic) 34%, transparent);
  box-shadow: 0 0 1.4em -0.2em color-mix(in srgb, var(--aq-topic) 42%, transparent);
}
