/* ============================================================
   PhD personal website — single stylesheet (Editorial-Sage).
   Editorial layout: serif display headings, sans-serif accents.
   Light is the default; a dark variant kicks in automatically via
   prefers-color-scheme. Everything that differs between the two
   modes is a CSS variable, so the rules below are written once.

   Light palette: green #1f4a2c, amber #C77A04, orange #C2540A,
   brown #7a430f, page #e1f2e7, cards #fff.
   ============================================================ */
:root{
  --green:#1f4a2c; --amber:#C77A04; --orange:#C2540A; --brown:#7a430f;
  --fg:#15291c; --muted:#3f5746; --bg:#e1f2e7; --card:#ffffff;
  --accent:var(--brown); --border:#bcd6c6; --max-width:780px;
  /* mode-specific surfaces / on-colour text / shadows */
  --header-bg:rgba(225,242,231,.92); --on-accent:#ffffff;
  --shadow:rgba(31,51,38,.18); --scroll-shadow:rgba(21,41,28,.18);
  --serif:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,"Times New Roman",serif;
  --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
  /* ---- Font pairing (self-hosted, see fonts.css) -------------
     Change these three lines to re-skin the whole site. The
     trailing system fallback keeps text readable before the
     webfont loads / if it fails. Bundled families:
     Fraunces, Playfair Display, Lora, EB Garamond (display);
     Newsreader, Source Serif 4, Lora, EB Garamond (body).      */
  --font-display:"Newsreader",var(--serif);
  --font-body:"Inter",var(--sans);
  --font-ui:"Inter",var(--sans);
}

/* Dark variant — editorial palette retuned for WCAG AA on a deep-green page.
   Applied when the theme script resolves the choice to dark (html[data-theme=
   "dark"]); the @media block is the no-JS fallback, used only while no explicit
   choice is set (html:not([data-theme])). The var list is identical in both. */
html[data-theme="dark"]{
  --green:#74c897; --amber:#e0a23c; --orange:#ef824a; --brown:#d8a05a;
  --fg:#e7efe9; --muted:#9fb6a6; --bg:#11201a; --card:#22392b;
  --accent:var(--amber); --border:#3a5743;
  --header-bg:rgba(17,32,26,.92); --on-accent:#11201a;
  --shadow:rgba(0,0,0,.45); --scroll-shadow:rgba(0,0,0,.4);
}
@media(prefers-color-scheme:dark){
  html:not([data-theme]){
    --green:#74c897; --amber:#e0a23c; --orange:#ef824a; --brown:#d8a05a;
    --fg:#e7efe9; --muted:#9fb6a6; --bg:#11201a; --card:#22392b;
    --accent:var(--amber); --border:#3a5743;
    --header-bg:rgba(17,32,26,.92); --on-accent:#11201a;
    --shadow:rgba(0,0,0,.45); --scroll-shadow:rgba(0,0,0,.4);
  }
}

*{box-sizing:border-box}
/* scroll-padding-top offsets every in-page anchor jump by the sticky-header
   height so section titles aren't hidden under the bar. Desktop header is a
   single nav row (~3.5rem); the mobile nav stacks brand over a scrollable link
   row (taller), so the offset grows below 560px. */
html{scroll-behavior:smooth;scroll-padding-top:4.75rem}
@media(max-width:560px){html{scroll-padding-top:6.75rem}}
body{margin:0;background:var(--bg);color:var(--fg);line-height:1.65;font-family:var(--font-body)}
.site-header{position:sticky;top:0;background:var(--header-bg);backdrop-filter:blur(6px);border-bottom:1px solid var(--border);z-index:10}
.nav{max-width:var(--max-width);margin:0 auto;padding:1rem;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:.5rem}
.nav-brand{font-family:var(--font-display);font-weight:700;font-size:1.18rem;text-decoration:none;color:var(--green)}
.nav-links{list-style:none;display:flex;gap:1.3rem;margin:0;padding:0;font-family:var(--font-ui);font-size:.84rem;text-transform:uppercase;letter-spacing:.08em;font-weight:600}
.nav-links a{text-decoration:none;color:var(--muted)}
.nav-links a:hover{color:var(--orange)}
/* Right-hand nav cluster: the links plus the theme toggle, kept together so the
   toggle sits at the page edge on desktop and at the row end on mobile. */
.nav-right{display:flex;align-items:center;gap:1.3rem}
/* Minimalist theme toggle: a borderless circular icon button. It shows a moon in
   light mode (tap to go dark) and a sun in dark mode. Colour/scale only on hover
   + press, so it stays quiet until reached for. */
.theme-toggle{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;
  width:2.1rem;height:2.1rem;padding:0;border:1px solid transparent;border-radius:50%;
  background:none;color:var(--muted);cursor:pointer;-webkit-appearance:none;appearance:none;
  transition:color .2s ease,border-color .2s ease,transform .2s ease}
.theme-toggle:hover{color:var(--orange);border-color:var(--border)}
.theme-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.theme-toggle:active{transform:scale(.9)}
.theme-icon{width:1.15rem;height:1.15rem;display:block;fill:none;stroke:currentColor;
  stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.theme-toggle .icon-sun{display:none}
html[data-theme="dark"] .icon-moon{display:none}
html[data-theme="dark"] .icon-sun{display:block}
@media(prefers-color-scheme:dark){
  html:not([data-theme]) .icon-moon{display:none}
  html:not([data-theme]) .icon-sun{display:block}
}
main{max-width:var(--max-width);margin:0 auto;padding:0 1rem}
.section{padding:3.5rem 0 4.5rem;position:relative;border-bottom:none}
.section:last-of-type{padding-bottom:3.5rem}
/* End-of-section divider: amber diamond centred on a fading hairline. */
.section:not(:last-of-type)::after{content:"";position:absolute;left:0;right:0;bottom:2rem;height:1px;background:linear-gradient(90deg,transparent,var(--border) 20%,var(--border) 80%,transparent)}
.section:not(:last-of-type)::before{content:"\25C6";position:absolute;left:50%;bottom:1.35rem;transform:translateX(-50%);background:var(--bg);padding:0 .7rem;color:var(--amber);font-size:.72rem;z-index:1}
h1{font-family:var(--font-display);font-size:3.2rem;line-height:1.08;margin:0 0 .4rem;letter-spacing:-.015em;color:var(--green)}
.nickname{color:var(--orange);font-style:italic;font-weight:400}
h2{font-family:var(--font-ui);font-size:1.35rem;text-transform:uppercase;letter-spacing:.14em;color:var(--accent);margin:0 0 1.6rem;font-weight:700}
h3{font-family:var(--font-display);font-size:1.35rem;margin:1.6rem 0 .5rem;letter-spacing:-.01em;color:var(--green)}
a{color:var(--accent)}
a:hover{color:var(--orange)}
main a:hover,.tagline a:hover{text-decoration:underline}
p{font-size:1.08rem}
/* Bio standfirst: the opening claim set larger in the display serif, like a
   journal lead. Lighter weight + tighter leading keep it calm, not shouty. */
.bio-lead{font-family:var(--font-display);font-size:clamp(1.4rem,3.2vw,1.85rem);
  line-height:1.32;font-weight:400;letter-spacing:-.01em;color:var(--green);
  margin:0 0 1.4rem;max-width:32em}
.bio-lead em{font-style:italic;color:var(--accent)}
/* Body paragraphs of the bio sit a touch narrower for a comfortable measure. */
.about-bio:not(.bio-lead){max-width:40em;margin:0 0 1rem}
/* Hero — the whole first screen: a centred column of avatar → identity → arc →
   socials → skip cue. Forced to one viewport under the arc engine on roomy screens
   (see the .js-arc rule in the Motion block); otherwise it flows at natural height. */
.hero{display:flex;flex-direction:column;align-items:center;text-align:center;gap:.8rem;margin-bottom:1.4rem}
.hero-intro{display:flex;flex-direction:column;align-items:center;gap:.05rem;max-width:42rem}
.hero-greeting{font-family:var(--font-ui);text-transform:uppercase;letter-spacing:.14em;font-size:.8rem;font-weight:700;color:var(--muted);margin:0}
.avatar{width:92px;height:92px;border-radius:50%;object-fit:cover;border:4px solid var(--card);box-shadow:0 6px 20px var(--shadow)}
.tagline{font-family:var(--font-ui);color:var(--muted);font-size:.98rem;margin:.2rem 0}
.tagline a{color:var(--green);font-weight:600}
.social-icons{list-style:none;padding:0;margin:.7rem 0 0;display:flex;gap:.7rem}
.social-icons a{display:inline-flex;width:36px;height:36px;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:50%;color:var(--green);background:var(--card)}
.social-icons a:hover{color:var(--on-accent);background:linear-gradient(135deg,var(--amber),var(--orange));border-color:transparent}
.social-icons svg{width:18px;height:18px;fill:currentColor}
.interests{margin-top:1.5rem}
.interests-label{font-family:var(--font-ui);display:block;font-size:.74rem;font-weight:700;text-transform:uppercase;letter-spacing:.12em;color:var(--muted);margin-bottom:.6rem}
.chips{list-style:none;padding:0;margin:0;display:flex;flex-wrap:wrap;gap:.5rem;font-family:var(--font-ui)}
.chips li{font-size:.86rem;font-weight:600;padding:.32rem .8rem;border:1px solid var(--green);border-radius:2px;color:var(--green);background:var(--card)}
.news-list,.pub-list,.timeline,.links,.services{list-style:none;padding:0}
.news{display:grid;grid-template-columns:70px 116px 1fr;gap:.8rem;align-items:baseline;padding:.6rem 0;border-bottom:1px solid var(--border)}
.news-date{color:var(--muted);font-style:italic}
.news-text{font-family:var(--font-ui);font-size:.95rem}
.tag{font-family:var(--font-ui);display:inline-block;font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:.14rem .55rem;border-radius:2px;color:var(--on-accent)}
.tag-full,.tag-conference{background:var(--green)}
.tag-paper{background:var(--brown)}
.tag-preprint{background:var(--orange)}
.tag-talk{background:var(--amber);color:#3a2a05}
.tag-award{background:var(--orange)}
.tag-part,.tag-news{background:var(--card);color:var(--green);border:1px solid var(--border)}
.pub{padding:1rem 0;border-bottom:1px solid var(--border)}
.news:last-child,.pub:last-child{border-bottom:none}
.pub-title{font-family:var(--font-display);font-weight:700;font-size:1.16rem;display:block;color:var(--green)}
.pub-authors{font-family:var(--font-ui);font-size:.95rem;display:block}
.pub-venue{color:var(--muted);font-style:italic;display:block}
.pub-links{font-family:var(--font-ui);font-size:.86rem;margin-top:.2rem}
.timeline li{display:flex;gap:1rem;padding:.5rem 0}
.timeline .when{flex:0 0 110px;color:var(--brown);font-style:italic;font-size:.95rem}
.timeline .what{font-family:var(--font-ui);font-size:.98rem}
.timeline .what .tag{margin-left:.6rem}
.button{font-family:var(--font-ui);display:inline-block;padding:.6rem 1.2rem;background:var(--green);color:var(--on-accent);text-decoration:none;border-radius:3px;font-weight:600}
.button:hover{color:var(--on-accent);background:var(--accent)}
.services{font-family:var(--font-ui);list-style:disc;padding-left:1.4rem;margin:.4rem 0}
.services li{padding:.18rem 0}
.services li::marker{color:var(--green)}
.links{font-family:var(--font-ui);display:flex;flex-wrap:wrap;gap:.7rem 1.5rem}
.site-footer{font-family:var(--font-ui);max-width:var(--max-width);margin:0 auto;padding:2rem 1rem 3rem;color:var(--muted)}
.site-footer p{font-size:.75rem;font-style:italic;margin:0}
@media(max-width:480px){h1{font-size:2.5rem}.social-icons{justify-content:center}.news{grid-template-columns:1fr;gap:.2rem}}
/* Mobile nav: stack brand over a full-width, horizontally scrollable link row.
   Styled scrollbar (Android/desktop) + CSS scroll-shadow edge fade (iOS, where the
   overlay scrollbar is hidden) both signal there is more to scroll. */
@media(max-width:560px){
  .nav{flex-direction:column;align-items:stretch;gap:.45rem}
  /* Links scroll inside the row; the toggle stays pinned at the row's end.
     width:100%+min-width:0 keep .nav-right from overflowing the viewport so the
     overflow lands on .nav-links (its scroll container), not the page. */
  .nav-right{gap:.6rem;width:100%;min-width:0}
  .nav-links{flex:1 1 auto;min-width:0;flex-wrap:nowrap;overflow-x:auto;max-width:100%;-webkit-overflow-scrolling:touch;padding-bottom:.4rem;scrollbar-width:thin;scrollbar-color:var(--green) transparent;
    background-image:linear-gradient(90deg,var(--bg) 30%,transparent),linear-gradient(90deg,transparent,var(--bg) 70%),radial-gradient(farthest-side at 0 50%,var(--scroll-shadow),transparent),radial-gradient(farthest-side at 100% 50%,var(--scroll-shadow),transparent);
    background-position:left center,right center,left center,right center;background-repeat:no-repeat;background-size:30px 100%,30px 100%,16px 100%,16px 100%;background-attachment:local,local,scroll,scroll}
  .nav-links li{flex:0 0 auto}
  .nav-links::-webkit-scrollbar{height:6px}
  .nav-links::-webkit-scrollbar-thumb{background:var(--green);border-radius:3px}
  .nav-links::-webkit-scrollbar-track{background:var(--border);border-radius:3px}
}

/* ============================================================
   Motion & flair — progressive enhancement.

   Hover transitions apply to everyone. On-scroll reveals are
   gated behind the `.js-reveal` class, which the inline script
   in index.html adds to <html> ONLY when JS runs AND the user
   has not asked for reduced motion. So:
     - no JS            -> nothing is hidden, content shows as-is
     - reduced motion   -> reveals + transitions are switched off
     - JS + full motion -> elements fade/slide in once on scroll
   ============================================================ */

/* ---- Hover transitions (subtle, ~200ms) -------------------- */
a,.nav-links a,.button,.chips li,.social-icons a,.tag{
  transition:color .2s ease,background-color .2s ease,border-color .2s ease,
             transform .2s ease,box-shadow .2s ease}
.avatar{transition:transform .4s ease,box-shadow .4s ease}
.avatar:hover{transform:scale(1.03)}
.social-icons a:hover{transform:translateY(-2px)}
.chips li:hover{transform:translateY(-2px);border-color:var(--orange);color:var(--orange)}
.button:hover{transform:translateY(-2px);box-shadow:0 6px 16px var(--shadow)}

/* Animated underline on nav links (grows from the left). */
.nav-links a{position:relative}
.nav-links a::after{content:"";position:absolute;left:0;bottom:-3px;height:2px;width:0;
  background:var(--orange);transition:width .25s ease}
.nav-links a:hover::after{width:100%}

/* ---- On-scroll reveal -------------------------------------- */
.js-reveal .reveal{opacity:0;transform:translateY(18px);
  transition:opacity .6s ease,transform .6s ease;transition-delay:var(--reveal-delay,0ms)}
.js-reveal .reveal.is-visible{opacity:1;transform:none}

/* ---- Hero: animated intro (screen 1) ----------------------- */
/* Under the engine the hero is one full screen: the visitor lands on the typed
   intro, then the page auto-scrolls down to the arc (screen 2). Pinning the hero to
   one viewport keeps the arc below the fold so that scroll reads as a reveal. The
   intro words are wrapped in .arc-word (hidden until .is-on — see the arc-word rule
   below) and revealed one at a time by the engine. No-JS / reduced motion: natural
   flow, static text, no auto-scroll, no skip button. svh avoids the URL-bar jump. */
.js-arc .hero{min-height:100vh;min-height:100svh;justify-content:center;
  padding-top:4rem;margin-bottom:0}
.intro-line{margin:.18rem 0}
.hero-intro{gap:.3rem}
.hero-intro p.intro-line{font-family:var(--font-body);font-size:clamp(1.1rem,2.7vw,1.4rem);
  color:var(--muted);max-width:34rem;line-height:1.5;margin-left:auto;margin-right:auto}
/* No underline on intro links: the line is split into per-word .arc-word spans, but
   the spaces between them stay as plain text nodes — an underline would show on those
   (and so on the link) before the words type in. Colour + weight mark the link; the
   underline returns on hover. */
.hero-intro .intro-line a{color:var(--green);font-weight:600;text-decoration:none}
.hero-intro .intro-line a:hover{text-decoration:underline}

/* The avatar fades in immediately; the socials are held until the engine adds
   .is-shown (once the intro has finished typing) so they don't sit under half-typed
   lines. The skip button is shown from the start so the visitor can always bail. */
.js-arc .avatar{opacity:0;animation:hero-rise .6s ease .05s both}
.js-arc .social-icons{opacity:0}
.js-arc .social-icons.is-shown{animation:hero-rise .6s ease both}
@keyframes hero-rise{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}

.skip-anim{display:none}
.js-arc .skip-anim{display:inline-flex;align-items:center;gap:.3rem;margin-top:.5rem;
  padding:.32rem .85rem;border:1px solid var(--border);border-radius:999px;background:none;
  cursor:pointer;font-family:var(--font-ui);font-size:.72rem;font-weight:700;
  text-transform:uppercase;letter-spacing:.06em;color:var(--muted);
  opacity:0;animation:hero-rise .6s ease .3s both}
.skip-anim:hover{color:var(--accent);border-color:var(--accent)}

/* ---- Animated dependency parse (bio opener) ----------------
   A showcase sentence with labelled dependency arcs that draw
   themselves in, echoing the parse figures from the thesis.
   The SVG overlay is built + measured by JS; without JS only
   the plain sentence shows. See buildDepParse() in index.html. */
.depparse{position:relative;margin:.2rem 0 1.5rem;overflow-x:auto;overflow-y:hidden}
.depparse .sentence{display:flex;flex-wrap:nowrap;gap:.42em;align-items:flex-end;
  font-family:var(--font-display);font-size:clamp(1.35rem,4.5vw,1.95rem);
  line-height:1.25;color:var(--green);width:max-content;max-width:100%}
.depparse .w{position:relative;white-space:nowrap}
.dep-svg{position:absolute;left:0;top:0;pointer-events:none;overflow:visible}
.dep-arc{fill:none;stroke:var(--brown);stroke-width:1.6}
.dep-head{fill:var(--brown)}
.dep-labelbox{fill:var(--card);stroke:var(--border);stroke-width:1}
.dep-label{fill:var(--accent);font-family:var(--font-ui);font-size:11px;font-weight:700;
  text-transform:uppercase;letter-spacing:.03em}
/* The draw-in (stroke reveal) + fades are driven by the Web Animations API
   in buildDepParse(); initial hidden state is set inline. Reduced motion is
   handled in JS (elements painted straight to their final state). */
.dep-scissors{fill:none;stroke:var(--orange);stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}

/* ---- Research-arc band (autoplay on view + controls) -------
   A hero that walks through the PhD's central question, then hands off to
   the static .about-bio below. The .arc-stage holds the figure; .arc-beats
   are the narrative lines; .arc-controls (dots + replay) drive playback.
   The whole band sits in normal page flow — no scroll hijack. The JS engine
   adds .js-arc to <html>, which crossfades one beat at a time and shows the
   controls; without it (no-JS or prefers-reduced-motion) the band shows its
   static figure, the beats read as a short intro paragraph, and the controls
   stay hidden. The real bio is always in the DOM regardless. See the arc
   engine in index.html and docs/research-arc-animation-plan.md. */
.research-arc{position:relative;margin:.6rem 0 1.6rem}

/* The figure stage. Bottom-aligned (justify-content:flex-end) and reserved at the
   *tallest* figure's height — that of beat 7, whose dependency parse unfurls below the
   glyph row — so the figure grows upward into the spare space above (which blends into
   the hero whitespace) and its bottom edge, the caption, the dots, and the Replay
   button below it NEVER move as beats change. (Previously the stage was sized to the
   beat-5 figure and grew ~140px at beat 7, shoving the whole lower stack down — which
   pushed the beat-7 caption off-screen.) The mobile override below matches the smaller
   mobile figure. */
.arc-stage{display:flex;flex-direction:column;align-items:center;justify-content:center;
  min-height:352px}
/* The figure stacks the glyph row above the concept chips (beat 5). */
.arc-figure{position:relative;display:flex;flex-direction:column;align-items:center;
  gap:1.1rem;width:100%;max-width:520px;text-align:center}
/* Beat 3: the two columns (LM, brain) sit side by side with the ≈ between. */
.arc-row{display:flex;flex-direction:row;align-items:flex-start;justify-content:center;
  gap:clamp(1rem,5vw,2.4rem)}
/* Each column is exactly the pattern width so the two RDMs sit equidistant from
   the ≈; the (wider) labels overflow symmetrically and don't shift the layout. */
.arc-col{display:flex;flex-direction:column;align-items:center;gap:.7rem;width:60px}

/* The glyph (reused across beats). */
.arc-glyph{position:relative;display:flex;flex-direction:column;align-items:center;
  gap:.45rem;color:var(--green)}
.arc-glyph svg{width:52px;height:52px;display:block}
.arc-glyph-label{font-family:var(--font-ui);font-size:.66rem;font-weight:700;
  text-transform:uppercase;letter-spacing:.1em;color:var(--muted);white-space:nowrap}

/* Beat 5: a magnifier badge on the LM glyph (we look inside the model). */
.arc-magnifier{position:absolute;top:-8px;right:-14px;width:22px;height:22px;
  display:flex;align-items:center;justify-content:center;border-radius:50%;
  background:var(--card);color:var(--accent);box-shadow:0 0 0 1.5px var(--accent)}
.arc-magnifier svg{width:13px;height:13px}

/* Beat 5: the concept chips that fan in below the model. */
.arc-concepts{display:flex;flex-wrap:wrap;justify-content:center;gap:.5rem}
.arc-concept{font-family:var(--font-ui);font-size:.8rem;font-weight:600;
  padding:.3rem .7rem;border:1px solid var(--accent);border-radius:999px;
  color:var(--accent);background:var(--card);white-space:nowrap}

/* Representation patterns: a 4×4 RDM under each glyph (warm/red heatmap). The
   diagonal is the same stimulus vs itself → maximal (v3, full red), identical
   across both. Off-diagonal cells are symmetric; the brain's differing pairs
   carry .d (a green ring) to underline "similar, not identical". */
.arc-pattern{display:grid;grid-template-columns:repeat(4,1fr);gap:3px;
  width:60px;height:60px}
.arc-cell{background:var(--orange);border-radius:1px}
.arc-cell.v0{opacity:.16}
.arc-cell.v1{opacity:.42}
.arc-cell.v2{opacity:.7}
.arc-cell.v3{opacity:1}
.arc-cell.d{box-shadow:0 0 0 1.5px var(--green)}

/* The ≈ relation, vertically centred on the pattern row (its height matches the
   patterns, pinned to the bottom of the columns where the patterns sit). */
/* Fixed width so swapping the inner content (≈ similar → ? → drives?) never
   reflows the row — the LM and brain glyphs hold their position from beat 3
   through beat 6. */
.arc-rel{align-self:flex-end;width:74px;height:60px;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:.15rem;color:var(--muted)}
.arc-approx{font-family:var(--font-display);font-size:1.5rem;line-height:1;color:var(--accent)}
.arc-rel-label{font-family:var(--font-ui);font-size:.58rem;font-weight:700;
  text-transform:uppercase;letter-spacing:.1em}

/* The predicted phrase inside beat 2 — reads as a generated token. */
.arc-pred{color:var(--accent);font-weight:600}

/* Narrative beat lines (static: stacked paragraph). */
.arc-beats{margin:.95rem 0 0;text-align:center}
.arc-beat{margin:.2rem 0;font-family:var(--font-body);color:var(--fg);
  font-size:1.08rem;line-height:1.6}

/* Controls hidden until JS un-hides them. */
.arc-controls{display:none}

/* ---- With the JS engine live (.js-arc) --------------------- */
/* Beats stack in one place and crossfade — one shown at a time. */
.js-arc .arc-beats{position:relative;display:grid;
  min-height:3.4em;place-items:center;margin-top:.85rem}
.js-arc .arc-beat{grid-area:1/1;margin:0;max-width:30em;
  font-size:clamp(1.05rem,2.6vw,1.3rem);
  opacity:0;transform:translateY(8px);
  transition:opacity .5s ease,transform .5s ease}
.js-arc .arc-beat.is-active{opacity:1;transform:none}

/* Word-by-word reveal (beat 2 onward): words are wrapped in .arc-word spans by
   the engine and held at opacity 0 — reserving their layout so the line never
   reflows — then faded in one at a time as .is-on is added. */
.js-arc .arc-word{opacity:0}
.js-arc .arc-word.is-on{animation:arc-fadein .24s ease both}

/* Figure elements start hidden; the cumulative data-seen list reveals them. */
.js-arc .arc-glyph{opacity:0;transform:translateY(8px);
  transition:opacity .55s ease,transform .55s ease}
.js-arc .arc-stage[data-seen~="1"] .arc-glyph{opacity:1;transform:none}

/* Beat 3 is staged in three (see BEATS_CFG in the arc engine):
   - "3"  arrives on the words "human brain": the brain column joins the LM, and
          both glyph labels appear to disambiguate the two;
   - "3r" arrives at the end of the line: the two RDM patterns pop in and the
          "≈ similar" relation lands between them, then the beat lingers.
   Brain column + ≈ are out of flow until revealed, so the lone LM stays centred. */
.js-arc .arc-col-brain,
.js-arc .arc-rel{display:none}
.js-arc .arc-stage[data-seen~="3"] .arc-col-brain{display:flex;animation:arc-fadein .55s ease both}
/* The relation slot is reserved (fixed width) the moment the brain joins at "3",
   so both columns settle into their final positions immediately; its ≈ + label
   then fade in at end-of-line "3r" without nudging the glyphs. */
.js-arc .arc-stage[data-seen~="3"] .arc-rel{display:flex}
.js-arc .arc-approx,
.js-arc .arc-rel-label{visibility:hidden}
.js-arc .arc-stage[data-seen~="3r"] .arc-approx{visibility:visible}
.js-arc .arc-stage[data-seen~="3r"] .arc-rel-label{visibility:visible;animation:arc-fadein .55s ease both}

/* Labels are display:none (no reserved space) until the brain joins at "3", so the
   caption hugs the lone glyph in beats 1–2 instead of sitting below empty labels. */
.js-arc .arc-glyph-label{display:none}
.js-arc .arc-stage[data-seen~="3"] .arc-glyph-label{display:block;animation:arc-fadein .5s ease}

/* Pattern cells pop in (scale) only at the end-of-line "3r" stage; per-cell delays
   are set by the engine so the LM grid fills first, then the brain grid. Opacity
   stays at each cell's value. */
.js-arc .arc-pattern{display:none}
/* Reserve the RDM box the moment the brain joins ("3") — cells hidden — so the
   glyphs rise to their final height once, with the brain's entrance. The cells
   then pop in (scale) at end-of-line "3r" without nudging the glyphs upward. */
.js-arc .arc-stage[data-seen~="3"] .arc-pattern{display:grid;visibility:hidden}
.js-arc .arc-stage[data-seen~="3r"] .arc-pattern{visibility:visible}
.js-arc .arc-cell{transform:scale(0);transition:transform .3s ease}
.js-arc .arc-stage[data-seen~="3r"] .arc-cell{transform:scale(1)}

/* The ≈ breathes once both patterns are up. */
.js-arc .arc-stage[data-seen~="3r"] .arc-approx{
  animation:arc-pulse 1.9s ease-in-out infinite}
@keyframes arc-pulse{0%,100%{opacity:.55;transform:scale(1)}50%{opacity:1;transform:scale(1.12)}}

/* Beat 4 — the pivot to "why?": the figure quiets (patterns + labels dim) and
   the ≈/similar swaps to a pulsing question mark. (Static keeps ≈; the ? is a
   JS-only flourish — the beat's text carries the question without JS.) */
.arc-q{display:none}
.js-arc .arc-q{font-family:var(--font-display);font-size:2.6rem;line-height:1;
  color:var(--accent)}
.js-arc .arc-stage[data-seen~="4"] .arc-pattern,
.js-arc .arc-stage[data-seen~="4"] .arc-glyph-label{opacity:.28;transition:opacity .6s ease}
.js-arc .arc-stage[data-seen~="4"] .arc-approx,
.js-arc .arc-stage[data-seen~="4"] .arc-rel-label{display:none}
.js-arc .arc-stage[data-seen~="4"] .arc-q{display:block;
  animation:arc-qpulse 1.7s ease-in-out infinite}
@keyframes arc-qpulse{0%,100%{opacity:.6;transform:scale(1)}50%{opacity:1;transform:scale(1.16)}}

/* Beat 5 — look inside the model: the brain side recedes, the LM regains focus
   with a magnifier, and the concept chips fan in. The ? is dropped. */
.js-arc .arc-magnifier{opacity:0;transform:scale(.6);
  transition:opacity .4s ease,transform .4s ease}
/* Reserve the concept-chip row from when the brain joins ("3"), chips hidden, so
   the glyph row holds its height through beats 3–5; the chips then fan in
   (visible) at beat 5 without lifting the glyphs. */
.js-arc .arc-concepts{display:none}
.js-arc .arc-stage[data-seen~="3"] .arc-concepts{display:flex;visibility:hidden}
.js-arc .arc-stage[data-seen~="5"] .arc-concepts{visibility:visible}
.js-arc .arc-stage[data-seen~="5"] .arc-q{display:none}
.js-arc .arc-stage[data-seen~="5"] .arc-col-brain,
.js-arc .arc-stage[data-seen~="5"] .arc-rel{opacity:.18;transition:opacity .6s ease}
.js-arc .arc-stage[data-seen~="5"] .arc-col-lm .arc-pattern,
.js-arc .arc-stage[data-seen~="5"] .arc-col-lm .arc-glyph-label{opacity:1}
.js-arc .arc-stage[data-seen~="5"] .arc-magnifier{opacity:1;transform:none}
.js-arc .arc-stage[data-seen~="5"] .arc-concepts{display:flex}
.js-arc .arc-stage[data-seen~="5"] .arc-concept{
  animation:arc-chipin .4s ease both;animation-delay:calc(var(--c) * .12s)}
@keyframes arc-chipin{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}

/* Beat 6 — accuracy & whether the concepts drive the brain prediction. The brain
   returns to full focus (undoing beat 5's dim) and the concept chips stay (now
   "measured" for accuracy); the link resolves from "≈ similar" into a directed,
   still-open "drives?" arrow. Rules sit after beat 5 so they win on source order. */
.arc-drive{display:none;flex-direction:column;align-items:center;gap:.05rem;color:var(--accent)}
.arc-drive-arrow{font-family:var(--font-display);font-size:1.5rem;line-height:1}
.arc-drive-label{font-family:var(--font-ui);font-size:.56rem;font-weight:700;
  text-transform:uppercase;letter-spacing:.06em}
.js-arc .arc-drive{display:none}
.js-arc .arc-stage[data-seen~="6"] .arc-col-brain,
.js-arc .arc-stage[data-seen~="6"] .arc-rel{opacity:1}
.js-arc .arc-stage[data-seen~="6"] .arc-pattern,
.js-arc .arc-stage[data-seen~="6"] .arc-glyph-label{opacity:1}
.js-arc .arc-stage[data-seen~="6"] .arc-approx,
.js-arc .arc-stage[data-seen~="6"] .arc-rel-label,
.js-arc .arc-stage[data-seen~="6"] .arc-q,
.js-arc .arc-stage[data-seen~="6"] .arc-magnifier{display:none}
.js-arc .arc-stage[data-seen~="6"] .arc-drive{display:flex;animation:arc-fadein .55s ease both}
.js-arc .arc-stage[data-seen~="6"] .arc-drive-arrow{animation:arc-pulse 1.9s ease-in-out infinite}

/* Beat 7 — the syntax probe. The concept chips, brain side, and link recede; the
   LM re-centres and a dependency parse flows out beneath it. The parse SVG is
   built + drawn by the engine (buildSyntax in index.html); without JS / under
   reduced motion only the plain sentence shows. */
.arc-syntax{margin-top:.1rem;width:100%;display:flex;justify-content:center}
/* Shown the moment beat 7 starts (data-beat="7"), so prepareSyntax can reserve its
   full height before the words reveal; kept on screen through beats 8–9 via seen. */
.js-arc .arc-syntax{display:none}
.js-arc .arc-stage[data-beat="7"] .arc-syntax,
.js-arc .arc-stage[data-seen~="7"] .arc-syntax{display:flex;animation:arc-fadein .5s ease both}
/* The chips + drive arrow recede and the brain dims — keyed to beat-7 *start*
   (data-beat="7") as well as seen, so the figure settles to its final height the
   moment beat 7 begins (not on the "syntax" word, which would shrink it mid-line
   and lift the caption); seen~"7" then holds that state through beats 8–9. The
   brain stays (dimmed) so the glyph row keeps its position — it re-lights at
   beat 8 rather than jumping back in. */
.js-arc .arc-stage[data-beat="7"] .arc-concepts,
.js-arc .arc-stage[data-seen~="7"] .arc-concepts,
.js-arc .arc-stage[data-beat="7"] .arc-drive,
.js-arc .arc-stage[data-seen~="7"] .arc-drive{display:none}
.js-arc .arc-stage[data-beat="7"] .arc-col-brain,
.js-arc .arc-stage[data-seen~="7"] .arc-col-brain,
.js-arc .arc-stage[data-beat="7"] .arc-rel,
.js-arc .arc-stage[data-seen~="7"] .arc-rel{opacity:.2;transition:opacity .6s ease}

/* Dependency-parse overlay (beat 7), ported from the parked engine (git 6c9a5ca).
   The SVG is positioned + measured by buildSyntax; the initial hidden state is set
   inline and the staggered draw-in runs via the Web Animations API. */
/* Horizontal padding so the edge arcs/labels (e.g. the leftmost "nsubj" between the
   first two words) have room — without it they sit at x≈0 and overflow-x:auto clips
   their left side, most visibly on narrow phones. layoutSyntax() only sets paddingTop/
   Bottom inline, so these left/right values survive and shift the word centres inward. */
.arc-syntax .depparse{position:relative;display:inline-block;margin:0 auto;
  overflow-x:auto;overflow-y:hidden;max-width:100%;padding-left:1.5em;padding-right:1.5em}
.arc-syntax .sentence{display:flex;flex-wrap:nowrap;gap:.42em;align-items:flex-end;
  font-family:var(--font-display);font-size:clamp(1.1rem,3.4vw,1.45rem);
  line-height:1.25;color:var(--green);width:max-content;max-width:100%}
.arc-syntax .w{position:relative;white-space:nowrap}
.dep-svg{position:absolute;left:0;top:0;pointer-events:none;overflow:visible}
.dep-arc{fill:none;stroke:var(--brown);stroke-width:1.6}
.dep-head{fill:var(--brown)}
.dep-labelbox{fill:var(--card);stroke:var(--border);stroke-width:1}
.dep-label{fill:var(--accent);font-family:var(--font-ui);font-size:11px;font-weight:700;
  text-transform:uppercase;letter-spacing:.03em}
/* The model's mistaken attachment, once marked: warm-red arc + struck label + ✗.
   (Colour only — the inline strokeDasharray drives the reveal, so don't override it.) */
.dep-arc.dep-wrong.is-marked{stroke:var(--orange)}
.dep-head.dep-wrong.is-marked{fill:var(--orange)}
.dep-labelg.dep-wrong.is-marked .dep-labelbox{stroke:var(--orange)}
.dep-labelg.dep-wrong.is-marked .dep-label{fill:var(--orange);text-decoration:line-through}
.dep-x{fill:var(--orange);font-family:var(--font-ui);font-size:14px;font-weight:700}
/* The corrected attachment, drawn after the error is marked. */
.dep-arc.dep-fix{stroke:var(--green)}
.dep-head.dep-fix{fill:var(--green)}
.dep-labelg.dep-fix .dep-label{fill:var(--green)}

/* Beat 8 — the open question: the brain re-lights to full focus and a pulsing "?"
   sits in the relation slot ("do models with better syntax predict the brain
   better?"). The parse (beat 7) stays on screen. */
.js-arc .arc-stage[data-seen~="8"] .arc-col-brain,
.js-arc .arc-stage[data-seen~="8"] .arc-rel{opacity:1}
.js-arc .arc-stage[data-seen~="8"] .arc-q{display:block;
  animation:arc-qpulse 1.7s ease-in-out infinite}

/* Beat 9 — causal erasure (one-shot, keyed off the *current* beat so it clears
   itself at beat 10). Scissors snip the model, the dependency arcs fade out, and
   the brain RDM partially scrambles — the prediction degrades, it does not vanish. */
.arc-scissors{position:absolute;top:-10px;left:-16px;width:24px;height:24px;
  display:flex;align-items:center;justify-content:center;color:var(--orange);
  opacity:0;transform:scale(.5) rotate(-12deg)}
.arc-scissors svg{width:20px;height:20px}
.js-arc .arc-stage[data-beat="9"] .arc-scissors{animation:arc-snip .9s ease both}
@keyframes arc-snip{
  0%{opacity:0;transform:scale(.5) rotate(-24deg)}
  30%{opacity:1;transform:scale(1) rotate(-12deg)}
  45%{transform:scale(1) rotate(2deg)}
  60%{transform:scale(1) rotate(-12deg)}
  100%{opacity:1;transform:scale(1) rotate(-12deg)}}
/* The syntax representation is cut: the arc overlay fades, the sentence remains. */
.js-arc .arc-stage[data-beat="9"] .arc-syntax .dep-svg{opacity:.12;
  transition:opacity .9s ease .35s}
.js-arc .arc-stage[data-beat="9"] .arc-syntax .sentence{opacity:.45;
  transition:opacity .9s ease .35s}
/* Brain RDM degrades: a handful of cells shift intensity, breaking the visible
   similarity with the model (partial, not blank). */
.js-arc .arc-stage[data-beat="9"] .arc-pattern[data-pat="brain"] .arc-cell{
  transition:opacity .7s ease .3s}
.js-arc .arc-stage[data-beat="9"] .arc-pattern[data-pat="brain"] .arc-cell:nth-child(2){opacity:.7}
.js-arc .arc-stage[data-beat="9"] .arc-pattern[data-pat="brain"] .arc-cell:nth-child(4){opacity:1}
.js-arc .arc-stage[data-beat="9"] .arc-pattern[data-pat="brain"] .arc-cell:nth-child(6){opacity:.16}
.js-arc .arc-stage[data-beat="9"] .arc-pattern[data-pat="brain"] .arc-cell:nth-child(9){opacity:.7}
.js-arc .arc-stage[data-beat="9"] .arc-pattern[data-pat="brain"] .arc-cell:nth-child(11){opacity:.16}
.js-arc .arc-stage[data-beat="9"] .arc-pattern[data-pat="brain"] .arc-cell:nth-child(14){opacity:.7}
.js-arc .arc-stage[data-beat="9"] .arc-pattern[data-pat="brain"] .arc-cell:nth-child(16){opacity:.42}

/* Beat 10 — resolve. The parse + scissors + "?" clear and the figure settles to
   its through-line at rest: model ≈ brain. The ≈/"similar" relation is restored,
   calm (no pulse). The real bio (.about-bio) carries on below. */
.js-arc .arc-stage[data-seen~="10"] .arc-syntax{display:none}
.js-arc .arc-stage[data-seen~="10"] .arc-q{display:none}
.js-arc .arc-stage[data-seen~="10"] .arc-concepts{display:none}
.js-arc .arc-stage[data-seen~="10"] .arc-approx{display:block;visibility:visible;
  opacity:.7;animation:none}
.js-arc .arc-stage[data-seen~="10"] .arc-rel-label{display:block;visibility:visible}

/* Beat 2 enacts prediction: "…good at pred" lands first with a blinking caret
   that holds while the model "thinks"; then the model generates the completion
   one token at a time (accent), the caret trailing the stream until done.
   Streaming is JS-driven (see PAUSE / TOKEN_STEP in the arc engine), so tokens
   reserve no width before they appear and the word joins seamlessly. */
.arc-caret{display:none}
.js-arc .arc-caret{display:inline-block;width:.55ch;height:1.05em;
  margin-left:.02em;vertical-align:-.16em;background:var(--accent);border-radius:1px;
  opacity:0}
/* Caret blinks only during the prediction phase (after the words land), not while
   the earlier words of beat 2 are still revealing. */
.js-arc .arc-beat.is-predicting:not(.is-typed) .arc-caret{
  animation:arc-blink .8s steps(1,end) infinite}
.js-arc .arc-beat.is-typed .arc-caret{display:none}
@keyframes arc-blink{0%,100%{opacity:1}50%{opacity:0}}

/* Tokens stream in one by one. Hidden (no reserved width) until the engine adds
   .is-on, then each pops in. */
.js-arc .arc-tok{display:none}
.js-arc .arc-tok.is-on{display:inline;animation:arc-fadein .22s ease both}
@keyframes arc-fadein{from{opacity:0}to{opacity:1}}

/* Playback controls: a row of progress dots + a replay button. */
.js-arc .arc-controls{display:flex;align-items:center;justify-content:center;
  gap:1rem;margin-top:1.1rem}
.arc-dots{display:flex;gap:.5rem}
.arc-dot{width:8px;height:8px;padding:0;border:0;border-radius:50%;
  background:var(--border);cursor:pointer;transition:background .25s ease,transform .25s ease}
.arc-dot:hover{background:var(--muted)}
.arc-dot.is-active{background:var(--accent);transform:scale(1.3)}
.arc-replay{display:inline-flex;align-items:center;gap:.35rem;padding:.25rem .6rem;
  border:1px solid var(--border);border-radius:999px;background:none;cursor:pointer;
  font-family:var(--font-ui);font-size:.72rem;font-weight:700;text-transform:uppercase;
  letter-spacing:.06em;color:var(--muted);transition:color .2s ease,border-color .2s ease}
.arc-replay:hover{color:var(--accent);border-color:var(--accent)}

/* Mobile: keep LM ≈ brain side by side, scaled down to fit without colliding. */
@media(max-width:560px){
  .arc-stage{min-height:312px}   /* reserves the beat-7 parse height so nothing below shifts */
  .arc-row{gap:.7rem}
  .arc-glyph svg{width:42px;height:42px}
  .arc-pattern{width:46px;height:46px;gap:2px}
  .arc-col{width:46px;gap:.5rem}
  .arc-glyph-label{font-size:.52rem;letter-spacing:.04em}
  .arc-rel{width:58px;height:46px}
  .arc-approx{font-size:1.2rem}
  .arc-q{font-size:2rem}
  .arc-concepts{gap:.35rem}
  .arc-concept{font-size:.72rem;padding:.25rem .55rem}
}

/* ---- Respect prefers-reduced-motion ------------------------ */
@media(prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  a,.nav-links a,.button,.chips li,.social-icons a,.tag,.avatar,.nav-links a::after,.theme-toggle{
    transition:none}
  .avatar:hover,.social-icons a:hover,.chips li:hover,.button:hover{transform:none}
  .theme-toggle:active{transform:none}
  .js-reveal .reveal{opacity:1;transform:none;transition:none}
  /* The arc engine bails out under reduced motion (never adds .js-arc), but
     guard the static state anyway: everything visible, nothing animating. */
  .arc-glyph,.arc-pred,.arc-beat,
  .avatar,.intro-line,.intro-line .arc-word,.social-icons{
    opacity:1;transform:none;transition:none;animation:none}
  .hero{min-height:0}              /* never force one-view height */
  .skip-anim{display:none}         /* nothing to skip when the hero just flows */
}
