:root{
  --bg:#08090c;
  --bg-2:#0d0f14;
  --bg-3:#12151d;
  --line:#1c1f28;
  --line-soft:#15181f;
  --fg:#f3f4f7;
  --fg-soft:#a6abb9;
  --dim:#7d8397;
  --faint:#737990;
  --a1:#8b7cff;
  --a2:#2fd4c6;
  --grad:linear-gradient(100deg,var(--a1),var(--a2));
  --mono:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,monospace;
  --sans:"Inter",system-ui,-apple-system,"Segoe UI",sans-serif;

  --t-label:11px;
  --t-huge:clamp(2.4rem,6.2vw,4.5rem);
  --t-lead:clamp(1.15rem,2.8vw,1.75rem);
  --t-head:clamp(1.45rem,3.2vw,2.1rem);

  --max:1120px;
  --gutter:clamp(1.25rem,4vw,3rem);
  --nav-h:58px;
  --sec-y:clamp(3.25rem,5vw,4.5rem);
  --ease:cubic-bezier(.16,1,.3,1);
}

*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%}
body{
  background:var(--bg);
  color:var(--fg-soft);
  font-family:var(--sans);
  font-size:15px;
  line-height:1.7;
  font-weight:300;
  letter-spacing:.005em;
  overflow-x:hidden;
  -webkit-font-smoothing:antialiased;
}
::selection{background:rgba(139,124,255,.28);color:#fff}

/* No native drag ghosts: dragging a link, icon or the model should do nothing
   rather than start an HTML5 drag. JS covers Firefox, which ignores user-drag. */
a,img,svg,canvas,button,kbd{-webkit-user-drag:none;user-drag:none}

/* Nothing on the page is selectable — set on body so it inherits everywhere. */
body{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}

/* A soft fixed wash so the flat black has some depth behind the content. */
body::before{
  content:'';
  position:fixed;inset:0;z-index:0;pointer-events:none;
  background:
    radial-gradient(70vw 55vw at 88% -8%, rgba(139,124,255,.075), transparent 62%),
    radial-gradient(60vw 50vw at 2% 104%, rgba(47,212,198,.05), transparent 62%);
}

/* ---------- Nav ---------- */
nav{
  position:fixed;top:0;left:0;width:100%;
  height:var(--nav-h);
  z-index:100;
  display:flex;align-items:center;justify-content:center;
  background:rgba(8,9,12,.78);
  backdrop-filter:blur(14px) saturate(140%);
  -webkit-backdrop-filter:blur(14px) saturate(140%);
  border-bottom:1px solid var(--line-soft);
}
.nav-inner{
  width:100%;max-width:var(--max);
  padding:0 var(--gutter);
  display:flex;align-items:center;justify-content:space-between;gap:1rem;
}
.nav-logo{
  font-family:var(--mono);font-size:13px;font-weight:500;
  color:var(--fg);letter-spacing:.02em;text-decoration:none;
  display:inline-flex;align-items:center;gap:.55rem;flex:none;
}
.nav-logo .dot{width:6px;height:6px;border-radius:50%;background:var(--grad);flex:none}
.nav-links{display:flex;gap:1.75rem;overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}
.nav-links::-webkit-scrollbar{display:none}
.nav-links a{
  font-family:var(--mono);font-size:var(--t-label);
  text-transform:uppercase;letter-spacing:.13em;
  color:var(--faint);text-decoration:none;white-space:nowrap;
  transition:color .3s var(--ease);
  position:relative;padding:2px 0;
}
.nav-links a:hover{color:var(--fg-soft)}
.nav-links a.active{color:var(--fg)}
.nav-links a.active::after{
  content:'';position:absolute;left:0;right:0;bottom:-2px;height:1px;background:var(--grad);
}
/* Hidden until the links stop fitting — see the 760px block, which is also
   where it is given something to open. Two hairlines rather than the usual
   three, to sit in the same weight as every other rule on the page. */
.nav-toggle{
  display:none;flex:none;
  width:34px;height:34px;align-items:center;justify-content:center;
  padding:0;background:none;cursor:pointer;
  border:1px solid var(--line);border-radius:2px;
  color:var(--fg-soft);
  transition:color .3s var(--ease),border-color .3s var(--ease);
}
.nav-toggle:hover{color:var(--fg);border-color:#2f3441}
.nav-toggle .bars{position:relative;display:block;width:15px;height:9px}
.nav-toggle .bars span{
  position:absolute;left:0;width:100%;height:1px;background:currentColor;
  transition:transform .3s var(--ease);
}
.nav-toggle .bars span:first-child{top:0}
.nav-toggle .bars span:last-child{bottom:0}
/* Open: the two bars meet in the middle and cross, so the button states its
   own action rather than needing a second icon. */
.nav-open .nav-toggle{color:var(--fg)}
.nav-open .nav-toggle .bars span:first-child{transform:translateY(4px) rotate(45deg)}
.nav-open .nav-toggle .bars span:last-child{transform:translateY(-4px) rotate(-45deg)}

#progress{position:fixed;top:0;left:0;height:1px;width:0;background:var(--grad);z-index:101;pointer-events:none}

/* ---------- Layout ---------- */
.wrap{position:relative;z-index:2;max-width:var(--max);margin:0 auto;padding:0 var(--gutter)}
section{padding:var(--sec-y) 0;position:relative}

/* Each section carries its own accent, stepping down the existing violet →
   teal brand gradient as you scroll. No new hues are introduced — the page
   just walks the gradient it already uses, so sections read as distinct
   without any of them looking foreign. --sec is inherited by everything
   accent-coloured inside the section. */
section{--sec:var(--a2)}
#projects  {--sec:#8b7cff}
#experience{--sec:#7a89ff}
#research  {--sec:#5da7f0}
#awards    {--sec:#3fc2dd}
#education {--sec:#2fd4c6}

/* ---------- Reveal ---------- */
.reveal{opacity:0;transform:translateY(20px);transition:opacity .85s var(--ease),transform .85s var(--ease)}
.reveal.in{opacity:1;transform:none}
.d1{transition-delay:.06s}.d2{transition-delay:.12s}.d3{transition-delay:.18s}

/* ---------- Type ---------- */
.label{font-family:var(--mono);font-size:var(--t-label);text-transform:uppercase;letter-spacing:.16em;color:var(--faint)}
h1,h2,h3,h4{font-weight:500;color:var(--fg);letter-spacing:-.022em;line-height:1.14}

/* ---------- Section headers ---------- */
.sec-head{
  display:flex;align-items:baseline;gap:1.25rem;
  padding-bottom:1.25rem;margin-bottom:2.25rem;
  border-bottom:1px solid var(--line);
}
.sec-num{
  font-family:var(--mono);font-size:var(--t-label);
  letter-spacing:.14em;color:var(--sec);flex:none;
}
.sec-head h2{font-size:var(--t-head);flex:1}
.sec-count{
  font-family:var(--mono);font-size:10px;text-transform:uppercase;
  letter-spacing:.14em;color:var(--faint);flex:none;white-space:nowrap;
}

.cat{
  font-family:var(--mono);font-size:var(--t-label);
  text-transform:uppercase;letter-spacing:.16em;color:var(--dim);
  margin:2.75rem 0 1.25rem;
  display:flex;align-items:center;gap:1rem;
}
.cat::after{content:'';flex:1;height:1px;background:var(--line-soft)}
.cat.first{margin-top:0}

/* ---------- Hero ---------- */
.hero{
  /* One value used above and below, with the nav height added on top. The two
     have to match or the content centres in the padded box but sits off-centre
     in the space the visitor actually sees below the fixed nav. The value also
     has to clear the absolutely-positioned scroll cue. */
  --hero-pad:clamp(2.5rem,6vh,3.5rem);
  /* The scroll cue is absolutely positioned, so it takes no part in the
     centring — which left the grid mathematically centred but looking
     top-heavy, because the bottom gap was occupied by the cue while the top
     gap was empty. Reserving the cue's space as extra bottom padding centres
     the grid in the area that is actually empty. */
  --cue-space:3.5rem;
  position:relative;
  min-height:100vh;
  min-height:100svh;
  display:flex;flex-direction:column;justify-content:center;
  padding-top:calc(var(--nav-h) + var(--hero-pad));
  padding-bottom:calc(var(--hero-pad) + var(--cue-space));
}
/* The hero is a single column until the wireframe has actually rendered a
   frame — js/hero-3d.js sets .has-model on <html> at that point. Any failure
   (blocked script, no WebGL, slow connection, narrow screen) therefore leaves
   a clean one-column hero rather than a blank reserved column. */
.hero-grid{
  display:grid;
  grid-template-columns:1fr;
  gap:clamp(2rem,4vw,3.5rem);
  align-items:center;
}
/* The model sits in a framed panel: hairline border, faint inset ground, and
   the caption as a footer bar inside the same frame. */
.hero-model{
  display:none;position:relative;min-width:0;
  border:1px solid var(--line);
  border-radius:3px;
  background:rgba(13,15,20,.45);
  overflow:hidden;
  transition:border-color .4s var(--ease);
}
.hero-model.interactive{border-color:rgba(47,212,198,.4)}
.hero-model.interactive .model-stage{cursor:grab;touch-action:none}
.hero-model.interactive .model-stage:active{cursor:grabbing}

/* Fluid, so just above the 960px breakpoint the model column doesn't squeeze
   the name into a narrow ribbon. */
.has-model .hero-grid{grid-template-columns:minmax(0,1fr) clamp(360px,44vw,680px)}
.has-model .hero-model{display:block}
.model-stage{position:relative;width:100%;aspect-ratio:1/1;max-height:64vh}
.model-stage canvas{
  position:absolute;inset:0;width:100%;height:100%;display:block;
  opacity:0;transition:opacity .9s ease;
}
.model-stage canvas.ready{opacity:1}

/* Shown from first paint so the column never shifts, and faded out once the
   first frame has actually rendered. */
.model-loader{
  position:absolute;inset:0;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:.85rem;
  transition:opacity .5s var(--ease);
}
.hero-model.loaded .model-loader{opacity:0;pointer-events:none}
.loader-track{position:relative;width:110px;height:1px;background:var(--line);overflow:hidden}
.loader-track::after{
  content:'';position:absolute;top:0;left:0;height:1px;width:38%;
  background:var(--grad);animation:cue 1.4s var(--ease) infinite;
}
.loader-text{
  font-family:var(--mono);font-size:10px;
  text-transform:uppercase;letter-spacing:.16em;color:var(--faint);
}
.model-cap{
  display:flex;align-items:center;flex-wrap:nowrap;gap:.35rem .6rem;
  margin:0;padding:.7rem .9rem;
  border-top:1px solid var(--line-soft);
  font-family:var(--mono);font-size:10px;
  text-transform:uppercase;letter-spacing:.14em;color:var(--faint);
}
.model-dot{width:5px;height:5px;border-radius:50%;background:var(--a2);flex:none}
/* The caption is one row at every width: the hint keeps its full width and the
   model name is the part that gives, ellipsing rather than pushing the hint on
   to a second line. Only the longest names ever reach the ellipsis, and only
   while interacting — the idle hint is short enough to leave the name whole. */
#robot-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* Interact affordance — sits opposite the model label */
.model-cta{
  margin-left:auto;flex:none;
  display:inline-flex;align-items:center;gap:.4rem;
  font:inherit;letter-spacing:.14em;text-transform:uppercase;
  color:var(--dim);background:none;border:0;padding:0;cursor:pointer;
  transition:color .3s var(--ease);
  white-space:nowrap;
}
.model-cta:hover{color:var(--fg)}
.model-cta kbd{
  font-family:var(--mono);font-size:9px;line-height:1;
  border:1px solid var(--line);border-radius:2px;
  padding:.28rem .34rem;color:var(--fg-soft);
}
/* The interactive hint lists five controls where the idle one lists two, so it
   is set smaller and tracked tighter than the rest of the caption — at the
   caption's own size it cannot share a row with the longer model names
   (AmadorUAVs Payload Drone, FTC Centerstage) and drops to a second line. */
.hero-model.interactive .model-cta{
  color:var(--a2);
  font-size:8.5px;letter-spacing:.06em;gap:.3rem;
}
/* Colour only — the key caps keep the caption's own metrics in both states.
   They are the tallest thing in that row, so resizing them resized the caption,
   and the figure is centred in its grid cell: a caption 2.3px shorter moved the
   model 1.1px down the instant you pressed I. */
.hero-model.interactive .model-cta kbd{border-color:rgba(47,212,198,.45);color:var(--a2)}

.hero-eyebrow{margin-bottom:1.6rem;display:flex;align-items:center;gap:.75rem}
.hero-eyebrow .bar{width:26px;height:1px;background:var(--grad);flex:none}
.hero h1{
  font-size:var(--t-huge);
  font-weight:300;
  letter-spacing:-.04em;
  color:var(--fg);
  margin-bottom:1.1rem;
}
.hero h1 .ln{display:block;overflow:hidden;padding-bottom:.08em}
.hero h1 .ln > span{display:block}
.lead{
  font-size:var(--t-lead);
  font-weight:300;
  line-height:1.28;
  letter-spacing:-.02em;
  margin-bottom:2.5rem;
  max-width:22ch;
  background:var(--grad);
  -webkit-background-clip:text;background-clip:text;
  -webkit-text-fill-color:transparent;color:transparent;
}
.hero-actions{display:flex;flex-wrap:wrap;gap:.7rem;align-items:center}

/* Buttons */
.btn{
  display:inline-flex;align-items:center;gap:.6rem;
  font-family:var(--mono);font-size:var(--t-label);
  text-transform:uppercase;letter-spacing:.13em;
  padding:.85rem 1.4rem;border-radius:3px;
  text-decoration:none;border:1px solid var(--line);
  color:var(--fg-soft);background:transparent;
  transition:color .35s var(--ease),border-color .35s var(--ease),background .35s var(--ease);
}
.btn:hover{color:var(--fg);border-color:#2c313e;background:var(--bg-2)}
.btn-primary{border-color:transparent;color:#07080b;background:var(--fg);font-weight:500}
.btn-primary:hover{background:#fff;color:#07080b;border-color:transparent}
.btn .arrow{transition:transform .35s var(--ease)}
.btn:hover .arrow{transform:translateX(3px)}

/* Scroll cue — pinned to the bottom of the hero rather than stacked after the
   grid. As a sibling it both pushed itself below the fold once the model column
   grew, and dragged the grid off centre by taking part in the centring. */
.cue{
  position:absolute;left:0;bottom:clamp(1.25rem,3.5vh,2.25rem);
  display:flex;align-items:center;gap:.85rem;
  font-family:var(--mono);font-size:10px;
  text-transform:uppercase;letter-spacing:.18em;color:var(--faint);
}
.cue .track{position:relative;width:54px;height:1px;background:var(--line);overflow:hidden}
.cue .track::after{
  content:'';position:absolute;top:0;left:0;height:1px;width:40%;
  background:var(--grad);animation:cue 2.6s var(--ease) infinite;
}
@keyframes cue{0%{transform:translateX(-100%)}60%,100%{transform:translateX(250%)}}

/* ---------- Cards ---------- */
.card{
  position:relative;
  /* Transparent: the page's own background wash shows through, so cards read
     as outlined regions rather than panels. Section differentiation comes from
     the accent colours alone. */
  background:transparent;
  border:1px solid var(--line);
  border-radius:3px;
  padding:2rem;
  display:flex;flex-direction:column;
  transition:border-color .5s var(--ease),background .5s var(--ease),transform .5s var(--ease);
}
.card::before{
  content:'';position:absolute;top:-1px;left:0;height:1px;width:0;
  background:var(--grad);transition:width .6s var(--ease);
}
/* Hover stays a faint lift rather than a fill, so the card never becomes a
   panel — just brighter than its surroundings for a moment. */
.card:hover,
.card.spotlight{
  border-color:#2a2f3d;
  background:rgba(255,255,255,.02);
  transform:translateY(-2px);
}
.card:hover::before,
.card.spotlight::before{width:100%}
.card-kicker{
  font-family:var(--mono);font-size:10px;
  text-transform:uppercase;letter-spacing:.15em;
  color:var(--sec);margin-bottom:.8rem;
}
.card h3{font-size:1.15rem;margin-bottom:.8rem;letter-spacing:-.018em}
.card p{color:var(--dim);font-size:.9rem;margin-bottom:1.4rem;flex:1}
.card p:last-child{margin-bottom:0}
/* Feature cards differ by width and title size only — padding stays uniform so
   a full-width card and a half-width one read as the same object. */
.card-meta{
  font-family:var(--mono);font-size:11.5px;letter-spacing:.05em;
  color:var(--sec);margin:.5rem 0 1rem;
}
/* Two text columns inside a card. Wider than the card-grid gap on purpose:
   columns of prose need more separation than adjacent panels do. */
.split{display:grid;grid-template-columns:1fr 1fr;gap:clamp(1.5rem,3vw,2.5rem)}
.card.feature h3{font-size:clamp(1.25rem,2.6vw,1.7rem)}

/* Grids */
.grid-2{display:grid;grid-template-columns:repeat(2,1fr);gap:1.15rem;align-items:start}
/* An odd-numbered final card would otherwise sit half-width with a gap beside
   it. Let it run the full width instead, matching the feature cards. */
.grid-2 > .card:last-child:nth-child(odd){grid-column:1 / -1}
.stack{display:flex;flex-direction:column;gap:1.15rem}

/* Chips */
.chips{display:flex;flex-wrap:wrap;gap:.4rem;margin:0 0 1.3rem}
.chip{
  font-family:var(--mono);font-size:10.5px;letter-spacing:.04em;
  color:var(--dim);border:1px solid var(--line);
  padding:.28rem .6rem;border-radius:2px;
  transition:color .3s var(--ease),border-color .3s var(--ease);
}
.card:hover .chip,
.card.spotlight .chip{color:var(--fg-soft);border-color:#252a35}

/* Explore (E) marks its target: the hover look above, plus a short accent ring
   so it reads as being pointed at rather than merely moused over. */
.spotlight{box-shadow:0 0 0 1px color-mix(in srgb, var(--sec) 45%, transparent)}

/* Bullets */
.bullets{list-style:none;margin-bottom:1.4rem}
.bullets li{position:relative;padding-left:1.15rem;margin-bottom:.55rem;color:var(--dim);font-size:.9rem;line-height:1.65}
.bullets li::before{content:'';position:absolute;left:0;top:.68em;width:5px;height:1px;background:var(--faint)}
.bullets strong{color:var(--fg-soft);font-weight:500}
.bullets.last{margin-bottom:0}
/* A second line under a bullet, for detail that belongs to the entry but would
   crowd it on the same line — set smaller and fainter so the entry still reads
   as one item rather than two. */
.note{display:block;margin-top:.25rem;font-size:.78rem;line-height:1.7;color:var(--faint)}
/* Wide enough that the run reads as separate items rather than one sentence.
   The gap does the separating, so the dot itself stays at the text's own
   weight — the next step down in the palette is the hairline colour, which
   would leave the marks all but invisible. */
.note .sep{padding:0 .7em}

/* Links */
a{color:var(--fg-soft);text-decoration:none}
.lnk{color:var(--fg-soft);border-bottom:1px solid rgba(166,171,185,.25);transition:color .3s var(--ease),border-color .3s var(--ease)}
.lnk:hover{color:var(--a2);border-color:var(--a2)}
.links{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:auto}
.pill{
  display:inline-flex;align-items:center;gap:.45rem;
  font-family:var(--mono);font-size:10.5px;
  text-transform:uppercase;letter-spacing:.11em;
  color:var(--dim);border:1px solid var(--line);
  padding:.42rem .8rem;border-radius:2px;
  transition:color .3s var(--ease),border-color .3s var(--ease),background .3s var(--ease);
}
.pill:hover{color:var(--fg);border-color:#2f3441;background:var(--bg-3)}
.ico{width:1em;height:1em;flex:none;display:block}
.pill .ico{width:.85em;height:.85em}
.bullets .ico,.awards .ico{display:inline-block;vertical-align:-.08em;width:.72em;height:.72em;margin-left:.3em}
.sprite{position:absolute;width:0;height:0;overflow:hidden}

/* ---------- Experience ---------- */
.xp{border-top:1px solid var(--line)}
.xp-row{
  display:grid;grid-template-columns:3.25rem 1fr;gap:0 1.5rem;
  /* Symmetric: the left inset clears the accent rail, and the right side needs
     the same or the right-aligned date runs flush into the edge. */
  padding:2.1rem 1rem;
  border-bottom:1px solid var(--line-soft);
  position:relative;
  transition:background .4s var(--ease);
}
/* Accent rail that grows on hover */
.xp-row::before{
  content:'';position:absolute;left:0;top:0;width:1px;height:0;
  background:var(--grad);transition:height .5s var(--ease);
}
.xp-row:hover,
.xp-row.spotlight{background:rgba(255,255,255,.016)}
.xp-row:hover::before,
.xp-row.spotlight::before{height:100%}
.xp-idx{font-family:var(--mono);font-size:var(--t-label);letter-spacing:.12em;color:var(--faint);padding-top:.35rem;transition:color .4s var(--ease)}
.xp-row:hover .xp-idx,
.xp-row.spotlight .xp-idx{color:var(--sec)}
.xp-top{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:.3rem 1.5rem;margin-bottom:.3rem}
.xp-org{font-size:1.15rem;color:var(--fg);font-weight:500;letter-spacing:-.018em}
.xp-date{font-family:var(--mono);font-size:11px;letter-spacing:.06em;color:var(--dim);white-space:nowrap}
.xp-role{font-family:var(--mono);font-size:11.5px;letter-spacing:.05em;color:var(--sec);margin-bottom:1rem}
.xp-links{margin-top:1.15rem}

/* ---------- Awards ---------- */
.awards{list-style:none}
.awards li{
  display:grid;grid-template-columns:auto 1fr;gap:1rem;
  padding:.95rem 0;border-bottom:1px solid var(--line-soft);
  font-size:.9rem;color:var(--dim);line-height:1.6;
}
.awards li:last-child{border-bottom:0;padding-bottom:0}
.awards .mk{font-family:var(--mono);font-size:10px;color:var(--faint);padding-top:.35rem;letter-spacing:.1em}
.awards strong{display:block;color:var(--fg);font-weight:500;font-size:.95rem;margin-bottom:.1rem;letter-spacing:-.012em}
.awards a strong{transition:color .3s var(--ease)}
.awards a:hover strong{color:var(--sec)}

/* The certifications are the only links inside a bullet list, and unlike an
   award — whose title owns its own line — theirs sit mid-sentence against the
   issuer and year. What marks them is contrast: the linked title is the full
   foreground against the dimmer text around it, and the arrow carries the
   section accent, which the title joins on hover. */
.bullets a strong{color:var(--fg);transition:color .3s var(--ease)}
.bullets a .ico{color:var(--sec)}
.bullets a:hover strong{color:var(--sec)}

/* ---------- Education ---------- */
.stats{
  display:flex;flex-wrap:wrap;gap:2.5rem;
  padding:1.4rem 0;margin:1.6rem 0;
  border-top:1px solid var(--line);border-bottom:1px solid var(--line);
}
.stat-k{font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:.15em;color:var(--faint);margin-bottom:.35rem}
.stat-v{font-family:var(--mono);font-size:1.5rem;color:var(--fg);font-weight:500;letter-spacing:-.01em;line-height:1;font-variant-numeric:tabular-nums}
.stat-v small{font-size:.7rem;color:var(--dim);letter-spacing:.06em;margin-left:.4rem;font-weight:400}
.sub-h{font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:.15em;color:var(--sec);margin-bottom:.85rem}
.gap{margin-top:1.9rem}
.cols-2{columns:2;column-gap:2rem}
.cols-2 li{break-inside:avoid}

/* ---------- Viewer modal ----------
   One dialog, reused for every asset a card can show: a CAD model, a
   schematic, a board, a 3D board. The page stays visible and dimmed behind it
   rather than being replaced, so it reads as something opening on top of the
   work rather than a navigation away from it. */
.modal{
  position:fixed;inset:0;z-index:200;
  display:flex;align-items:center;justify-content:center;
  padding:clamp(1rem,3vw,2rem);
  background:rgba(8,9,12,.72);
  backdrop-filter:blur(6px) saturate(120%);
  -webkit-backdrop-filter:blur(6px) saturate(120%);
  opacity:0;visibility:hidden;
  transition:opacity .28s var(--ease),visibility .28s;
}
/* Author display:flex would otherwise beat the browser's own [hidden] rule,
   leaving a closed dialog in the layout and the accessibility tree. */
.modal[hidden]{display:none}
.modal.open{opacity:1;visibility:visible}
.modal-panel{
  width:80vw;height:80vh;
  display:flex;flex-direction:column;
  background:var(--bg-2);
  border:1px solid var(--line);border-radius:4px;
  overflow:hidden;
  /* Starts fractionally small so it grows into place — the same easing the
     rest of the page uses, not a separate animation vocabulary. */
  transform:scale(.985);
  transition:transform .28s var(--ease);
}
.modal.open .modal-panel{transform:none}
.modal-bar{
  display:flex;align-items:center;gap:1rem;flex:none;
  padding:.7rem .9rem;
  border-bottom:1px solid var(--line-soft);
  font-family:var(--mono);font-size:10px;
  text-transform:uppercase;letter-spacing:.14em;color:var(--faint);
}
.modal-title{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.modal-tabs{display:flex;gap:.4rem;margin-left:auto;flex:none}
.modal-tab{
  font:inherit;letter-spacing:.11em;text-transform:uppercase;
  color:var(--dim);background:none;cursor:pointer;
  border:1px solid var(--line);border-radius:2px;
  padding:.35rem .7rem;
  transition:color .3s var(--ease),border-color .3s var(--ease);
}
.modal-tab:hover{color:var(--fg)}
.modal-tab[aria-selected="true"]{color:var(--a2);border-color:rgba(47,212,198,.45)}
.modal-x{
  flex:none;width:26px;height:26px;line-height:1;
  font-size:16px;font-family:var(--sans);
  color:var(--dim);background:none;border:1px solid var(--line);border-radius:2px;
  cursor:pointer;transition:color .3s var(--ease),border-color .3s var(--ease);
}
.modal-x:hover{color:var(--fg);border-color:#2f3441}
.modal-body{
  position:relative;flex:1;min-height:0;
  display:flex;align-items:center;justify-content:center;
  background:var(--bg);
}
/* Schematics and board plots are line art on white; they need a light ground
   to stay legible, and room to be scrolled when zoomed past the panel. */
.modal-body.is-image{overflow:auto;background:#f7f8fa;align-items:flex-start}
/* Copper layers are drawn semi-transparent so both sides read at once, which
   only works over a dark ground — the same reason the PCB editor uses one.
   Schematics keep the light background they were drawn against. */
.modal-body.is-image.on-dark{background:#1b1c22}
/* margin:auto rather than centring the flex container: centred flex items that
   overflow get clipped at the top with no way to scroll back to them, and a
   schematic zoomed past the panel has to stay reachable. */
.modal-body.is-image img{display:block;width:100%;height:auto;min-width:640px;margin:auto}
.modal-body canvas{display:block;width:100%;height:100%}
/* ---- board layers ---- */
.modal-body.is-layers{align-items:stretch;background:#1b1c22}
.layer-stage{
  flex:1;min-width:0;position:relative;overflow:auto;
  display:flex;align-items:center;justify-content:center;padding:1.2rem;
}
/* The plots are separate files sharing one viewBox, so they only line up if
   they are all sized by the same box — hence one sized wrapper and absolutely
   positioned children rather than five independently laid out images. */
/* The aspect ratio is set from the artwork once it loads — every board is a
   different shape, and hardcoding one squashes the others. The 2/1 here is only
   a placeholder for the moment before the first image arrives. */
.layer-sheet{position:relative;width:100%;max-width:820px;aspect-ratio:2/1;margin:auto}
.layer-img{position:absolute;inset:0;width:100%;height:100%;display:block}
.layer-panel{
  flex:none;width:158px;
  border-left:1px solid var(--line);
  padding:.9rem .8rem;
  background:var(--bg-2);
  overflow-y:auto;
}
.layer-heading{
  font-family:var(--mono);font-size:10px;
  text-transform:uppercase;letter-spacing:.14em;color:var(--faint);
  margin-bottom:.7rem;
}
.layer-row{
  display:flex;align-items:center;gap:.5rem;
  font-family:var(--mono);font-size:11px;letter-spacing:.04em;
  color:var(--fg-soft);cursor:pointer;
  padding:.3rem 0;
}
.layer-row input{accent-color:var(--a2);cursor:pointer;flex:none;margin:0}
.layer-swatch{width:9px;height:9px;border-radius:1px;flex:none}

/* ---- 3D view presets, beside the cube ---- */
.view-presets{
  position:absolute;top:calc(14px + 92px + .5rem);right:14px;
  display:flex;flex-direction:column;gap:.3rem;width:92px;
}
.view-preset{
  font-family:var(--mono);font-size:9.5px;
  text-transform:uppercase;letter-spacing:.11em;
  color:var(--dim);background:rgba(13,15,20,.72);
  border:1px solid var(--line);border-radius:2px;
  padding:.3rem 0;cursor:pointer;
  transition:color .3s var(--ease),border-color .3s var(--ease);
}
.view-preset:hover{color:var(--fg);border-color:#2f3441}

.modal-note{
  position:absolute;left:0;right:0;bottom:0;
  padding:.55rem .9rem;
  font-family:var(--mono);font-size:10px;
  text-transform:uppercase;letter-spacing:.14em;color:var(--faint);
  background:linear-gradient(to top,rgba(8,9,12,.9),transparent);
  pointer-events:none;text-align:center;
}
/* The view buttons sit in the same row as the outbound links and should read
   as siblings of them, so they borrow .pill and only reset what a <button>
   brings with it. */
button.pill{background:none;cursor:pointer;font-family:var(--mono)}

/* ---------- Footer ---------- */
footer{position:relative;z-index:2;border-top:1px solid var(--line);padding:2.75rem 0 3.5rem}
.foot-inner{
  max-width:var(--max);margin:0 auto;padding:0 var(--gutter);
  display:flex;flex-wrap:wrap;gap:1rem;justify-content:space-between;align-items:center;
  font-family:var(--mono);font-size:11px;letter-spacing:.06em;color:var(--faint);
}
.foot-inner a{color:var(--faint);transition:color .3s var(--ease)}
.foot-inner a:hover{color:var(--a2)}
.foot-links{display:flex;gap:1.5rem;flex-wrap:wrap}

/* ---------- Responsive ---------- */
/* Desktop only — js/hero-3d.js checks the same 960px threshold and skips
   booting entirely below it, so nothing is downloaded on phones or tablets. */
@media (max-width:960px){
  .has-model .hero-grid{grid-template-columns:1fr}
  .has-model .hero-model{display:none}
  .lead{max-width:26ch}
  /* The viewer is desktop-only, on the same line as the hero model and for the
     same reasons. At 375px the dialog is 300px wide, the layer panel takes 158
     of them and the board lands at 103x55 — and a model is megabytes to pull
     down over mobile data for that. Drag-to-rotate and scroll-to-zoom are a
     mouse's vocabulary too. Better to not offer it than to offer it badly. */
  .pill[data-view]{display:none}
}
@media (max-width:760px){
  .grid-2,.split{grid-template-columns:1fr}
  .cols-2{columns:1}
  .xp-row{grid-template-columns:1fr;gap:.45rem}
  .xp-idx{padding-top:0}
  .stats{gap:1.75rem}
  /* Below this width the five links either scroll sideways out of sight or
     crowd the logo, so they move into a panel under the bar and the toggle
     takes their place. The panel is the same element, restyled — nothing is
     duplicated, so the active-link marking keeps working untouched.

     Everything below is scoped to .has-nav-menu, which main.js sets once the
     button is wired up: if the script never runs, the links stay the
     side-scrolling row they have always been rather than collapsing into a
     panel that nothing can open. */
  .nav-links{gap:1.1rem}
  .has-nav-menu .nav-toggle{display:inline-flex}
  .has-nav-menu .nav-links{
    position:fixed;top:var(--nav-h);left:0;right:0;
    flex-direction:column;gap:0;overflow:visible;
    padding:.25rem var(--gutter) .9rem;
    background:rgba(8,9,12,.94);
    backdrop-filter:blur(14px) saturate(140%);
    -webkit-backdrop-filter:blur(14px) saturate(140%);
    border-bottom:1px solid var(--line);
    opacity:0;visibility:hidden;transform:translateY(-6px);
    transition:opacity .25s var(--ease),transform .25s var(--ease),visibility .25s;
  }
  .has-nav-menu.nav-open .nav-links{opacity:1;visibility:visible;transform:none}
  .has-nav-menu .nav-links a{padding:.85rem 0;border-bottom:1px solid var(--line-soft)}
  .has-nav-menu .nav-links a:last-child{border-bottom:0}
  /* The gradient underline marks a link a few characters wide; stretched
     across a full-width row it reads as a divider instead. Colour alone
     carries the active state here. */
  .has-nav-menu .nav-links a.active::after{display:none}
  .sec-count{display:none}
}
@media (max-width:480px){
  .hero-eyebrow .label{font-size:10px;letter-spacing:.1em}
  .hero-actions .btn{white-space:nowrap;padding:.85rem 1.15rem}
}

/* Pre-animation state — only while JS is running; cleared if anime.js
   never arrives so the hero can never be stuck invisible. Declared before the
   reduced-motion block so that block can override it at equal specificity. */
.js [data-hero]{opacity:0}
.js [data-hero-line] > span{transform:translateY(105%)}

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{
    animation-duration:.001ms!important;
    animation-iteration-count:1!important;
    transition-duration:.001ms!important;
  }
  .reveal{opacity:1!important;transform:none!important}
  .js [data-hero]{opacity:1}
  .js [data-hero-line] > span{transform:none}
}

/* ---- project write-ups ----
   The dialog body centres its child for canvases and images. A long read wants
   the opposite: a column that starts at the top and scrolls. */
.modal-body.is-info{
  display:block;overflow-y:auto;overflow-x:hidden;
  background:var(--bg);
  align-items:stretch;
}
.info{
  max-width:74ch;margin:0 auto;
  padding:clamp(1.5rem,3.5vw,2.75rem) clamp(1.25rem,3vw,2rem) 4rem;
  color:var(--fg-soft);font-size:.925rem;line-height:1.7;
}
.info p{margin:0 0 1.1rem}
.info-lede{
  color:var(--fg);font-size:clamp(1.02rem,1.9vw,1.22rem);
  line-height:1.55;letter-spacing:-.012em;margin-bottom:1.6rem;
}
/* Section headings sit in the same mono register as the card kickers, so the
   dialog reads as part of the page rather than as a document dropped into it. */
.info-h{
  font-family:var(--mono);font-size:10.5px;
  text-transform:uppercase;letter-spacing:.15em;
  color:var(--a2);
  margin:2.2rem 0 .85rem;
  padding-bottom:.5rem;border-bottom:1px solid var(--line-soft);
}
.info > .info-h:first-of-type{margin-top:1.2rem}
.info-list{margin:0 0 1.1rem;padding-left:1.1rem}
.info-list li{margin-bottom:.45rem}

/* Numbers worth seeing before the prose. */
.info-stats{
  display:flex;flex-wrap:wrap;gap:.5rem;
  margin:0 0 1.9rem;
}
.info-stat{
  flex:1 1 auto;min-width:88px;
  border:1px solid var(--line);border-radius:2px;
  padding:.6rem .75rem;background:var(--bg-2);
}
.info-stat-n{
  display:block;font-family:var(--mono);font-size:1rem;
  color:var(--fg);letter-spacing:-.01em;
}
.info-stat-l{
  display:block;font-family:var(--mono);font-size:9.5px;
  text-transform:uppercase;letter-spacing:.13em;color:var(--faint);
  margin-top:.25rem;
}

/* A note is the place where a project admits to something. It gets an accent
   rule rather than a warning colour, because none of it is an error. */
.info-note{
  border-left:2px solid var(--a1);
  background:rgba(139,124,255,.055);
  padding:.85rem 1rem;border-radius:0 2px 2px 0;
  color:var(--dim);font-size:.885rem;
}

.info-fig{margin:1.5rem 0 1.8rem}
.info-dg{
  border:1px solid var(--line);border-radius:3px;
  background:var(--bg-2);padding:1.1rem 1rem;
  overflow-x:auto;
}
.info-fig figcaption{
  margin-top:.6rem;font-size:.815rem;color:var(--faint);line-height:1.55;
}
.info-shot img{
  display:block;width:100%;height:auto;
  border:1px solid var(--line);border-radius:3px;background:var(--bg-2);
}

.info-tw{overflow-x:auto;margin:1.4rem 0 1.7rem}
.info-table{border-collapse:collapse;width:100%;font-size:.855rem}
.info-table th{
  font-family:var(--mono);font-size:9.5px;font-weight:400;
  text-transform:uppercase;letter-spacing:.13em;color:var(--faint);
  text-align:left;padding:.5rem .8rem .5rem 0;
  border-bottom:1px solid var(--line);white-space:nowrap;
}
.info-table td{
  padding:.62rem .8rem .62rem 0;
  border-bottom:1px solid var(--line-soft);
  color:var(--dim);vertical-align:top;
}
.info-table tr td:first-child{color:var(--fg-soft);white-space:nowrap}

/* ---- diagrams ----
   One vocabulary across every figure: boxes on the panel ground, mono labels,
   teal for the path that carries the result. */
.dg{width:100%;height:auto;display:block;min-width:420px}
.dg text{font-family:var(--mono);font-size:10.5px;fill:var(--faint)}
.dg .t-fg{fill:var(--fg-soft)}
.dg .t-a{fill:var(--a2)}
.dg .dim{fill:var(--faint)}
.dg rect.b{fill:var(--bg-3);stroke:var(--line)}
.dg rect.b.acc{stroke:rgba(47,212,198,.42)}
.dg .ln{stroke:#2a2f3d;fill:none}
.dg .hd{fill:#2a2f3d;stroke:none}
.dg rect.track{fill:var(--bg-3)}
.dg rect.bar.a{fill:#3a4050}
.dg rect.bar.b{fill:var(--a2);opacity:.85}
.dg rect.bar.a2{fill:var(--a1);opacity:.85}
.dg rect.key.a{fill:#3a4050}
.dg rect.key.b{fill:var(--a2)}

/* A card that has a write-up says so, quietly, and only where a pointer can
   ask for one. */
.card.has-more{cursor:pointer}
.card.has-more::after{
  content:'';position:absolute;right:1.15rem;top:1.3rem;
  width:6px;height:6px;border-right:1px solid var(--faint);
  border-bottom:1px solid var(--faint);
  transform:rotate(-45deg);
  opacity:0;transition:opacity .4s var(--ease),transform .4s var(--ease);
}
.card.has-more:hover::after,
.card.has-more:focus-visible::after{opacity:1;transform:rotate(-45deg) translate(2px,2px)}
.card.has-more:focus-visible{outline:1px solid var(--a2);outline-offset:2px}

@media (max-width:640px){
  .dg{min-width:380px}
  .info{font-size:.895rem}
}
