/* ------- Tokens -------
   The dark neutrals are warm on purpose. They used to sit at OKLCH hue 286 —
   blue-violet — while the accent was hue 58 and the text hue 97, so a single
   theme carried three hue families with one on the opposite side of the wheel.
   logo.png has no cool colour in it at all: its browns run hue 46-67 and its
   cream sits at 67. The surfaces now sit at hue 70, between the logo's brown
   and the existing warm text.

   Chroma is deliberately tiny — R-B of only +2 to +5 across the six tokens.
   A first pass used roughly three times this and read as sepia: warm tints
   advance and the eye is far more sensitive to a yellow shift in a near-black
   than to a blue one, so matching the old cool chroma number for number
   overshoots badly. The aim is a dark that is essentially neutral and merely
   leans the same way as the logo, not a brown one.

   Lightness is unchanged on every token, so all measured contrast ratios hold
   (16.47 / 9.78 / 5.54 against --bg). */
:root {
  --bg: #0c0b0a;
  --bg-2: #121110;
  --bg-3: #1c1a19;
  --fg: #ecebe6;
  --fg-2: #b9b7ad;
  /* --fg-3 carries real reading copy (.impact-note, .chat-disclaimer, footer),
     so it has to clear 4.5:1 on --bg, not just look quiet. #6c6a62 measured
     3.63:1 and failed; this is ~5.3:1 and keeps the same warm cast. */
  --fg-3: #8a8880;
  --rule: #282624;
  --rule-2: #363331;
  --rule-soft: #1d1b1a; /* separators inside dense lists — the lightest tier */
  --accent: oklch(0.78 0.14 58);
  --accent-2: oklch(0.68 0.16 58);
  --accent-ink: #0c0b0a;
  --ok: oklch(0.78 0.14 145);
  --warn: oklch(0.78 0.14 60);
  /* Keyboard focus ring. Tracks --accent, which clears 3:1 against every
     surface it lands on in both themes (9.5:1 dark, 5.7:1 light). */
  --focus: var(--accent);

  --ff-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --ff-sans: "Inter", system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
  --ff-serif: "Instrument Serif", "Iowan Old Style", "Palatino", Georgia, serif;

  /* Motion. Every transition in this file used to be a bare duration, which
     means the browser default `ease` — the one curve worth never shipping.
     Three named curves and three durations, referenced everywhere. */
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in: cubic-bezier(0.64, 0, 0.78, 0);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --dur-fast: 150ms;
  --dur-base: 250ms;
  --dur-slow: 400ms;

  /* The graph-paper backdrop reads very differently per theme: near-invisible
     on ink, but a warm stain on cream. Opacity is a token so each theme can
     set its own. */
  --grid-opacity: .22;

  --unit: 8px;
  --pad: clamp(20px, 4vw, 56px);
  --maxw: 1400px;
  /* Height of the fixed .chrome bar. Anchor targets and the hero's top pad
     derive from it, so the taller two-row mobile header can't sit on top of
     the section you just jumped to. */
  --chrome-h: 52px;

  color-scheme: dark;
}

:root[data-theme="light"] {
  --bg: #f4f1e9;
  --bg-2: #ece8dd;
  --bg-3: #e1dccd;
  --fg: #171614;
  --fg-2: #4a4842;
  --fg-3: #63615a;
  --rule: #d8d3c3;
  --rule-2: #c2bca9;
  --rule-soft: #e6e1d2;
  /* Light accent is both 10–11px text (.kicker, .lib-cta, .sys-more) and the
     .btn.primary background under --accent-ink. At L=0.58 both were ~3.9:1 on
     cream; L=0.50 clears 4.5:1 for the text and the button label together.
     Keep ACCENTS.ember in app.js in sync — applyTweaks() sets --accent inline
     and would otherwise override whatever is declared here. */
  --accent: oklch(0.50 0.15 50);
  --accent-2: oklch(0.42 0.16 50);
  --accent-ink: #f4f1e9;
  --grid-opacity: .1; /* warm rule on cream shows far more than cool rule on ink */
  color-scheme: light;
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
/* `clip`, not `hidden`: hidden makes the element a scroll container, which
   silently breaks position:sticky descendants and scroll anchoring. Both
   elements, so an overflowing child can't escape via <html>. */
html, body { overflow-x: clip; }
body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--ff-sans);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

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

a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }

/* ------- Focus -------
   There was no :focus-visible anywhere in this stylesheet, so every control
   fell back to the UA ring — undesigned on a dark amber page, and invisible
   on the inputs that set `outline: 0`. One ring, applied everywhere, never
   transitioned: a focus indicator that fades in leaves keyboard users with
   nothing at the moment they arrive. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  transition: none;
}
/* Cells that sit flush in a 1px-gutter grid ring inward, so the outline
   isn't clipped by — or overlapping — the neighbouring cell. */
.sys-cell:focus-visible,
.arch-list-item:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -2px;
}
/* SVG nodes are made focusable in app.js; give them the same ring. The rect
   carries a stroke transition for hover, which would otherwise fade the focus
   ring in — keyboard users would have no indicator at the moment they arrive. */
.arch-node:focus-visible { outline: none; }
.arch-node:focus-visible rect {
  stroke: var(--focus);
  stroke-width: 2;
  transition: none;
}

/* ------- Grid background ------- */
.grid-bg {
  position: fixed; inset: 0; pointer-events: none; z-index: 0;
  background-image:
    linear-gradient(to right, var(--rule) 1px, transparent 1px),
    linear-gradient(to bottom, var(--rule) 1px, transparent 1px);
  background-size: 80px 80px;
  opacity: var(--grid-opacity);
  /* The old `ellipse 80% 60% at 50% 30%` was a hard-edged shape on a fixed
     layer, so it stayed put while the page scrolled past — a bright rectangle
     parked mid-screen that read as a light leak rather than a substrate. A
     top-weighted linear fade has no edge to notice. */
  mask-image: linear-gradient(to bottom, #000 0%, #000 30%, transparent 92%);
}
body[data-grid="off"] .grid-bg { display: none; }

/* ------- Chrome ------- */
.chrome {
  position: fixed; top: 0; left: 0; right: 0; z-index: 50;
  display: flex; align-items: center; justify-content: space-between;
  /* The bar is full-bleed for its blur and border, but its CONTENTS share the
     content column. Left at a plain --pad the wordmark sat ~460px outside the
     text on a wide display, labelling nothing. */
  padding: 14px max(var(--pad), calc((100% - var(--maxw)) / 2 + var(--pad)));
  backdrop-filter: blur(18px) saturate(1.1);
  -webkit-backdrop-filter: blur(18px) saturate(1.1);
  background: color-mix(in oklch, var(--bg) 72%, transparent);
  border-bottom: 1px solid var(--rule);
  font-family: var(--ff-mono);
  font-size: 12px;
  letter-spacing: .04em;
}
.chrome .brand {
  display: flex; align-items: center; gap: 10px;
  text-transform: uppercase;
}
.brand-dot {
  width: 9px; height: 9px; background: var(--accent); border-radius: 2px;
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--accent) 20%, transparent);
  animation: pulse 2.6s ease-in-out infinite;
}
@keyframes pulse {
  0%,100% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.2); opacity: .7; }
}
.chrome nav { display: flex; gap: 22px; }
.chrome nav a {
  color: var(--fg-2);
  transition: color var(--dur-base) var(--ease-out);
  text-transform: lowercase;
}
.chrome nav a:hover { color: var(--fg); }
.chrome nav a::before { content: "· "; color: var(--fg-3); }

.chrome .tools { display: flex; align-items: center; gap: 8px; }
.seg {
  display: inline-flex; border: 1px solid var(--rule-2); border-radius: 999px;
  padding: 2px; font-family: var(--ff-mono); font-size: 11px;
}
.seg button {
  padding: 4px 10px; border-radius: 999px; color: var(--fg-2);
  transition: background-color var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}
.seg button.on { background: var(--fg); color: var(--bg); }
.seg button:not(.on):hover { color: var(--fg); }

/* Two identical pill segments sat side by side in the header — on a phone they
   were most of it. Language stays a segment because both options are live
   choices; theme collapses to the single inactive option, so the control reads
   as "switch to Light" rather than a second twin of the language picker. */
.seg[data-seg="theme"] { border-color: var(--rule); }
.seg[data-seg="theme"] button.on { display: none; }

.chrome .brand-name { white-space: nowrap; }
.chrome .brand-suffix { color: var(--fg-3); }

/* Below 900px the nav was `display: none` with nothing in its place: a phone
   visitor got a wordmark, four toggle buttons, and ~9,000px of scroll with no
   way to reach any section. The links now take their own full-width row and
   scroll horizontally — all seven destinations stay reachable with no menu, no
   focus trap and no JS. The row bleeds to the viewport edges so the last link
   can scroll clear of the page padding. */
@media (max-width: 900px) {
  /* Two rows now: brand + tools, then the link strip. Measures ~82px; the
     token is rounded up so a longer label (pt-BR) can't tuck a section
     heading under the bar. */
  :root { --chrome-h: 92px; }
  .chrome {
    flex-wrap: wrap;
    row-gap: 8px;
    padding-bottom: 0;
  }
  .chrome nav {
    order: 3;
    width: 100%;
    gap: 18px;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    -ms-overflow-style: none;
    margin: 0 calc(var(--pad) * -1);
    padding: 2px var(--pad) 9px;
  }
  .chrome nav::-webkit-scrollbar { display: none; }
  .chrome nav a { white-space: nowrap; }
}
@media (max-width: 520px) {
  .chrome .brand-suffix { display: none; }
}

/* ------- Section scaffold ------- */
main { position: relative; z-index: 1; }
section {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 96px var(--pad);
  position: relative;
  scroll-margin-top: var(--chrome-h);
}
/* Rule hierarchy. Everything used to be drawn at --rule: section breaks, panel
   borders, grid gutters, list separators and the backdrop, all one weight, so
   nothing separated more than anything else. Three tiers now — section breaks
   are the strongest, panels sit at --rule, and internal list separators are
   lighter still. */
section + section { border-top: 1px solid var(--rule-2); }

/* Vertical rhythm. Ten sections at an identical 96px read as a contact sheet.
   The reference sections breathe; the dense reference lists tighten. */
#skills, #library { padding-top: 72px; padding-bottom: 72px; }
#contact { padding-bottom: 128px; }

.kicker {
  font-family: var(--ff-mono);
  font-size: 11px;
  letter-spacing: .12em;
  color: var(--accent);
  text-transform: uppercase;
  margin-bottom: 24px;
  display: flex; align-items: center; gap: 10px;
}
.kicker::before {
  content: ""; width: 18px; height: 1px; background: var(--accent);
}

h1, h2, h3 { margin: 0; font-weight: 500; letter-spacing: -0.02em; }
h1 { font-size: clamp(48px, 8vw, 120px); line-height: .96; }
h2 { font-size: clamp(34px, 4.5vw, 64px); line-height: 1.04; }
h3 { font-size: clamp(20px, 2vw, 28px); line-height: 1.2; }

/* The cap is a reading measure, not a pixel count. At 900px it was ~22ch of
   the 64px display face, so "A 20-year outcome-oriented journey" (27.4ch,
   1095px) broke onto a second line with ~390px of the column still empty.
   In `ch` the limit scales with the font — including a display face swapped
   from the tweaks panel — so a title wraps at the same character count at
   every viewport instead of at whatever the clamp happens to compute. 30ch
   keeps the one genuinely long title (About, 51ch) to two lines. */
.section-title { max-width: 30ch; text-wrap: balance; }
.section-sub { color: var(--fg-2); max-width: 680px; margin-top: 14px; text-wrap: pretty; }

/* ------- Hero ------- */
.hero {
  padding-top: calc(var(--chrome-h) + 88px);
  padding-bottom: 80px;
}
.hero-kicker {
  font-family: var(--ff-mono);
  font-size: 12px;
  color: var(--fg-3);
  margin-bottom: 32px;
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
}
.hero-kicker .tag {
  padding: 2px 8px; border: 1px solid var(--rule-2); border-radius: 4px;
  color: var(--fg-2);
}
.hero-kicker .tag.live { color: var(--accent); border-color: color-mix(in oklch, var(--accent) 40%, transparent); }
.hero-kicker .tag.live::before {
  content: "●"; margin-right: 6px;
  animation: pulse 1.6s ease-in-out infinite;
}

.hero-name {
  font-family: var(--ff-serif);
  font-weight: 400;
  letter-spacing: -0.03em;
  margin-bottom: 8px;
}
.hero-tag {
  font-family: var(--ff-sans);
  font-weight: 300;
  font-size: clamp(28px, 4vw, 54px);
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--fg-2);
  max-width: 1100px;
  margin-top: 24px;
  text-wrap: balance;
}
/* Roman, not italic. An italicised emphasis word inside an upright display
   line ("builds platforms *that engineers adopt*") is the most recognisable
   generated-page tell there is. The emphasis is already carried three other
   ways — a different face, a heavier weight, and --fg against the --fg-2 of
   the surrounding words — so the italic was the fourth signal and the only
   one that cost anything. Body-copy italic (.about-body p em) is untouched;
   that is where italic belongs. */
.hero-tag em {
  font-family: var(--ff-emphasis, "Space Grotesk", "Inter", sans-serif);
  font-style: normal;
  color: var(--fg);
  font-weight: var(--ff-emphasis-weight, 500);
  letter-spacing: -0.01em;
}

.hero-grid {
  display: grid;
  grid-template-columns: 1.1fr .9fr;
  gap: 60px;
  margin-top: 72px;
  align-items: start;
}
@media (max-width: 900px) {
  .hero-grid { grid-template-columns: 1fr; gap: 40px; }
}

.hero-intro {
  font-size: 18px;
  color: var(--fg-2);
  max-width: 560px;
  text-wrap: pretty;
}

.cta-row { display: flex; gap: 12px; margin-top: 28px; flex-wrap: wrap; }

.btn {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 12px 20px;
  border-radius: 999px;
  font-family: var(--ff-mono);
  font-size: 12px;
  letter-spacing: .04em;
  text-transform: uppercase;
  transition: background-color var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
  border: 1px solid var(--rule-2);
}
.btn.primary {
  background: var(--accent); color: var(--accent-ink); border-color: var(--accent);
}
.btn.primary:hover {
  background: var(--accent-2); border-color: var(--accent-2);
  transform: translateY(-1px);
}
.btn.ghost:hover { border-color: var(--fg-2); color: var(--fg); }
.btn .arr { transition: transform var(--dur-base) var(--ease-out); }
.btn:hover .arr { transform: translateX(3px); }

/* Terminal
   Deliberately NOT a drawn window. This used to carry a fake macOS title bar —
   three traffic-light dots and a "~/anderson — zsh — 80x24" caption — over a
   rounded, drop-shadowed panel. The visitor already has real window chrome, so
   redrawing it reads as an imitation of an app rather than a piece of writing,
   and the imitation was poor besides (the dots were flat --bg-3, no colour).
   What earns its place is the typed content, so the frame is now typographic:
   a hairline box, an accent label, and the transcript. */
.terminal {
  background: var(--bg-2);
  border: 1px solid var(--rule);
  font-family: var(--ff-mono);
  font-size: 13px;
  line-height: 1.7;
  overflow: hidden;
}
.terminal-head {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 20px;
  border-bottom: 1px solid var(--rule);
  color: var(--accent);
  font-size: 10px;
  letter-spacing: .12em;
  text-transform: uppercase;
}
.terminal-head::before {
  content: ""; width: 14px; height: 1px; background: var(--accent); flex: none;
}
.terminal-body { padding: 18px 20px 22px; min-height: 260px; }
.t-line { display: flex; gap: 10px; }
.t-prompt { color: var(--accent); user-select: none; }
.t-in { color: var(--fg); }
.t-out { color: var(--fg-2); padding-left: 24px; }
.t-caret {
  display: inline-block; width: 8px; height: 14px; background: var(--accent);
  vertical-align: -2px; margin-left: 2px;
  animation: blink 1.1s steps(1) infinite;
}
@keyframes blink { 50% { opacity: 0; } }

/* Meta strip */
.meta-strip {
  display: flex; flex-wrap: wrap; gap: 18px 36px;
  padding: 20px 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  margin-top: 80px;
  font-family: var(--ff-mono);
  font-size: 11px;
  letter-spacing: .04em;
  color: var(--fg-3);
}
.meta-strip div { display: flex; gap: 10px; }
.meta-strip b { color: var(--fg); font-weight: 500; }

/* ------- Impact ------- */
.impact-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  margin-top: 48px;
  background: var(--rule);
  border: 1px solid var(--rule);
}
@media (max-width: 900px) {
  .impact-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
  .impact-grid { grid-template-columns: 1fr; }
}
.impact-cell {
  background: var(--bg);
  padding: 40px 32px;
  position: relative;
  transition: background var(--dur-base) var(--ease-out);
}
.impact-cell:hover { background: var(--bg-2); }
/* The 01–06 corner numbers are gone: they numbered a set that has no order,
   at a size that couldn't be read anyway. */
.impact-value {
  font-family: var(--ff-serif);
  font-size: clamp(54px, 6vw, 84px);
  font-weight: 400;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--fg);
}
.impact-value .unit { color: var(--accent); font-family: var(--ff-sans); font-style: normal; font-size: .7em; font-weight: 400; }
.impact-label {
  font-family: var(--ff-mono);
  font-size: 11px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--fg-2);
  margin-top: 18px;
}
.impact-note {
  color: var(--fg-3);
  font-size: 13px;
  margin-top: 10px;
  line-height: 1.5;
}

/* ------- About -------
   The prose used to sit at 680px inside a 968px track, leaving ~288px of the
   section empty. Widening the text was not the fix: 680px is already ~85
   characters per line at 17px Inter, past the comfortable 65-75. So the
   leftover goes to content instead — the off-hours note moves out of the prose
   flow into its own column, which fills the width and gives the aside a proper
   place rather than a box wedged beneath the paragraphs. */
.about-grid {
  display: grid;
  grid-template-columns: 260px minmax(0, 1fr);
  gap: 60px;
  margin-top: 48px;
}
.about-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 320px);
  gap: 0 56px;
  align-items: start;
}
.about-prose { min-width: 0; }
/* Three columns only while the prose can still hold a decent measure. Below
   this the aside would squeeze it under ~62 characters, so it drops beneath
   the paragraphs instead. */
@media (max-width: 1279px) {
  .about-body { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 900px) {
  .about-grid { grid-template-columns: 1fr; gap: 40px; }
}
.portrait {
  /* <figure> carries a UA margin of 16px 40px, which was never reset: the
     portrait rendered 180px wide inside its 260px track and sat 40px right of
     the title's left edge. */
  margin: 0;
  aspect-ratio: 3 / 4;
  background: var(--bg-2);
  border: 1px solid var(--rule);
  position: relative;
  overflow: hidden;
  display: grid; place-items: center;
}
.portrait::before {
  content: "";
  position: absolute; inset: 0;
  background-image: repeating-linear-gradient(135deg, color-mix(in oklch, var(--fg) 5%, transparent) 0 1px, transparent 1px 8px);
}
.portrait-img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: center top;
}
.portrait .caption {
  position: absolute; bottom: 10px; left: 10px; right: 10px;
  z-index: 2;
  color: var(--accent-ink);
  mix-blend-mode: difference;
  font-family: var(--ff-mono); font-size: 10px; color: var(--fg-3);
  display: flex; justify-content: space-between;
}
.about-body p { color: var(--fg-2); font-size: 17px; max-width: 52ch; text-wrap: pretty; }
.about-body p + p { margin-top: 18px; }
.about-body p em { color: var(--fg); font-style: italic; font-family: var(--ff-serif); font-size: 1.04em; }

.offhours {
  margin-top: 0;
  padding: 18px 20px;
  border-left: 2px solid var(--accent);
  background: var(--bg-2);
}
@media (max-width: 1279px) {
  .offhours { margin-top: 32px; max-width: 52ch; }
}
.offhours .lbl {
  font-family: var(--ff-mono); font-size: 10px; color: var(--accent);
  letter-spacing: .1em; text-transform: uppercase;
}
.offhours p { margin: 6px 0 0; color: var(--fg-2); font-size: 15px; }

/* ------- Experience diagram ------- */
.arch-wrap {
  margin-top: 20px;
  border: 1px solid var(--rule);
  background: var(--bg-2);
  border-radius: 10px;
  overflow: hidden;
}
.arch-legend {
  display: flex; gap: 18px; padding: 12px 16px;
  border-bottom: 1px solid var(--rule);
  font-family: var(--ff-mono); font-size: 11px; color: var(--fg-3);
  flex-wrap: wrap;
}
.arch-legend .lg { display: flex; align-items: center; gap: 6px; }
.arch-legend .sw { width: 14px; height: 6px; border-radius: 2px; background: var(--fg-3); }
.arch-legend .sw.ai { background: var(--accent); }
.arch-legend .sw.ciam { background: oklch(0.7 0.12 260); }
.arch-legend .sw.plat { background: oklch(0.7 0.12 180); }

.arch-canvas {
  position: relative;
  padding: 32px 24px;
}
.arch-canvas svg { width: 100%; height: auto; display: block; }

/* Narrow-viewport experience list.
   The diagram's viewBox is ~1004 units wide and scales to fit, so on a 390px
   phone it renders at 0.29x — the 11px node labels land at ~3px and the tap
   targets at 46x19. Below 760px the SVG is dropped for this list, which is the
   same nodes from the same markdown driving the same selectNode(), just as
   rows. Above 760px the list is dropped and the diagram is authoritative. */
.arch-list { display: none; }
.arch-list-item {
  display: grid;
  /* Role gets the full column and the dates sit on their own line below it.
     Sharing one row squeezed long roles ("Senior Principal Cloud Platform
     Engineer") into three lines against a nowrap date. */
  grid-template-columns: 4px 1fr;
  gap: 3px 14px;
  width: 100%;
  padding: 14px 18px;
  border: 0;
  border-bottom: 1px solid var(--rule);
  background: transparent;
  text-align: left;
  color: inherit;
  font: inherit;
  cursor: pointer;
  min-height: 56px; /* comfortably past the 24px WCAG 2.5.8 floor */
  transition: background var(--dur-base) var(--ease-out);
}
.arch-list-item:last-child { border-bottom: 0; }
.arch-list-item .band {
  grid-row: 1 / span 2;
  align-self: stretch;
  border-radius: 2px;
  background: var(--fg-3);
}
.arch-list-item .band.ai   { background: var(--accent); }
.arch-list-item .band.ciam { background: oklch(0.7 0.12 260); }
.arch-list-item .band.plat { background: oklch(0.7 0.12 180); }
.arch-list-item .role {
  font-family: var(--ff-mono); font-size: 13px; font-weight: 500;
  letter-spacing: .02em; color: var(--fg);
}
.arch-list-item .meta {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 6px 16px; flex-wrap: wrap;
  font-family: var(--ff-mono); font-size: 11px; color: var(--fg-3);
  letter-spacing: .04em;
}
.arch-list-item .when { text-align: right; }
.arch-list-item:hover { background: var(--bg-3); }
.arch-list-item.active { background: var(--bg-3); }
.arch-list-item.active .role { color: var(--accent); }

@media (max-width: 760px) {
  .arch-canvas { display: none; }
  .arch-list { display: block; }
}

.arch-detail {
  padding: 24px 28px;
  border-top: 1px solid var(--rule);
  background: var(--bg-3);
  min-height: 180px;
  scroll-margin-top: calc(var(--chrome-h) + 12px); /* clear the fixed header when scrollIntoView focuses it */
}
.exp-detail-toggle {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  margin-bottom: 20px;
  border: 1px solid var(--rule-2);
  border-radius: 999px;
  background: var(--bg-2);
}
.exp-detail-opt {
  appearance: none;
  border: 0;
  border-radius: 999px;
  padding: 5px 14px;
  font-family: var(--ff-mono);
  font-size: 11px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--fg-2);
  background: transparent;
  cursor: pointer;
  transition: background var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
}
.exp-detail-opt:not(.on):hover { color: var(--fg); }
.exp-detail-opt.on {
  background: var(--accent);
  color: var(--accent-ink);
}

/* Prerendered experience panels. build.mjs emits every role's bullets so the
   Work section's substance is in the HTML; only the selected one is shown, and
   app.js swaps the class rather than rebuilding the container. A tab pattern,
   not hidden text — the same content a click reveals. */
.dt-panel { display: none; }
.dt-panel.on { display: block; }
.arch-detail .dt-secondary { display: none; }
.arch-detail.mode-complete .dt-secondary { display: block; margin-top: 10px; }

.arch-detail .dt-head {
  display: flex; justify-content: space-between; align-items: baseline;
  flex-wrap: wrap; gap: 12px;
  margin-bottom: 14px;
}
.arch-detail .dt-role {
  font-family: var(--ff-serif); font-size: 26px; color: var(--fg); letter-spacing: -0.01em;
}
.arch-detail .dt-co {
  font-family: var(--ff-mono); font-size: 12px; color: var(--fg-2);
  letter-spacing: .04em;
}
.arch-detail .dt-when {
  font-family: var(--ff-mono); font-size: 11px; color: var(--accent);
  letter-spacing: .06em;
}
.arch-detail ul {
  margin: 0; padding: 0; list-style: none;
  display: grid; grid-template-columns: 1fr 1fr; gap: 10px 32px;
  color: var(--fg-2); font-size: 14.5px;
}
@media (max-width: 700px) { .arch-detail ul { grid-template-columns: 1fr; } }
.arch-detail li {
  position: relative; padding-left: 18px;
}
.arch-detail li::before {
  content: "→"; position: absolute; left: 0; color: var(--accent); font-family: var(--ff-mono);
}

/* ------- Systems map ------- */
.systems-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  margin-top: 48px;
  background: var(--rule);
  border: 1px solid var(--rule);
}
@media (max-width: 900px) { .systems-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .systems-grid { grid-template-columns: 1fr; } }
.sys-cell {
  background: var(--bg);
  padding: 28px 26px;
  min-height: 220px;
  display: flex; flex-direction: column; justify-content: space-between;
  position: relative;
  transition: background var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
  cursor: pointer;
  text-align: left;
  border: 0;
  color: inherit;
  font: inherit;
  width: 100%;
}
.sys-cell:hover { background: var(--bg-2); }
.sys-cell:hover .sys-glyph { color: var(--accent); }
.sys-cell.active { background: var(--bg-2); outline: 1px solid var(--accent); outline-offset: -1px; }
.sys-cell.active .sys-glyph { color: var(--accent); }
.sys-cell-head { display: flex; align-items: center; justify-content: space-between; }
.sys-cell .sys-id {
  font-family: var(--ff-mono); font-size: 10px; color: var(--fg-3);
  letter-spacing: .08em;
}
.sys-band { width: 6px; height: 6px; border-radius: 50%; background: var(--fg-3); }
.sys-band-ai { background: var(--accent); }
.sys-band-ciam { background: oklch(0.7 0.12 260); }
.sys-band-plat { background: oklch(0.7 0.12 180); }
.sys-glyph {
  font-family: var(--ff-mono);
  font-size: 14px;
  color: var(--fg-2);
  letter-spacing: .02em;
  line-height: 1.5;
  margin: 14px 0;
  white-space: pre;
  transition: color var(--dur-base) var(--ease-out);
}
.sys-name {
  font-family: var(--ff-serif);
  font-size: 22px;
  letter-spacing: -0.01em;
  color: var(--fg);
}
.sys-note {
  font-size: 13px; color: var(--fg-2); margin-top: 8px; line-height: 1.5;
}
.sys-more {
  margin-top: 12px;
  font-family: var(--ff-mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--accent);
  /* Visible at rest. At opacity 0 this was a hover-only affordance, so a touch
     visitor had no way to know the card opened until they had already tapped. */
  opacity: .6; transition: opacity var(--dur-base) var(--ease-out);
}
.sys-cell:hover .sys-more,
.sys-cell:focus-visible .sys-more,
.sys-cell.active .sys-more { opacity: 1; }
.sys-loading {
  grid-column: 1 / -1;
  padding: 48px 26px;
  text-align: center;
  font-family: var(--ff-mono); font-size: 12px; color: var(--fg-3);
}

.sys-detail {
  margin-top: 1px;
  background: var(--bg-2);
  border: 1px solid var(--rule);
  border-top: 0;
  padding: 0;
  max-height: 0;
  overflow: hidden;
  transition: max-height var(--dur-slow) var(--ease-out), padding var(--dur-slow) var(--ease-out);
  position: relative;
  scroll-margin-top: calc(var(--chrome-h) + 12px); /* clear the fixed header when scrollIntoView focuses it */
}
.sys-detail.open {
  max-height: 2000px;
  padding: 32px 36px 36px;
}
.sys-detail-close {
  position: absolute; top: 14px; right: 18px;
  background: transparent; border: 1px solid var(--rule-2);
  color: var(--fg-2); width: 28px; height: 28px; border-radius: 50%;
  font-size: 18px; line-height: 1; cursor: pointer;
  transition: background-color var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}
.sys-detail-close:hover { color: var(--fg); border-color: var(--fg-2); }
.sys-detail-head { margin-bottom: 18px; }
.sys-detail-id {
  font-family: var(--ff-mono); font-size: 10px; color: var(--fg-3);
  letter-spacing: .12em; text-transform: uppercase;
}
.sys-detail-name {
  font-family: var(--ff-serif);
  font-size: 32px;
  letter-spacing: -0.01em;
  margin: 6px 0 10px;
  color: var(--fg);
}
.sys-detail-tags { display: flex; flex-wrap: wrap; gap: 8px; }
.sys-detail-tags span {
  font-family: var(--ff-mono); font-size: 10px; letter-spacing: .08em;
  color: var(--fg-3); padding: 3px 8px;
  border: 1px solid var(--rule-2); border-radius: 3px;
}
.sys-detail-glyph {
  font-family: var(--ff-mono); font-size: 14px; color: var(--accent);
  white-space: pre; margin: 20px 0;
  padding: 16px 20px; background: var(--bg);
  border: 1px solid var(--rule); border-radius: 6px;
  display: inline-block;
}
.sys-detail-body {
  max-width: 780px;
  color: var(--fg-2);
  font-size: 16px;
  line-height: 1.65;
  text-wrap: pretty;
}
.sys-detail-body h3 {
  font-family: var(--ff-mono); font-size: 11px;
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--accent); margin: 28px 0 10px; font-weight: 500;
}
.sys-detail-body h3:first-child { margin-top: 0; }
.sys-detail-body p { margin: 12px 0; }
.sys-detail-body strong { color: var(--fg); font-weight: 500; }
.sys-detail-body code {
  font-family: var(--ff-mono); font-size: .92em;
  background: var(--bg); padding: 1px 6px; border-radius: 3px;
  border: 1px solid var(--rule);
}
.sys-detail-body ul { margin: 12px 0; padding-left: 20px; }
.sys-detail-body li { margin: 6px 0; }

/* ------- Library (talks + writing) ------- */
.library-list {
  display: block;
  margin-top: 40px;
  border-top: 1px solid var(--rule-2);
}
/* A dated index, not a third card grid.
   Impact, Systems and Library were the same object three times over — the same
   1px-gutter grid, the same bordered cells, the same hover. The library is the
   one of the three whose content is genuinely chronological, so it reads as a
   list: date in the left margin, title, note, links. Different shape, less
   drawn furniture, and the reverse-chronological order finally shows. */
.lib-row {
  display: grid;
  /* Meta in a left margin, everything readable in one measured column. */
  grid-template-columns: 210px minmax(0, 1fr);
  gap: 10px 32px;
  padding: 38px 4px;
  /* --rule is nearly invisible on ink (1.3:1), so at --rule the entries ran
     together into one wall of text. Separators here are load-bearing. */
  border-bottom: 1px solid var(--rule-2);
}
.lib-row:last-child { border-bottom: 0; }
.lib-meta {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 10px; padding-top: 7px;
}
/* No stretched link. The whole row used to be one click target via
   .lib-hit::after, so a click anywhere in the (mostly empty) full-width band
   navigated. Only the real links are clickable now — which meant promoting
   the CTA from a <span> to an <a>, since it had never been one. */
.lib-body { max-width: 62ch; }
.lib-when {
  font-family: var(--ff-mono); font-size: 11px; color: var(--fg-3);
  letter-spacing: .06em; line-height: 1.7;
}
.lib-kind {
  font-family: var(--ff-mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--fg-3);
  border: 1px solid var(--rule-2); border-radius: 3px;
  padding: 2px 8px; white-space: nowrap;
}
.lib-kind-talk { color: var(--accent); border-color: color-mix(in oklch, var(--accent) 45%, transparent); }
.lib-title {
  font-weight: 400;
  font-size: clamp(21px, 2.2vw, 27px); line-height: 1.3; letter-spacing: -0.01em;
  margin: 0;
}
.lib-title a {
  color: var(--fg); text-decoration: none;
  transition: color var(--dur-base) var(--ease-out);
}
.lib-title a:hover { color: var(--accent); }
.lib-short {
  font-size: 14.5px; color: var(--fg-2); line-height: 1.65;
  margin: 10px 0 0;
}
.lib-foot {
  margin-top: 18px;
  display: flex; align-items: center; gap: 20px; flex-wrap: wrap;
}
.lib-cta {
  font-family: var(--ff-mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--accent);
  text-decoration: none;
}
.lib-cta .arr { margin-left: 6px; display: inline-block; transition: transform var(--dur-base) var(--ease-out); }
.lib-cta:hover .arr { transform: translateX(3px); }
.lib-alt {
  font-family: var(--ff-mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--fg-3);
  text-decoration: none; border-bottom: 1px solid var(--rule-2);
  padding-bottom: 2px;
  transition: color var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out);
}
.lib-alt:hover { color: var(--fg); border-color: var(--accent); }
@media (max-width: 820px) {
  .lib-row { grid-template-columns: minmax(0, 1fr); gap: 14px; padding: 30px 4px; }
  .lib-meta { flex-direction: row; align-items: center; flex-wrap: wrap; gap: 12px; padding-top: 0; }
}
.lib-loading {
  padding: 48px 4px; text-align: center;
  font-family: var(--ff-mono); font-size: 12px; color: var(--fg-3);
}

/* ------- Skills ------- */
.skills-grid {
  display: grid;
  /* Was repeat(2, 1fr), which did nothing: every .skill-row sets
     grid-column: 1 / -1, so the track list was never used. */
  grid-template-columns: 1fr;
  gap: 0;
  margin-top: 48px;
  border-top: 1px solid var(--rule);
}
@media (max-width: 700px) { .skills-grid { grid-template-columns: 1fr; } }
.skill-row {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: 24px;
  padding: 22px 0;
  border-bottom: 1px solid var(--rule-soft);
  grid-column: 1 / -1;
}
@media (max-width: 700px) { .skill-row { grid-template-columns: 1fr; gap: 8px; } }
.skill-head {
  font-family: var(--ff-mono);
  font-size: 12px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--fg);
  padding-top: 3px;
}
.skill-head::before { content: "# "; color: var(--accent); }
.skill-list { color: var(--fg-2); font-size: 15px; line-height: 1.7; text-wrap: pretty; }

/* ------- Speaking ------- */
.speaking-wrap {
  margin-top: 48px;
  display: grid; grid-template-columns: 1fr 1fr; gap: 60px;
}
@media (max-width: 900px) { .speaking-wrap { grid-template-columns: 1fr; gap: 40px; } }
.speaking-wrap .topics { list-style: none; padding: 0; margin: 0; }
.speaking-wrap .topics li {
  padding: 16px 0;
  border-bottom: 1px dashed var(--rule);
  font-family: var(--ff-serif);
  font-size: 22px;
  letter-spacing: -0.01em;
  color: var(--fg);
  display: flex; gap: 18px; align-items: baseline;
}
/* Classes for what used to be inline style="" on the markup — and duplicated
   verbatim inside app.js's venues template, so a token change meant editing
   two places. */
.topics-label,
.venues-label {
  font-family: var(--ff-mono); font-size: 11px; color: var(--fg-3);
  letter-spacing: .08em; text-transform: uppercase; margin-bottom: 14px;
}
.venues-card {
  border: 1px solid var(--rule);
  background: var(--bg-2);
  padding: 28px;
}
.venues-label { color: var(--accent); font-size: 10px; letter-spacing: .1em; margin-bottom: 0; }
.venues-list {
  font-family: var(--ff-display, var(--ff-serif));
  font-size: 26px; line-height: 1.3; margin-top: 14px; color: var(--fg);
}
.venues-note { margin-top: 20px; font-size: 14px; color: var(--fg-2); }
.venues-cta { margin-top: 24px; }

/* ------- Chat ------- */
.chat-wrap {
  margin-top: 48px;
  border: 1px solid var(--rule);
  background: var(--bg-2);
  border-radius: 10px;
  overflow: hidden;
}
.chat-head {
  display: flex; justify-content: space-between; align-items: center;
  padding: 12px 18px;
  border-bottom: 1px solid var(--rule);
  font-family: var(--ff-mono); font-size: 11px; color: var(--fg-3);
  letter-spacing: .05em;
}
.chat-head .online { color: var(--accent); }
.chat-head .online::before { content: "●"; margin-right: 6px; animation: pulse 1.6s infinite; }
.chat-stream {
  min-height: 260px; max-height: 440px; overflow-y: auto;
  padding: 22px 24px;
  display: flex; flex-direction: column; gap: 14px;
  scrollbar-width: thin;
}
.msg {
  max-width: 80%;
  font-size: 15px; line-height: 1.55;
}
.msg.user {
  align-self: flex-end;
  background: var(--accent); color: var(--accent-ink);
  padding: 10px 14px; border-radius: 12px 12px 2px 12px;
}
.msg.bot {
  align-self: flex-start;
  color: var(--fg);
  font-family: var(--ff-serif); font-size: 18px; line-height: 1.5;
  padding: 4px 0;
  max-width: 90%;
}
.msg.bot .label {
  display: block;
  font-family: var(--ff-mono); font-size: 10px; color: var(--accent);
  letter-spacing: .1em; text-transform: uppercase; margin-bottom: 6px;
}
.msg.bot.thinking { color: var(--fg-3); font-style: italic; }

/* The reply arrives as Markdown and is rendered to real elements by
   renderRich() in app.js — before that it was a text node, so asterisks and
   hyphens appeared literally on screen. These are the only tags it produces. */
.msg.bot p { margin: 0 0 10px; }
.msg.bot p:last-child { margin-bottom: 0; }
.msg.bot strong { font-weight: 600; color: var(--fg); }
.msg.bot em { font-style: italic; }
.msg.bot code {
  font-family: var(--ff-mono); font-size: .82em;
  background: var(--bg); border: 1px solid var(--rule);
  border-radius: 3px; padding: 1px 5px;
}
.msg.bot ul { margin: 0 0 10px; padding-left: 20px; }
.msg.bot ul:last-child { margin-bottom: 0; }
.msg.bot li { margin: 4px 0; }
.msg.bot li::marker { color: var(--accent); }

.chat-suggest {
  display: flex; flex-wrap: wrap; gap: 8px;
  padding: 0 24px 14px;
}
.chat-suggest button {
  padding: 6px 12px;
  border: 1px solid var(--rule-2);
  border-radius: 999px;
  font-family: var(--ff-mono); font-size: 11px;
  color: var(--fg-2);
  transition: background-color var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}
.chat-suggest button:hover { border-color: var(--accent); color: var(--accent); }

.chat-input {
  display: flex; gap: 0;
  border-top: 1px solid var(--rule);
  background: var(--bg-3);
}
.chat-input input {
  flex: 1; background: transparent; border: 0;
  color: var(--fg); font-family: var(--ff-mono); font-size: 14px;
  padding: 14px 18px;
  min-width: 0;
}
/* Was `outline: 0`, which removed the only focus indicator this field had.
   It sits flush inside the chat frame, so the ring goes inward. */
.chat-input input:focus-visible { outline-offset: -3px; }
.chat-input input::placeholder { color: var(--fg-3); }
.chat-input button {
  padding: 0 22px;
  font-family: var(--ff-mono); font-size: 12px; letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--accent-ink); background: var(--accent);
  transition: background var(--dur-base) var(--ease-out);
}
.chat-input button:hover { background: var(--accent-2); }
.chat-disclaimer {
  padding: 10px 18px;
  font-family: var(--ff-mono); font-size: 10px; color: var(--fg-3);
  letter-spacing: .04em;
  border-top: 1px solid var(--rule);
}

/* ------- Contact ------- */
.contact-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 60px;
  margin-top: 48px;
}
@media (max-width: 700px) { .contact-grid { grid-template-columns: 1fr; gap: 32px; } }
.contact-list { list-style: none; padding: 0; margin: 0; }
.contact-list li {
  display: grid; grid-template-columns: 120px 1fr auto;
  gap: 16px; align-items: baseline;
  padding: 18px 0;
  border-bottom: 1px solid var(--rule-soft);
}
.contact-list li[hidden] { display: none; } /* author display:grid would otherwise beat [hidden] */
.contact-list .k {
  font-family: var(--ff-mono); font-size: 11px; color: var(--fg-3);
  letter-spacing: .08em; text-transform: uppercase;
}
.contact-list .v {
  font-family: var(--ff-serif); font-size: 16px; color: var(--fg);
  letter-spacing: -0.01em;
  min-width: 0; overflow-wrap: anywhere;
}
.contact-list a.v:hover { color: var(--accent); }
.contact-list .v a { color: var(--fg); text-decoration: none; }
.contact-list .v a:hover { color: var(--accent); }
.contact-list .v a:hover .arr { color: var(--accent); transform: translate(3px,-3px); }
.contact-list .v .arr { margin-left: 6px; color: var(--fg-3); transition: transform var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out); display: inline-block; }
.contact-list a.v:hover .arr { color: var(--accent); transform: translate(3px,-3px); }
/* :not([hidden]) so the `hidden` attribute actually wins — an author `display`
   rule would otherwise override the UA [hidden]{display:none} and show it on load. */
.reveal-loading:not([hidden]) { display: block; margin-top: 10px; }
.reveal-spinner:not([hidden]) {
  display: inline-block; width: 12px; height: 12px; vertical-align: middle; margin-right: 8px;
  border: 2px solid var(--rule); border-top-color: var(--accent); border-radius: 50%;
  animation: reveal-spin .7s linear infinite;
}
@keyframes reveal-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .reveal-spinner { animation: none; } }
.reveal-status { font-size: 12px; color: var(--fg-3); font-family: var(--ff-mono); }
#ts-contact { margin-top: 8px; }

.contact-mono {
  font-family: var(--ff-mono);
  font-size: 12px;
  line-height: 1.7;
  color: var(--fg-2);
  padding: 24px;
  border: 1px solid var(--rule);
  background: var(--bg-2);
}
.contact-mono .c { color: var(--fg-3); }
.contact-mono .k { color: var(--accent); }
.contact-mono .s { color: var(--fg); }

/* ------- Footer ------- */
footer {
  padding: 40px var(--pad);
  border-top: 1px solid var(--rule);
  font-family: var(--ff-mono);
  font-size: 11px;
  color: var(--fg-3);
  display: flex; justify-content: space-between; gap: 20px; flex-wrap: wrap;
  max-width: var(--maxw); margin: 0 auto;
  letter-spacing: .04em;
}
/* pre-line so the \n in footer_last renders as a break. The i18n applier sets
   textContent, so a <br> would show as literal markup — and switching that
   path to innerHTML to gain one line break is not a trade worth making.
   660px because the first sentence measures 627px in en and 613px in pt-BR;
   at the old 600px cap it wrapped and left "passion." orphaned on its own
   line, which read as a third line rather than a deliberate break. */
footer .colophon { max-width: 660px; white-space: pre-line; }

/* ------- Tweaks panel ------- */
.tweaks {
  position: fixed; right: 20px; bottom: 20px; z-index: 100;
  width: 280px;
  background: var(--bg-2);
  border: 1px solid var(--rule-2);
  border-radius: 10px;
  box-shadow: 0 30px 60px -20px rgba(0,0,0,.5);
  font-family: var(--ff-mono);
  font-size: 12px;
  display: none;
  overflow: hidden;
}
.tweaks.show { display: block; }
.tweaks-head {
  padding: 10px 14px;
  border-bottom: 1px solid var(--rule);
  display: flex; justify-content: space-between; align-items: center;
  color: var(--fg-3);
  letter-spacing: .08em;
  text-transform: uppercase;
}
.tweaks-head .title { color: var(--fg); }
.tweaks-body { padding: 14px; display: flex; flex-direction: column; gap: 14px; }
.tw-row { display: flex; flex-direction: column; gap: 6px; }
.tw-row .lbl { color: var(--fg-3); letter-spacing: .04em; text-transform: uppercase; font-size: 10px; }
.tw-opts { display: flex; gap: 4px; flex-wrap: wrap; }
.tw-opts button {
  padding: 5px 9px; border: 1px solid var(--rule-2); border-radius: 4px;
  color: var(--fg-2); font-size: 11px;
}
.tw-opts button.on { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.tw-swatch { display: inline-block; width: 14px; height: 14px; border-radius: 3px; margin-right: 6px; vertical-align: -3px; }

.tw-opts-fonts { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; }
.tw-font-btn {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 6px 3px !important;
  line-height: 1;
}
.tw-font-prev { font-size: 18px; line-height: 1; color: var(--fg); }
.tw-font-btn.on .tw-font-prev { color: var(--accent-ink); }
.tw-font-lbl { font-size: 8px; letter-spacing: .04em; text-transform: uppercase; opacity: .75; }
.tw-toggle {
  display: flex; justify-content: space-between; align-items: center;
  color: var(--fg-2);
}
.tw-toggle input { accent-color: var(--accent); }

/* ------- Reduced motion -------
   This used to fire only from the tweaks panel's own toggle, so the four
   perpetual animations (brand dot, the two live pulses, the caret blink) and
   every transition kept running for visitors who had asked the OS to stop
   them. The same rule now answers the media query as well. */
body[data-reduced="true"] *, body[data-reduced="true"] *::before, body[data-reduced="true"] *::after {
  animation-duration: 0.001s !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.001s !important;
  scroll-behavior: auto !important;
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001s !important;
  }
  html { scroll-behavior: auto; }
}

/* Density */
body[data-density="compact"] section { padding: 64px var(--pad); }
body[data-density="roomy"] section { padding: 128px var(--pad); }

body[data-emphasis="space-gro"]  { --ff-emphasis: "Space Grotesk", sans-serif; --ff-emphasis-weight: 500; }
body[data-emphasis="instrument"] { --ff-emphasis: "Instrument Serif", Georgia, serif; --ff-emphasis-weight: 400; }
body[data-emphasis="fraunces"]   { --ff-emphasis: "Fraunces", Georgia, serif; --ff-emphasis-weight: 500; }
body[data-emphasis="playfair"]   { --ff-emphasis: "Playfair Display", Georgia, serif; --ff-emphasis-weight: 500; }
body[data-emphasis="inter"]      { --ff-emphasis: "Inter", sans-serif; --ff-emphasis-weight: 500; }
body[data-emphasis="jetbrains"]  { --ff-emphasis: "JetBrains Mono", monospace; --ff-emphasis-weight: 500; }

/* Display font swap — unified selector.
   .lib-title, .sys-detail-name and .msg.bot used to reference --ff-serif
   directly and so sat outside this list: with the shipped Space Grotesk
   default, a Systems CARD title was sans while its own DETAIL title was serif,
   and Library cards were serif beside sans Systems cards. Same object class,
   two faces, for no reason a reader could infer. */
.display-target,
.hero-name,
.impact-value,
h2,
.sys-name,
.sys-detail-name,
.lib-title,
.msg.bot,
.speaking-wrap .topics li,
.contact-list .v,
.arch-detail .dt-role { font-family: var(--ff-display, var(--ff-serif)); font-style: var(--ff-display-style, normal); font-weight: var(--ff-display-weight, 400); letter-spacing: var(--ff-display-tracking, -0.02em); }

body[data-display="instrument"] { --ff-display: "Instrument Serif", Georgia, serif; --ff-display-style: italic; --ff-display-weight: 400; --ff-display-tracking: -0.02em; }
body[data-display="fraunces"]  { --ff-display: "Fraunces", Georgia, serif; --ff-display-style: normal; --ff-display-weight: 600; --ff-display-tracking: -0.03em; }
body[data-display="dm-serif"]  { --ff-display: "DM Serif Display", Georgia, serif; --ff-display-style: normal; --ff-display-weight: 400; --ff-display-tracking: -0.02em; }
body[data-display="playfair"]  { --ff-display: "Playfair Display", Georgia, serif; --ff-display-style: italic; --ff-display-weight: 500; --ff-display-tracking: -0.015em; }
body[data-display="bodoni"]    { --ff-display: "Bodoni Moda", Georgia, serif; --ff-display-style: normal; --ff-display-weight: 500; --ff-display-tracking: -0.015em; }
body[data-display="syne"]      { --ff-display: "Syne", "Inter", sans-serif; --ff-display-style: normal; --ff-display-weight: 700; --ff-display-tracking: -0.02em; }
body[data-display="space-gro"] { --ff-display: "Space Grotesk", "Inter", sans-serif; --ff-display-style: normal; --ff-display-weight: 500; --ff-display-tracking: -0.02em; }
body[data-display="unbounded"] { --ff-display: "Unbounded", "Inter", sans-serif; --ff-display-style: normal; --ff-display-weight: 500; --ff-display-tracking: -0.02em; }
body[data-display="major-mono"]{ --ff-display: "Major Mono Display", ui-monospace, monospace; --ff-display-style: normal; --ff-display-weight: 400; --ff-display-tracking: 0; }
body[data-display="jetbrains"] { --ff-display: "JetBrains Mono", ui-monospace, monospace; --ff-display-style: normal; --ff-display-weight: 500; --ff-display-tracking: -0.02em; }

/* Architecture SVG */
.arch-node {
  cursor: pointer;
}
.arch-node rect {
  fill: var(--bg-3);
  stroke: var(--rule-2);
  stroke-width: 1;
  transition: stroke var(--dur-base) var(--ease-out), fill var(--dur-base) var(--ease-out);
}
.arch-node .role { fill: var(--fg); font-family: var(--ff-mono); font-size: 11px; font-weight: 500; letter-spacing: .02em; }
.arch-node .co { fill: var(--fg-3); font-family: var(--ff-mono); font-size: 9px; letter-spacing: .04em; }
.arch-node .when { fill: var(--fg-3); font-family: var(--ff-mono); font-size: 9px; }
.arch-node.active rect { stroke: var(--accent); stroke-width: 1.5; fill: color-mix(in oklch, var(--accent) 10%, var(--bg-2)); }
.arch-node.active .role { fill: var(--accent); }
.arch-node:hover rect { stroke: var(--fg-2); }
.arch-edge { stroke: var(--rule-2); stroke-width: 1; fill: none; transition: stroke var(--dur-base) var(--ease-out); }
.arch-edge.active { stroke: var(--accent); stroke-width: 1.5; }
.arch-tick { fill: var(--fg-3); font-family: var(--ff-mono); font-size: 9px; letter-spacing: .04em; }
.arch-axis { stroke: var(--rule); stroke-width: 1; stroke-dasharray: 2 3; }

/* One orchestrated entrance, not nine.
   Every section used to fade up on intersection, so the page never settled.
   The entrance now belongs to the impact grid alone — the one place a reveal
   does work, because the six figures land in sequence — and everything else
   is simply there when you reach it. */
.impact-cell.fade-in { transition-delay: 0ms; }
.impact-cell.fade-in:nth-child(2) { transition-delay: 60ms; }
.impact-cell.fade-in:nth-child(3) { transition-delay: 120ms; }
.impact-cell.fade-in:nth-child(4) { transition-delay: 180ms; }
.impact-cell.fade-in:nth-child(5) { transition-delay: 240ms; }
.impact-cell.fade-in:nth-child(6) { transition-delay: 300ms; }

/* Fade-in — opt-IN, never opt-out.
   Sections used to start at `opacity: 0` unconditionally, so anything that
   stopped app.js before the IntersectionObserver ran (a throw, a blocked
   script, a failed load) left every section below the hero invisible — the
   page still scrolled ~9,000px of nothing. The hidden state now lives behind
   `html.js`, which an inline script in <head> sets only when JS is actually
   executing, and index.html arms a timeout that reveals everything if the
   observer never arrives. Failure degrades to "no animation", not "no site". */
.fade-in { transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out); }
html.js .fade-in { opacity: 0; transform: translateY(16px); }
html.js .fade-in.in { opacity: 1; transform: translateY(0); }

/* Scrollbar */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--rule-2); border-radius: 6px; }
::-webkit-scrollbar-thumb:hover { background: var(--fg-3); }
