/* ── tokens ─────────────────────────────────────────────────────────── */
:root{
  --bg:#0b0e0c;
  --surface:#0e120d;
  --chrome:#11150f;
  --line:#1e2419;
  --line-2:#2e3a2a;
  --dim:#7d8d76;        /* mockup used #5d6b57 (~3.3:1 on --bg, fails AA); this is ~4.7:1 */
  --muted:#8fa088;
  --text:#c9d1c9;
  --text-2:#a9b8a2;
  --bright:#eef3ec;
  --accent:oklch(0.8 0.17 150);
  --accent-hi:oklch(0.9 0.15 150);
  --amber:oklch(0.8 0.17 80);
  --amber-hi:oklch(0.85 0.14 80);
  --pad:64px;
  --boot-delay:1.1s;    /* hero sequence starts as the boot overlay clears */
}
.no-boot{--boot-delay:.2s}

/* ── keyframes (ported verbatim from the mockup) ────────────────────── */
@keyframes blink{0%,49%{opacity:1}50%,100%{opacity:0}}
@keyframes lineIn{to{opacity:1}}
@keyframes bootOut{to{opacity:0;visibility:hidden}}
@keyframes typeW{to{width:6ch}}
/* Single comet dot per diagram spine — replaces the old per-connector packets.
   Linear + clamped so the head starts at the top of the first box and stops
   at the bottom of the last, never overshooting past either end. */
@keyframes flow{
  0%{top:0;opacity:0}
  6%{opacity:1}
  94%{opacity:1}
  100%{top:calc(100% - 26px);opacity:0}
}
@keyframes flowUp{
  0%{bottom:-26px;opacity:0}
  6%{opacity:1}
  94%{opacity:1}
  100%{bottom:calc(100% - 26px);opacity:0}
}
@keyframes flowX{
  0%{left:-26px;opacity:0}
  6%{opacity:1}
  94%{opacity:1}
  100%{left:calc(100% - 26px);opacity:0}
}
/* Choreographed multi-dot cycles — Ascend GROP, Sensora, Simmer. Each dot on a
   diagram shares one master animation-duration and start time (a closed
   <dialog> is display:none, so showModal() starts every child animation at
   0% in lockstep); a dot is opacity:0 outside its own [start%,end%] window and
   only moves inside it, so same-duration siblings stay permanently ordered
   instead of looping on independent clocks. Gaps between windows are the
   deliberate pause between stages. ponytail: %s are tuned by hand against the
   current node sizes/durations below — retune together if either changes.

   GROP: green descends and parks BEHIND the GRPO Trainer box (masked by its
   z-index:1, not just faded) so it visibly "stops" there; amber only starts
   once green has parked. Every keyframe below explicitly repeats its final
   top/bottom/opacity value at 100% — a property with no 100% keyframe gets an
   IMPLICIT one synthesized from the element's static (pre-animation) style,
   which would silently interpolate the "parked" dot back toward its base
   top:0/opacity:1 for the rest of the cycle instead of holding still/hidden. */
@keyframes grpoGreen{
  0%{top:0;opacity:0}
  3%{opacity:1}
  40%{top:calc(100% - 73px);opacity:1}
  46%{top:calc(100% - 73px);opacity:0}
  100%{top:calc(100% - 73px);opacity:0}
}
@keyframes grpoAmber{
  0%,50%{bottom:0;opacity:0}
  53%{opacity:1}
  90%{bottom:calc(100% - 26px);opacity:1}
  94%{bottom:calc(100% - 26px);opacity:0}
  100%{bottom:calc(100% - 26px);opacity:0}
}
/* Sensora: one dot, color flips accent->amber while masked behind the "LLM
   recommendation" box — two keyframes 0.01% apart fake an instant swap without
   a visible blend, since currentColor-driven backgrounds re-evaluate live. */
@keyframes sensoraFlow{
  0%{top:0;opacity:0;color:var(--accent)}
  6%{opacity:1}
  75%{color:var(--accent)}
  75.01%{color:var(--amber-hi)}
  94%{opacity:1}
  100%{top:calc(100% - 26px);opacity:0;color:var(--amber-hi)}
}
/* Simmer: top segment -> pause -> fork (both arms move together) -> pause ->
   converge (both arms move together) -> pause -> loop. */
@keyframes simTop{
  0%{top:0;opacity:0}
  2%{opacity:1}
  /* Stops well short of the Flask API box's own bottom edge (100% - 26px would
     put the dot's head, offset 3px past its trail, ~3px past the box) so it
     settles inside the box instead of overlapping the fork zone below it. */
  24%{top:calc(100% - 40px);opacity:1}
  26%{top:calc(100% - 40px);opacity:0}
  100%{top:calc(100% - 40px);opacity:0}
}
@keyframes simFork{
  0%,34%{top:0;opacity:0}
  36%{opacity:1}
  54%{top:calc(100% - 26px);opacity:1}
  57%{top:calc(100% - 26px);opacity:0}
  100%{top:calc(100% - 26px);opacity:0}
}
@keyframes simConverge{
  0%,66%{top:0;opacity:0}
  68%{opacity:1}
  88%{top:calc(100% - 26px);opacity:1}
  91%{top:calc(100% - 26px);opacity:0}
  100%{top:calc(100% - 26px);opacity:0}
}
/* Mockup pulsed .16 -> .34 alpha under near-black text, which is illegible (~1.5:1).
   Same pulse, raised into a range where the dark label actually reads (6.2:1 at rest). */
@keyframes cellPulse{0%,100%{background:oklch(0.8 0.17 150 / .72)}50%{background:oklch(0.8 0.17 150 / .92)}}
@keyframes pulseDot{0%,100%{box-shadow:0 0 0 0 oklch(0.8 0.17 150 / .5)}50%{box-shadow:0 0 0 7px oklch(0.8 0.17 150 / 0)}}
@keyframes pagerIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}

/* ── base ───────────────────────────────────────────────────────────── */
*{box-sizing:border-box}
html{scroll-behavior:smooth;scrollbar-color:var(--line-2) var(--bg)}
::-webkit-scrollbar{width:12px;height:12px}
::-webkit-scrollbar-track{background:var(--bg)}
::-webkit-scrollbar-thumb{background:var(--line-2);border-radius:6px;border:2px solid var(--bg)}
::-webkit-scrollbar-thumb:hover{background:var(--accent)}
body{
  margin:0;
  background:var(--bg);
  color:var(--text);
  font-family:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  /* No overflow-x:hidden here — it makes <body> a scroll container and breaks
     position:sticky on .nav. Overflow is prevented with min-width:0 instead. */
}
a{color:var(--accent);text-decoration:none}
a:hover{color:var(--accent-hi)}
:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:3px}

.user{color:var(--accent)}
.pathsym,.comment{color:var(--dim)}

.cursor{
  display:inline-block;width:9px;height:19px;background:var(--accent);
  vertical-align:-4px;animation:blink 1.1s step-end infinite;
}

.section{
  padding:0 var(--pad) 52px;
  max-width:1180px;
  margin:0 auto;
  scroll-margin-top:72px;
}
.cmd{font-size:15px;font-weight:400;margin:0 0 24px;color:var(--bright)}
/* Plain-English section name from the aria-label already on each h2 — a fallback
   label for readers the terminal metaphor loses (recruiters skimming, not just
   engineers), with no extra markup. Stacked above the prompt rather than floated
   beside it because the prompt line wraps to 2 lines under 430px and a float
   would collide with it. Matches the existing .dg-layer-label/.skill-group-label
   treatment: 10px, .16em tracking, uppercase, --dim. */
.cmd::before{
  content:attr(aria-label);display:block;
  font-size:13px;letter-spacing:.16em;text-transform:uppercase;
  color:var(--dim);margin-bottom:9px;
}

/* scroll reveal — hidden state only applies when JS is running */
.js [data-reveal]{opacity:0;transform:translateY(14px);transition:opacity .3s ease-out,transform .3s ease-out}
.js [data-reveal].revealed{opacity:1;transform:none}

/* ── boot overlay ───────────────────────────────────────────────────── */
.boot{
  position:fixed;inset:0;z-index:300;
  background:var(--bg);padding:48px 56px;cursor:pointer;
  font-size:13.5px;line-height:2;
  animation:bootOut .3s ease 1s forwards;
}
.no-boot .boot{display:none}
.boot-line{opacity:0;animation:lineIn 1ms linear forwards}
.boot-line:nth-child(1){animation-delay:.03s}
.boot-line:nth-child(2){animation-delay:.11s}
.boot-line:nth-child(3){animation-delay:.19s}
.boot-line:nth-child(4){animation-delay:.27s}
.boot-line:nth-child(5){animation-delay:.35s}
.boot-line:nth-child(6){animation-delay:.43s}
.boot-welcome{animation-delay:.55s;margin-top:18px;color:var(--bright)}
.boot-hint{color:var(--dim)}
.ok{color:var(--accent)}
.warn{color:var(--amber-hi)}

.scanlines{
  position:fixed;inset:0;pointer-events:none;z-index:250;
  background:repeating-linear-gradient(0deg,rgba(255,255,255,.025) 0 1px,transparent 1px 3px);
}

/* ── nav ────────────────────────────────────────────────────────────── */
.nav{
  display:flex;align-items:center;gap:8px;
  padding:14px 24px;
  background:var(--chrome);border-bottom:1px solid var(--line);
  position:sticky;top:0;z-index:50;
}
.nav-title{flex:1;font-size:12px;color:var(--dim)}
.nav-links{display:flex;flex-wrap:wrap;gap:18px;font-size:12px}
.nav-links a{color:var(--muted);transition:color .15s}
.nav-links a:hover,.nav-links a.nav-current{color:var(--accent)}

/* ── hero ───────────────────────────────────────────────────────────── */
.hero{padding:60px var(--pad) 48px;max-width:1180px;margin:0 auto}
.lastlogin{font-size:13px;color:var(--dim)}
.hero-cmd{margin-top:26px;font-size:15px;display:flex}
.typed{
  display:inline-block;overflow:hidden;white-space:nowrap;width:0;
  animation:typeW .5s steps(6) var(--boot-delay) forwards;
}
.hero-body{opacity:0;animation:lineIn 1ms linear calc(var(--boot-delay) + .7s) forwards}
.hero h1{
  margin:18px 0 0;
  font-size:clamp(30px,7vw,48px);line-height:1.15;font-weight:600;
  color:var(--bright);letter-spacing:-.01em;
}
.accentline{color:var(--accent)}
.blurb{margin:22px 0 0;font-size:14.5px;line-height:1.8;color:var(--muted);max-width:640px}

.cta{display:flex;flex-wrap:wrap;gap:12px;margin-top:28px}
.hero .cta{opacity:0;animation:lineIn 1ms linear calc(var(--boot-delay) + 1s) forwards}
.hero-prompt{
  margin-top:30px;font-size:15px;
  opacity:0;animation:lineIn 1ms linear calc(var(--boot-delay) + 1.3s) forwards;
}

.btn{
  display:inline-block;padding:12px 20px;border-radius:6px;
  border:1px solid var(--line-2);font-size:13px;color:var(--text-2);
  cursor:pointer;transition:border-color .15s,background .15s,color .15s,transform .12s;
}
.btn:hover{border-color:var(--accent);color:var(--text-2)}
.btn-accent{border-color:var(--accent);color:var(--accent)}
.btn-accent:hover{background:var(--accent);color:var(--bg)}
.btn-solid{
  padding:11px 24px;background:var(--accent);color:var(--bg);
  border-color:var(--accent);font-weight:600;
}
.btn-solid:hover{transform:translateY(-2px);color:var(--bg)}

/* ── projects ───────────────────────────────────────────────────────── */
.proj-table{border:1px solid var(--line);border-radius:8px;overflow:hidden}
.proj-head{
  display:grid;grid-template-columns:96px 1fr 290px 84px 90px;gap:16px;
  padding:12px 22px;background:var(--chrome);
  font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--dim);
}
.proj-row{
  display:grid;grid-template-columns:96px 1fr 290px 84px 90px;gap:16px;
  padding:18px 22px;border-top:1px solid var(--line);
  cursor:pointer;align-items:baseline;list-style:none;
  transition:background .12s,transform .12s;
}
.proj-row::-webkit-details-marker{display:none}
.proj-row:hover{background:var(--chrome);transform:translateX(4px)}
.proj-year{font-size:13px;color:var(--dim)}
.proj-name{font-size:15px;font-weight:600;color:var(--bright);min-width:0}
.proj-tag{font-weight:400;color:var(--muted);font-size:13px}
.proj-stack{font-size:12px;color:var(--amber)}
/* Pinned to the last track so rows without a .proj-link don't pull it left. */
.proj-icon{font-size:13px;color:var(--accent);text-align:right;white-space:nowrap;grid-column:-2/-1}
.icon-glyph::before{content:'[+]'}
.proj[open] .icon-glyph::before{content:'[-]'}

/* Skim-path link for projects with a real public URL (live site, repo, poster).
   Same visual language as .case-btn, smaller, sized for the 84px row column.
   stopPropagation in main.js keeps a click from also toggling the <details>. */
.proj-link{
  justify-self:end;padding:4px 10px;
  border:1px solid oklch(0.8 0.17 150 / .5);border-radius:5px;
  font-size:11.5px;color:var(--accent);white-space:nowrap;
  transition:background .15s,color .15s;
}
.proj-link:hover{background:var(--accent);color:var(--bg)}

.proj-body{
  padding:6px 22px 22px 134px;
  border-top:1px dashed var(--line);background:var(--surface);
}
.proj[open] .proj-body{animation:pagerIn .2s ease-out both}
.bullet{font-size:13px;line-height:1.9;color:var(--text-2)}
.mark{color:var(--accent)}
.case-btn{
  display:inline-block;margin-top:14px;padding:8px 16px;
  border:1px solid oklch(0.8 0.17 150 / .5);border-radius:6px;background:none;
  font:inherit;font-size:12.5px;color:var(--accent);cursor:pointer;
  transition:background .15s,color .15s;
}
.case-btn:hover{background:var(--accent);color:var(--bg)}

/* ── case-study diagrams ────────────────────────────────────────────── */
.dg{margin-top:30px;padding-top:24px;border-top:1px dashed var(--line)}
.dg-cmd{font-size:13px;color:var(--dim);margin-bottom:22px}
.dg-col{display:flex;flex-direction:column;align-items:center;position:relative}
.dg-stack{display:flex;flex-direction:column;position:relative}
.dg-full{width:100%}

/* Nodes. ponytail: mockup used 7 distinct min-widths; collapsed to 3 buckets.
   Only Ascend's chain reads the narrow/wide rhythm, which --sm preserves. */
.node{
  position:relative;z-index:1;                                /* paints over the .dg-flow dot passing behind it */
  border:1px solid var(--line-2);border-radius:8px;
  padding:12px 20px;text-align:center;background:var(--bg);
  min-width:210px;max-width:100%;
}
.node-sm{min-width:160px}
.node-lg{min-width:280px}
.node-accent{border-color:oklch(0.8 0.17 150 / .45)}
.node-amber{border-color:oklch(0.8 0.17 80 / .5)}
.node-title{font-size:13px;font-weight:600;color:var(--bright)}
.node-title-accent{color:var(--accent)}
.node-title-amber{color:var(--amber-hi)}
.node-sub{font-size:11px;color:var(--muted);margin-top:3px}
.node-sub-accent{color:var(--accent)}
.node-sub-amber{color:var(--amber-hi)}

/* Connectors. Vertical is the backbone of 4 of the 5 diagrams.
   Dashes are static now — a single .dg-flow dot carries the motion instead.
   Lengths are kept at 12n+6 (one dash unit = 6px) and the pattern is centered
   via background-position, so every bar starts and ends on a full dash
   instead of clipping mid-stripe — keep new lengths on that grid. */
.dg-conn{
  position:relative;width:2px;height:30px;margin:6px auto;flex:none;
  background:repeating-linear-gradient(180deg,oklch(0.8 0.17 150 / .5) 0 6px,transparent 6px 12px);
  background-size:2px 12px;background-position:0 calc(50% + 3px);
}
.dg-conn-short{height:18px}
.dg-conn-tall{height:42px}
.dg-conn-static{
  background-image:repeating-linear-gradient(180deg,var(--line-2) 0 6px,transparent 6px 12px);
}
.dg-conn-amber{background-image:repeating-linear-gradient(180deg,oklch(0.8 0.17 80 / .5) 0 6px,transparent 6px 12px)}
.dg-conn-label{
  position:absolute;left:14px;top:50%;transform:translateY(-50%);white-space:nowrap;
  font-size:11px;color:var(--dim);background:var(--surface);
  padding:2px 7px;border:1px solid var(--line);border-radius:4px;
}
.dg-conn-label-amber{color:var(--amber-hi);border-color:oklch(0.8 0.17 80 / .35)}
/* Ascend kept its pre-alignment connector geometry — user preferred the original look. */
#cs-ascend .dg-conn{
  height:34px;margin:5px auto;
  background:repeating-linear-gradient(180deg,oklch(0.8 0.17 150 / .5) 0 6px,transparent 6px 12px);
}
#cs-ascend .dg-conn-short{height:26px}

/* Single comet dot — travels the vertical spine of one column, hiding behind
   opaque .node boxes so it reads as hopping box-to-box, then fades and resets.
   z-index:1 ties it with .node/.dg-layer (also 1) — DOM order then keeps boxes
   (which come after the dot) painting over it — while sitting strictly above
   .dg-conn's dashes (z-index:auto, the 0-level layer), which would otherwise
   win by DOM order too and render on top of the dot passing through them. */
.dg-flow{
  position:absolute;left:50%;top:0;width:2px;height:26px;z-index:1;
  transform:translateX(-50%);pointer-events:none;
  background:linear-gradient(to top,var(--accent),transparent);
  animation:flow 4.2s linear infinite;
}
.dg-flow::after{
  content:"";position:absolute;left:50%;bottom:-3px;
  width:8px;height:8px;border-radius:2px;transform:translateX(-50%);
  background:var(--accent);box-shadow:0 0 7px 1px oklch(0.8 0.17 150 / .55);
}
/* GROP green — parks behind GRPO Trainer instead of looping independently;
   see the grpoGreen/grpoAmber choreography comment above. */
.dg-flow.dg-flow-grpo{animation:grpoGreen 7.6s linear infinite}
/* Sensora — currentColor lets the shared gradient/glow flip accent->amber
   mid-flight via the sensoraFlow color step. */
.dg-flow.dg-flow-sensora{
  background:linear-gradient(to top,currentColor,transparent);
  animation:sensoraFlow 6.4s linear infinite;
}
.dg-flow.dg-flow-sensora::after{
  background:currentColor;
  box-shadow:0 0 7px 1px color-mix(in oklch,currentColor 55%,transparent);
}
/* Simmer — top segment and fork arms share this dot shape; choreographed via
   the sim* keyframes above instead of independent 4.2s loops. */
.dg-flow.sim-top{animation:simTop 9.6s linear infinite}
.dg-flow.sim-fork{animation:simFork 9.6s linear infinite}
/* Amber rail comet — Ascend GROP only. Travels upward (data feeds back into
   the model) once the green descent has parked; see grpoGreen/grpoAmber. */
.dg-flow-amber{
  position:absolute;left:50%;bottom:0;width:2px;height:26px;z-index:1;
  transform:translateX(-50%);pointer-events:none;
  background:linear-gradient(to bottom,var(--amber-hi),transparent);
  animation:grpoAmber 7.6s linear infinite;
}
.dg-flow-amber::after{
  content:"";position:absolute;left:50%;top:-3px;
  width:8px;height:8px;border-radius:2px;transform:translateX(-50%);
  background:var(--amber-hi);box-shadow:0 0 7px 1px oklch(0.8 0.17 80 / .55);
}
/* Horizontal comets — ThreadConnect CLIP encoders. Same clamp/linear recipe as
   .dg-flow, rotated onto the x-axis. */
.edge-flow{
  position:absolute;left:0;top:50%;width:26px;height:2px;z-index:1;
  transform:translateY(-50%);pointer-events:none;
  background:linear-gradient(to left,var(--accent),transparent);
  animation:flowX 4.2s linear infinite;
}
.edge-flow::after{
  content:"";position:absolute;top:50%;right:-3px;
  width:8px;height:8px;border-radius:2px;transform:translateY(-50%);
  background:var(--accent);box-shadow:0 0 7px 1px oklch(0.8 0.17 150 / .55);
}
.edge-flow-amber{
  background:linear-gradient(to left,var(--amber-hi),transparent);
}
.edge-flow-amber::after{
  background:var(--amber-hi);box-shadow:0 0 7px 1px oklch(0.8 0.17 80 / .55);
}
/* Horizontal connector — ThreadConnect only. Static; the CLIP matrix pulse is the focus there.
   Same 12n+6 length + centered-phase rule as .dg-conn. */
.edge{
  position:relative;width:42px;height:2px;flex:none;margin:0 6px;
  background:repeating-linear-gradient(90deg,oklch(0.8 0.17 150 / .5) 0 6px,transparent 6px 12px);
  background-size:12px 2px;background-position:calc(50% + 3px) 0;
}
.edge-static{
  width:42px;
  background-image:repeating-linear-gradient(90deg,var(--line-2) 0 6px,transparent 6px 12px);
}
.edge-amber{background-image:repeating-linear-gradient(90deg,oklch(0.8 0.17 80 / .5) 0 6px,transparent 6px 12px)}

/* Layered boxes — FSA. */
.dg-layer{position:relative;z-index:1;border:1px solid oklch(0.8 0.17 150 / .4);border-radius:8px;background:var(--bg);padding:14px 18px}
.dg-layer-amber{border-color:oklch(0.8 0.17 80 / .5)}
.dg-layer-dashed{border:1px dashed var(--line-2)}
.dg-layer-label{font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--dim);margin-bottom:11px}
.dg-chips{display:flex;flex-wrap:wrap;gap:8px}
.chip-accent{border-color:oklch(0.8 0.17 150 / .4);color:var(--accent)}
.chip-amber{border-color:oklch(0.8 0.17 80 / .45);color:var(--amber-hi)}

/* Cards — FSA external services and Ascend difficulty buckets share this shape. */
.dg-grid-2{display:grid;grid-template-columns:repeat(2,1fr);gap:8px}
.dg-grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
.dg-card{border:1px solid var(--line-2);border-radius:6px;padding:11px 12px;text-align:center}
.dg-card-accent{border-color:oklch(0.8 0.17 150 / .4)}
.dg-card-t{font-size:12.5px;font-weight:600;color:var(--bright)}
.dg-card-t-accent{color:var(--accent)}
.dg-card-s{font-size:11px;color:var(--muted);margin-top:3px}

/* Ascend — phases and the GRPO feedback loop. */
.dg-phases{display:flex;flex-direction:column;gap:34px}
.dg-phase-label{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);margin-bottom:16px}
/* transparent, not --surface: the 30%-group comet dot now runs through this box's
   own outer .dg-col and must show in the gaps between nodes, not be occluded. */
.dg-loop{position:relative;border:1px solid var(--line-2);border-radius:10px;padding:22px 74px;background:transparent}
/* Offsets track the first box height + connector, which are font-size driven and
   stable. Retune if .node padding or the 12.5px title size changes. */
.dg-loop-rail{
  position:absolute;top:74px;bottom:26px;right:36px;width:2px;
  background:repeating-linear-gradient(180deg,oklch(0.8 0.17 80 / .5) 0 6px,transparent 6px 12px);
}
/* .dg-loop shrink-to-fits (centred flex item), so this label hangs into the empty
   space beside it rather than over the boxes. Do not flip it to the left gutter. */
.dg-loop-rail .dg-conn-label{left:12px}

/* Simmer — the two leaf services sit side by side, forking from Flask API and
   converging back into Supabase. Narrower than the full diagram column so the
   fork/converge diagonals (below) aren't stretched into near-horizontal lines. */
.dg-split{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.dg-split,.dg-fork-wrap,.dg-converge-wrap{max-width:440px;width:100%;margin:0 auto}

/* Fork (Flask API -> Import pipeline / Voice agent, green) and converge (both ->
   Supabase, amber) branches. Pure-CSS rotated dashed bars, not SVG: each arm is a
   vertical dashed bar pinned at one end (transform-origin) and rotated to reach
   the other node's center. ponytail: angle/length are tuned for .dg-split's fixed
   440px max-width, not computed from it — retune both if that width, the fork/
   converge height, or the split gap changes. Below the .dg-split breakpoint
   (860px, single column) the geometry no longer applies, so both are hidden in
   favor of the straight .dg-fallback-conn. */
.dg-fork,.dg-converge{position:relative;width:100%;height:96px}
.dg-fork-arm,.dg-converge-arm{
  position:absolute;left:50%;margin-left:-1px;width:2px;height:150px;
  background:repeating-linear-gradient(180deg,oklch(0.8 0.17 150 / .5) 0 6px,transparent 6px 12px);
}
.dg-fork-arm{top:0;transform-origin:top center}
.dg-fork-arm-l{transform:rotate(50deg)}
.dg-fork-arm-r{transform:rotate(-50deg)}
.dg-converge-arm{
  bottom:0;transform-origin:bottom center;
  background-image:repeating-linear-gradient(180deg,oklch(0.8 0.17 80 / .5) 0 6px,transparent 6px 12px);
}
.dg-converge-arm-l{transform:rotate(-50deg)}
.dg-converge-arm-r{transform:rotate(50deg)}
/* Amber comet dot for the converge arms — same recipe as .dg-flow, travelling
   the arm's own (rotated) local axis, colored to match the Supabase box. */
.dg-converge-flow{
  position:absolute;left:50%;top:0;width:2px;height:26px;z-index:1;
  transform:translateX(-50%);pointer-events:none;
  background:linear-gradient(to top,var(--amber-hi),transparent);
  animation:simConverge 9.6s linear infinite;
}
.dg-converge-flow::after{
  content:"";position:absolute;left:50%;bottom:-3px;
  width:8px;height:8px;border-radius:2px;transform:translateX(-50%);
  background:var(--amber-hi);box-shadow:0 0 7px 1px oklch(0.8 0.17 80 / .55);
}
.dg-fallback-conn{display:none}

/* ThreadConnect — CLIP contrastive matrix. */
.clip{display:flex;align-items:center;row-gap:24px;column-gap:0;flex-wrap:wrap;justify-content:center}
.clip-encoders{display:flex;flex-direction:column;gap:22px}
.clip-row{display:flex;align-items:center}
.clip-row .node{min-width:0;width:120px;padding:12px 14px}
.clip-row .node-title{font-size:12px}
.clip-row .node-sub{font-size:10.5px;margin-top:2px}
.clip-matrix{display:grid;grid-template-columns:34px repeat(4,44px);gap:4px}
.clip-hdr{text-align:center;font-size:11px;padding:5px 0;border-radius:4px}
.clip-hdr-t{color:var(--amber-hi);border:1px solid oklch(0.8 0.17 80 / .35)}
.clip-hdr-i{color:var(--accent);border:1px solid oklch(0.8 0.17 150 / .35)}
.clip-cell{
  text-align:center;font-size:10px;color:var(--dim);
  padding:8px 0;border:1px solid var(--line);border-radius:4px;
}
.clip-cell-match{
  color:var(--bg);border-color:transparent;
  background:oklch(0.8 0.17 150 / .72);   /* static base keeps it legible when motion is off */
  animation:cellPulse 3s ease infinite;
}
.clip-note{font-size:11px;color:var(--dim);margin-top:12px;max-width:230px;line-height:1.6}

/* Stagger utilities. MUST stay last: every `animation:` shorthand above resets
   animation-delay, so these only take effect when declared after them.
   ponytail: mockup had 8 distinct delays; 4 is enough to desynchronise. */
.dl1{animation-delay:.3s}.dl2{animation-delay:.6s}.dl3{animation-delay:.9s}.dl4{animation-delay:1.2s}

/* ── experience / git log ───────────────────────────────────────────── */
.gitlog{display:flex;flex-direction:column;border-left:2px solid var(--line);margin-left:6px}
.commit{
  display:flex;gap:18px;align-items:baseline;
  padding:13px 0 13px 24px;position:relative;transition:background .12s;
}
.commit:hover{background:var(--surface)}
.commit-dot{
  position:absolute;left:-6px;top:19px;width:10px;height:10px;border-radius:50%;
  background:var(--bg);border:2px solid var(--accent);
}
.hash{font-size:12.5px;color:var(--amber);flex:none}
.msg{font-size:13.5px;color:var(--text);line-height:1.6;min-width:0}
.role{color:var(--bright);font-weight:600}
.commit-sub{display:block;margin-top:5px;font-size:12.5px;line-height:1.7;color:var(--dim)}
.tag{color:var(--accent);font-size:12px}
.date{font-size:12px;color:var(--dim);flex:none;margin-left:auto}

/* ── about ──────────────────────────────────────────────────────────── */
.about{
  border:1px solid var(--line);border-radius:8px;background:var(--surface);
  padding:32px 36px;display:grid;grid-template-columns:200px 1fr;gap:36px;align-items:center;
}
.headshot{
  position:relative;width:200px;height:200px;border-radius:8px;overflow:hidden;
  background:repeating-linear-gradient(-45deg,#141a12 0 12px,#101509 12px 24px);
  border:1px solid var(--line-2);
  display:flex;align-items:center;justify-content:center;
}
.headshot-ph{
  font-size:11px;color:var(--dim);background:var(--bg);
  padding:5px 10px;border-radius:4px;border:1px solid var(--line);
}
/* Portrait source in a square box: default centring clips the top of the head,
   so bias the cover window upward. Retune if the photo is ever replaced. */
.headshot img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center 20%}
.about-h{margin:0;font-size:16px;font-weight:600;color:var(--bright)}
.about-p{margin:14px 0 0;font-size:13.5px;line-height:1.9;color:var(--text-2);max-width:640px}
.chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px}
.chip{font-size:12px;padding:5px 12px;border:1px solid var(--line-2);border-radius:5px;color:var(--muted)}

/* ── education ──────────────────────────────────────────────────────── */
.edu{
  border:1px solid var(--line);border-radius:8px;padding:28px 32px;background:var(--surface);
  display:grid;grid-template-columns:1fr auto;gap:24px;align-items:center;
}
.edu-name{font-size:17px;font-weight:600;color:var(--bright)}
.edu-sub{margin-top:6px;font-size:13px;color:var(--muted)}
.edu-course{margin-top:14px;font-size:12px;line-height:1.9;color:var(--dim)}
.gpa{text-align:center;padding:18px 28px;border:1px solid oklch(0.8 0.17 150 / .4);border-radius:8px}
.gpa-val{font-size:30px;font-weight:600;color:var(--accent)}
.gpa-lbl{font-size:11px;color:var(--muted);margin-top:3px}

/* ── skills ─────────────────────────────────────────────────────────── */
.skills{display:flex;flex-direction:column;gap:16px;max-width:900px}
.skill-group{display:grid;grid-template-columns:130px 1fr;gap:16px;align-items:baseline}
.skill-group-label{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--dim);padding-top:7px}
.skill-row{display:flex;flex-wrap:wrap;gap:9px}
.skill{
  font-size:12.5px;padding:6px 13px;border:1px solid var(--line-2);border-radius:6px;
  color:var(--text-2);background:var(--surface);
  transition:border-color .12s,color .12s,transform .12s;
}
.skill:hover{border-color:var(--accent);color:var(--accent);transform:translateY(-2px)}

/* ── contact ────────────────────────────────────────────────────────── */
.contact{
  border:1px solid oklch(0.8 0.17 150 / .35);border-radius:10px;
  padding:36px 40px;background:var(--surface);
}
.contact-cmd{font-size:15px;font-weight:400;margin:0}
.contact-p{margin:18px 0 24px;font-size:13.5px;line-height:1.8;color:var(--muted);max-width:620px}
.contact .cta{margin-top:0}

/* ── footer ─────────────────────────────────────────────────────────── */
.site-footer{
  display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;
  padding:20px var(--pad);border-top:1px solid var(--line);
  font-size:12.5px;color:var(--dim);max-width:1180px;margin:0 auto;
}
.pulse{
  width:8px;height:8px;border-radius:50%;background:var(--accent);
  display:inline-block;margin-right:8px;animation:pulseDot 2s infinite;
}

/* ── case-study dialog ──────────────────────────────────────────────── */
.cs{
  background:var(--surface);color:var(--text);
  border:1px solid oklch(0.8 0.17 150 / .4);border-radius:10px;
  max-width:820px;width:calc(100% - 80px);max-height:calc(100vh - 80px);
  overflow:auto;padding:0;
  scrollbar-color:var(--line-2) var(--surface);
}
.cs::-webkit-scrollbar-track{background:var(--surface)}
.cs::-webkit-scrollbar-thumb{background:var(--line-2);border-radius:6px;border:2px solid var(--surface)}
.cs::backdrop{background:rgba(4,6,4,.75)}
.cs[open]{animation:pagerIn .25s ease-out both}
.cs-head{
  display:flex;justify-content:space-between;align-items:center;gap:16px;
  padding:14px 26px;background:var(--chrome);border-bottom:1px solid var(--line);
  position:sticky;top:0;z-index:20;   /* above .node/.dg-layer (z-index:1) so diagrams never paint over it or eat [q] close clicks */
}
.cs-file{font-size:12.5px;color:var(--muted)}
.cs-close{
  font:inherit;font-size:12.5px;color:var(--accent);cursor:pointer;background:none;
  padding:5px 12px;border:1px solid oklch(0.8 0.17 150 / .5);border-radius:5px;
  transition:background .15s,color .15s;flex:none;
}
.cs-close:hover{background:var(--accent);color:var(--bg)}
.cs-body{padding:32px 40px 40px}
.cs-title{margin:0;font-size:26px;font-weight:600;color:var(--bright)}
.cs-year{font-size:14px;font-weight:400;color:var(--dim)}
.cs-role{margin-top:8px;font-size:13px;color:var(--accent)}
.cs-overview{margin:20px 0 0;font-size:13.5px;line-height:1.9;color:var(--text-2)}
.cs-metrics{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:26px 0}
.metric{border:1px solid var(--line-2);border-radius:8px;padding:16px;text-align:center}
.metric-val{font-size:24px;font-weight:600;color:var(--accent)}
.metric-lbl{font-size:11px;color:var(--muted);margin-top:4px}
.cs-sub{font-size:13px;color:var(--dim);margin-bottom:12px}
.cs-bullets{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:9px}
.cs-bullets li{
  display:flex;gap:12px;font-size:13px;line-height:1.8;color:var(--text-2);
}
.cs-bullets li::before{content:'-';color:var(--accent);flex:none}
.cs-stack{
  margin-top:24px;padding-top:20px;border-top:1px dashed var(--line);
  font-size:12.5px;color:var(--amber);
}

/* ── responsive ─────────────────────────────────────────────────────── */
@media (max-width:860px){
  :root{--pad:20px}

  /* Terminal pane bar: the desktop frame (64px gutters, 1180px cap) is what makes
     each section read as its own block; both disappear at this width, leaving a
     continuous stack of same-weight boxes 52px apart. Give each section a real
     edge instead — same --chrome/--line treatment as .nav — plus real gap below. */
  .section{padding-bottom:80px;scroll-margin-top:96px} /* nav wraps to 2 rows down here (up to ~90px tall) — 72px let the new band tuck half under it */
  .cmd{
    margin:0 calc(var(--pad) * -1) 26px;
    padding:14px var(--pad) 15px;
    background:var(--chrome);
    border-top:1px solid var(--line-2);
    border-bottom:1px solid var(--line);
    font-size:16px;
  }

  .proj-head{display:none}
  .proj-row{
    grid-template-columns:1fr auto;
    grid-template-areas:"name icon" "stack year";
    row-gap:8px;column-gap:12px;align-items:start;
  }
  .proj-row:hover{transform:none}
  .proj-name{grid-area:name}
  .proj-icon{grid-area:icon}
  .proj-stack{grid-area:stack}
  .proj-year{grid-area:year;text-align:right}
  /* Only rows that actually have a link gain the third row + its row-gap. */
  .proj-row:has(.proj-link){grid-template-areas:"name icon" "stack year" "link link"}
  .proj-link{grid-area:link;justify-self:start}
  .proj-body{padding:6px var(--pad) 22px}

  /* Connector labels are nowrap pills hung off the side of a 2px line — the long
     ones ("RSC / server actions · Zod-validated requests") run past the viewport.
     Centre them over the line, let them wrap, and grow labelled connectors so a
     two-line label can't collide with the boxes above and below. */
  .dg-conn-label{
    left:50%;transform:translate(-50%,-50%);
    white-space:normal;max-width:min(240px,80vw);text-align:center;
  }
  .dg-conn:has(.dg-conn-label){height:66px}
  .node,.node-lg{min-width:0;width:100%;max-width:280px}
  /* Full width here so the percentage-width nodes inside resolve against a real
     box instead of a shrink-to-fit one. */
  .dg-loop{width:100%;padding:22px 40px}
  .dg-loop-rail{right:16px}
  /* No gutter left to hold it without covering a box; the dashed amber rail on
     its own still reads as the feedback loop against the green spine. */
  .dg-loop-rail .dg-conn-label{display:none}
  .dg-grid-2,.dg-grid-3,.dg-split{grid-template-columns:1fr}
  /* Fork/converge diagonals are tuned for the 2-column split; once it stacks to
     1 column, swap them for the same straight connector every other diagram uses. */
  .dg-fork,.dg-converge{display:none}
  .dg-fallback-conn{display:block}
  .clip-row .node{width:110px}
  .clip-matrix{grid-template-columns:30px repeat(4,38px)}

  .commit{flex-wrap:wrap;gap:6px 14px}
  .date{margin-left:0;width:100%}

  .skill-group{grid-template-columns:1fr;gap:8px}
  .skill-group-label{padding-top:0}

  .about{grid-template-columns:1fr;gap:24px;padding:24px}
  .headshot{width:100%;max-width:220px;height:220px}
  .edu{grid-template-columns:1fr;padding:24px}
  .contact{padding:24px}
  .cs{width:100%;max-width:none;max-height:100vh;border-radius:0;border:none}
  .cs-head{padding:14px 20px}
  .cs-body{padding:24px 20px 32px}
  .cs-metrics{grid-template-columns:1fr}
  .site-footer{flex-direction:column}
  .boot{padding:32px 20px;font-size:12.5px;line-height:1.9}
}

@media (max-width:560px){
  .nav-title{display:none}
  .nav-links{flex:1;justify-content:center;gap:12px;font-size:11px}
  .nav-links a{padding:5px 0}
  .hero{padding:40px var(--pad) 36px}
}

/* ── reduced motion ─────────────────────────────────────────────────── */
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{
    animation-duration:1ms!important;
    animation-delay:0s!important;
    animation-iteration-count:1!important;
    transition-duration:0s!important;
  }
  .js [data-reveal]{opacity:1!important;transform:none!important}
  /* A frozen comet dot would sit as a stray mark mid-spine. */
  .dg-flow,.dg-flow-amber,.edge-flow,.dg-converge-flow{display:none}
}
