:root {
  --bg: #151615;
  --panel: #202220;
  --ink: #f1f0e8;
  --muted: #aaada5;
  --accent: #6c7bff;
  --line: #383a35;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
}
/* Scroll stack: every section pins as a full-viewport panel and is covered by
   the next, which paints above it.
   A panel taller than the viewport cannot be pinned safely — it stops moving,
   so everything below the fold becomes unreachable and the next panel simply
   covers it. #work is the one section that is genuinely taller than a screen,
   so it is split into three rows (.work-panel), each of which pins in turn.
   The rest fit inside a viewport, with overflow-y as a safety valve for short
   windows. */
.hero,
.stats,
.work-panel,
#about,
#experience,
#leadership,
#repositories,
.contact {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow-y: auto;
  scrollbar-width: thin;
}
.hero {
  z-index: 1;
}
.stats {
  z-index: 2;
}
#work {
  position: relative;
  z-index: 3;
}
#about {
  z-index: 4;
}
#experience {
  z-index: 5;
}
#leadership {
  z-index: 6;
}
#repositories {
  z-index: 7;
}
.contact {
  z-index: 8;
}
/* Section reveal.
   Only transform is animated — never opacity. These panels are stacked over
   sections that stay pinned behind them, and a partly transparent panel would
   let the pinned section underneath show through, so the stats strip would
   surface in the background wherever a panel was mid-reveal. Full opacity at
   all times keeps every handover clean.
   The hidden state is gated behind .js so that if scripting is unavailable the
   content still renders instead of staying invisible. */
.reveal {
  transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}
.js .reveal {
  transform: translateY(28px);
}
.js .reveal.in {
  transform: none;
}
/* Where supported, the panels are driven by scroll position instead of a tween,
   so the stacking motion itself eases rather than merely arriving eased.
   Every .reveal is a sticky panel exactly one viewport tall, so its entry
   range ends at the instant it pins — and because a pinned box stops moving,
   its progress freezes at 100% and the panel stays settled instead of stalling
   part-way up the screen. */
@supports (animation-timeline: view()) {
  .js .reveal {
    transform: none;
    transition: none;
    animation: scroll-reveal linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 100%;
  }
  /* Motion comes purely from position, so a panel is opaque the whole way and
     the pinned section behind it stays hidden. The easing is split across two
     segments: most of the travel lands early, then the panel settles. */
  @keyframes scroll-reveal {
    0% {
      transform: translateY(72px);
      animation-timing-function: cubic-bezier(0.33, 0, 0.2, 1);
    }
    55% {
      transform: translateY(20px);
      animation-timing-function: cubic-bezier(0.2, 0, 0.1, 1);
    }
    100% {
      transform: none;
    }
  }
}
* {
  box-sizing: border-box;
}
html {
  /* Lenis owns smooth scrolling when it is available; native smooth scrolling
     would otherwise compete with its anchor animation. */
  scroll-behavior: auto;
  scroll-padding-top: 95px;
}
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: Arial, Helvetica, sans-serif;
  line-height: 1.6;
}
a {
  color: inherit;
  text-decoration: none;
}
button,
input {
  font: inherit;
}
button,
a,
input {
  outline-offset: 5px;
}
button {
  cursor: pointer;
}
header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 25px 5vw;
  border-bottom: 1px solid var(--line);
  gap: 20px;
}
.brand {
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 1px;
  display: flex;
  align-items: center;
  gap: 10px;
}
.brand-icon {
  display: inline-grid;
  place-items: center;
  background: var(--accent);
  color: var(--bg);
  width: 32px;
  height: 32px;
  font-size: 25px;
  letter-spacing: -3px;
  padding-right: 4px;
}
.brand-sub {
  color: var(--muted);
  font-size: 10px;
}
.text-link:hover {
  color: var(--accent);
}
main {
  max-width: 1600px;
  margin: auto;
}
.hero {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  background: var(--bg);
}
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(58% 68% at 76% 38%, #6c7bff2e, transparent 68%),
    radial-gradient(48% 58% at 18% 88%, #6c7bff17, transparent 70%);
}
.hero-inner {
  position: relative;
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  padding: 80px 5vw;
  /* The shallower layer of the pointer drift; .hero-title adds its own on top,
     so the headline sits closer to the viewer than the copy beneath it. */
  transform: translate3d(
    calc(var(--mx, 0) * 3px),
    calc(var(--my, 0) * 1.5px),
    0
  );
}
.hero-top {
  position: relative;
  display: flex;
  justify-content: space-between;
  gap: 20px;
  width: 100%;
  margin-bottom: 34px;
}
.eyebrow,
.small-note {
  font: 11px/1.6 monospace;
  letter-spacing: 1.5px;
}
.eyebrow {
  color: var(--accent);
}
.small-note {
  color: var(--muted);
}
h1 {
  font-size: clamp(46px, 6.6vw, 104px);
  line-height: 0.98;
  letter-spacing: -0.045em;
  font-weight: 400;
  text-transform: uppercase;
  margin: 0 0 40px;
}
em {
  font-family: Georgia, serif;
  color: var(--accent);
  font-weight: 400;
}
/* ---------- Kinetic headline ----------
   "Software that meets the real world." is authored as two .kt-line blocks of
   ordinary text. hero.js splits each line into words, gives every word its own
   mask, and slides it up from underneath. .armed is the parked state, .running
   the released one — script adds those and nothing else, so with scripting off
   the headline is simply the two lines of text it always was.

   The masks are what sell it: a word rising from below its own box rather than
   fading in reads as type being set, which suits a headline about software
   meeting physical things. The <em> words arrive slower and slightly wide, out
   of a deeper blur, so the serif lands as a settle rather than as more of the
   same move. */
.hero-title {
  /* Pointer drift, written by hero.js. Unset until the pointer moves, so the
     headline sits exactly where the layout puts it on load. */
  transform: translate3d(
    calc(var(--mx, 0) * 5px),
    calc(var(--my, 0) * 2.5px),
    0
  );
}
.kt-line {
  display: block;
  overflow: hidden;
}
.kt-w {
  display: inline-block;
  overflow: hidden;
}
.kt-w-i {
  /* Must be inline-block, not a bare inline span: transforms do not apply to
     non-replaced inline boxes, so as an inline the word would only ever blur
     and never actually rise. */
  display: inline-block;
}
.hero.armed .kt-w-i {
  transform: translateY(115%);
  filter: blur(3px);
  transition:
    transform 0.95s var(--ease-out) var(--d, 0s),
    filter 0.7s linear var(--d, 0s);
}
.hero.armed .kt-w-em .kt-w-i {
  transform: translateY(122%) scale(1.04);
  filter: blur(8px);
}
.hero.running .kt-w-i,
.hero.running .kt-w-em .kt-w-i {
  transform: none;
  filter: blur(0);
}
/* Everything around the headline is queued behind the words so four things
   never arrive at once. The handoff is deliberately not exact: the lead starts
   at 0.9s, while the last word is still settling, and reads as a continuation
   rather than a wait. Unlike the .reveal panels, these are interior elements —
   the panels stay fully opaque either way, which is why opacity is safe here. */
.hero-top,
.hero-lead,
.cta-row {
  transition:
    opacity 0.7s var(--ease-out) var(--enter-delay, 0s),
    transform 0.7s var(--ease-out) var(--enter-delay, 0s);
}
.hero.armed .hero-top {
  --enter-delay: 0.05s;
  opacity: 0;
  transform: translateY(14px);
}
.hero.armed .hero-lead {
  --enter-delay: 0.9s;
  opacity: 0;
  transform: translateY(18px);
}
.hero.armed .cta-row {
  --enter-delay: 1.02s;
  opacity: 0;
  transform: translateY(18px);
}
.hero.running .hero-top,
.hero.running .hero-lead,
.hero.running .cta-row {
  opacity: 1;
  transform: none;
}
p {
  color: var(--muted);
}
.hero-lead {
  font: 16px/1.8 monospace;
  color: var(--ink);
  max-width: 62ch;
  margin: 0 0 46px;
}
.cta-row {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
}
.cta {
  display: inline-flex;
  gap: 7px;
}
.cta span {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--ink);
  color: var(--bg);
  padding: 20px 32px;
  font: 12px monospace;
  letter-spacing: 1.5px;
  text-transform: uppercase;
}
.cta span + span {
  padding: 20px 26px;
}
.cta:hover span {
  background: var(--accent);
}
/* Download variant: outlined instead of filled, so the two hero actions don't
   read as one being the more important one. The outline is an inset shadow so
   the split button's shared middle edge stays clean. These rules must stay
   after .cta:hover or the hover fill wins on specificity. */
.cta-outline span {
  background: transparent;
  color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
}
.cta-outline:hover span {
  background: var(--accent);
  color: var(--bg);
  box-shadow: inset 0 0 0 1px var(--accent);
}
.primary {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: 45px;
  background: var(--accent);
  color: #141714;
  padding: 16px 22px;
  font-size: 13px;
  font-weight: 700;
  border: 0;
}
.primary:hover {
  background: #909cff;
}
.marquee {
  position: relative;
  border-top: 1px solid var(--line);
  padding: 22px 0;
  overflow: hidden;
  white-space: nowrap;
  font: 12px monospace;
  letter-spacing: 2px;
  color: var(--muted);
}
.marquee-track {
  display: flex;
  width: max-content;
  animation: marquee-scroll 42s linear infinite;
}
/* Hold the ticker still while the pointer is on it, so the list of practices
   can be read instead of only glimpsed. */
.marquee:hover .marquee-track {
  animation-play-state: paused;
}
.marquee-set {
  display: flex;
  align-items: center;
  gap: 34px;
  padding-right: 34px;
}
.marquee span {
  text-transform: uppercase;
}
.marquee i {
  font-style: normal;
  color: var(--accent);
}
@keyframes marquee-scroll {
  to {
    transform: translateX(-50%);
  }
}
.stats {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  align-content: center;
  gap: 40px;
  padding: 75px 5vw;
  border-bottom: 1px solid var(--line);
  background: var(--bg);
}
.stat-label {
  display: block;
  font: 11px/1.6 monospace;
  letter-spacing: 1.5px;
  color: var(--muted);
  margin-bottom: 16px;
}
.stat-value {
  display: block;
  font-size: clamp(46px, 5.4vw, 74px);
  line-height: 1;
  letter-spacing: -0.05em;
  font-weight: 500;
}
.plus {
  color: var(--accent);
  letter-spacing: 0;
  margin-left: 0.04em;
}
.section {
  padding: 90px 5vw;
  scroll-margin-top: 20px;
  background: var(--bg);
}
.section-title {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 25px;
  margin-bottom: 35px;
}
h2 {
  font-size: clamp(34px, 4vw, 62px);
  font-weight: 500;
  line-height: 1.1;
  letter-spacing: -0.05em;
  margin: 18px 0 0;
}
.section-title p {
  font-size: 14px;
}
.filters {
  display: flex;
  gap: 8px;
  margin: 35px 0;
  flex-wrap: wrap;
}
.filters button {
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--line);
  padding: 9px 16px;
  font-size: 12px;
  border-radius: 30px;
}
.filters .active {
  background: var(--ink);
  color: var(--bg);
  border-color: var(--ink);
}
.filters small {
  margin-left: 12px;
}
.projects {
  background: var(--bg);
}
/* Each row of the old 2x3 grid is now its own pinned panel. */
.work-panel {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-content: center;
  gap: 26px;
  padding: 30px 5vw;
  background: var(--bg);
}
.work-panel:nth-child(1) {
  z-index: 1;
}
.work-panel:nth-child(2) {
  z-index: 2;
}
.work-panel:nth-child(3) {
  z-index: 3;
}
.work-panel:nth-child(4) {
  z-index: 4;
}
.project {
  border: 1px solid var(--line);
  background: #191b18;
}
.project-art {
  height: 230px;
  padding: 22px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  position: relative;
  overflow: hidden;
  background-color: #252a24;
  background-image:
    linear-gradient(#ffffff06 1px, transparent 1px),
    linear-gradient(90deg, #ffffff06 1px, transparent 1px);
  background-size: 24px 24px;
  background-position: 0 0;
  /* Pairs with the .art-mark lift below: the grid drifting a little reads as
     the card tipping toward the viewer, where a plain colour change would just
     read as a flash. */
  transition: background-position 0.6s var(--ease-out);
}
.art-label,
.art-bottom {
  font: 10px monospace;
  letter-spacing: 1px;
  color: #c0c8b9;
  z-index: 1;
}
.art-mark {
  font:
    90px/1 Georgia,
    serif;
  text-align: center;
  color: #bfcdb4;
  text-shadow: 0 0 55px #a4d17c33;
  transition:
    transform 0.55s var(--ease-out),
    filter 0.55s var(--ease-out);
}
/* Hovering a project card lifts and brightens its mark. Gated on hover: hover
   so it cannot stick on after a tap on a touch screen. */
@media (hover: hover) {
  .project:hover .art-mark {
    transform: translateY(-6px) scale(1.05);
    filter: brightness(1.15);
  }
  .project:hover .project-art {
    background-position: 14px 14px;
  }
}
.art-02 {
  background-color: #292722;
}
.art-02 .art-mark {
  color: #ddd3ab;
}
.art-03 {
  background-color: #272530;
}
.art-03 .art-mark {
  color: #c8c1e2;
}
.art-04 {
  background-color: #292b27;
}
.art-04 .art-mark {
  font: 40px monospace;
  color: #ccd8ba;
  letter-spacing: 10px;
  border: 1px solid #879574;
  padding: 14px;
  align-self: center;
}
.art-05 {
  background-color: #2b2721;
}
.art-05 .art-mark {
  color: #ffb57e;
}
.art-06 {
  background-color: #23302c;
}
.art-06 .art-mark {
  color: #aad8c3;
}
.art-07 {
  background-color: #212a31;
}
.art-07 .art-mark {
  font: 52px/1 monospace;
  color: #a9d9e8;
  letter-spacing: -4px;
}
.art-08 {
  background-color: #2b252c;
}
.art-08 .art-mark {
  font: 56px/1 Georgia, serif;
  color: #d8b8df;
}
.project-body {
  padding: 27px;
}
h3 {
  font-size: 27px;
  letter-spacing: -1px;
  margin: 12px 0 5px;
  font-weight: 500;
}
h4 {
  font-size: 14px;
  font-weight: 400;
  color: #dedfd4;
  margin: 0 0 12px;
}
.project-body p {
  font-size: 14px;
  min-height: 67px;
}
.project-actions {
  display: flex;
  justify-content: space-between;
  border-top: 1px solid var(--line);
  padding-top: 20px;
  margin-top: 24px;
  font-size: 12px;
}
.case {
  background: transparent;
  border: 0;
  padding: 0;
  color: var(--ink);
  display: flex;
  gap: 35px;
  align-items: center;
}
.case:hover,
.project-actions a:hover {
  color: var(--accent);
}
.about {
  background: #20221e;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 50px;
}
.about-copy p {
  font-size: 15px;
}
.about-copy .lead,
.lead {
  font-size: 23px;
  color: var(--ink);
  line-height: 1.5;
}
.text-link {
  font-size: 13px;
  border-bottom: 1px solid var(--line);
  padding-bottom: 5px;
}
.stack {
  grid-column: 1/-1;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 30px;
  border-top: 1px solid var(--line);
  padding-top: 30px;
}
.summary-points {
  grid-column: 1/-1;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 30px;
  border-top: 1px solid var(--line);
  padding-top: 26px;
}
.summary-points span {
  font: 10px monospace;
  letter-spacing: 1px;
  color: var(--accent);
}
.summary-points p {
  margin: 8px 0 0;
  font-size: 13px;
}
.stack span {
  font: 11px monospace;
  color: var(--accent);
  letter-spacing: 1px;
}
.stack p {
  font-size: 13px;
}
.experience {
  background: #191b18;
}
.timeline {
  border-top: 1px solid var(--line);
}
.role {
  display: grid;
  grid-template-columns: 0.85fr 1fr 1.7fr;
  gap: 28px;
  padding: 22px 0;
  border-bottom: 1px solid var(--line);
}
.role-meta,
.education-card > span,
.community-card > span {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font: 10px/1.55 monospace;
  letter-spacing: 0.8px;
  color: var(--accent);
}
.role h3,
.leadership h3 {
  font-size: 18px;
  margin: 0 0 3px;
  letter-spacing: -0.5px;
}
.role h4 {
  margin: 0;
  font-size: 12px;
}
.role p {
  margin: 0;
  font-size: 13px;
}
.leadership {
  background: #20221e;
  display: grid;
  grid-template-columns: 0.9fr 1.1fr;
  gap: 50px;
  align-content: center;
}
.leadership-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 24px;
}
.education-card,
.community-card {
  border-top: 1px solid var(--line);
  padding-top: 20px;
}
.education-card > span,
.community-card > span {
  margin-bottom: 14px;
}
.education-card p {
  margin: 6px 0;
  font-size: 13px;
}
.community-card ul {
  list-style: none;
  padding: 0;
  margin: 0;
}
.community-card li {
  display: flex;
  justify-content: space-between;
  gap: 18px;
  padding: 9px 0;
  border-bottom: 1px solid #383a3570;
  font-size: 13px;
}
.community-card small {
  flex: 0 0 auto;
  color: var(--muted);
  font: 10px monospace;
}
.repo-tools {
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
  margin: 35px 0;
}
.repo-tools label {
  font-size: 12px;
}
.repo-tools input {
  color: var(--ink);
  background: #20221e;
  border: 1px solid var(--line);
  padding: 13px 16px;
  width: 330px;
  max-width: 100%;
  border-radius: 0;
}
.repo-tools > span {
  font: 11px monospace;
  color: var(--muted);
  margin-left: auto;
}
.repo-list {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: 35px;
}
.repo {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: 18px;
  border-bottom: 1px solid var(--line);
  padding: 17px 0;
  font-size: 13px;
}
.repo:hover {
  color: var(--accent);
}
.repo small {
  color: var(--muted);
  font: 11px monospace;
}
.repo-more {
  background: transparent;
  color: var(--ink);
  border: 1px solid var(--line);
  padding: 14px 24px;
  margin-top: 32px;
  font: 12px monospace;
  letter-spacing: 1px;
  text-transform: uppercase;
}
.repo-more:hover {
  color: var(--accent);
  border-color: var(--accent);
}
.repo-note {
  font-size: 11px;
  margin-top: 24px;
  max-width: 650px;
}
.contact {
  background: var(--accent);
  color: var(--bg);
}
.contact .eyebrow,
.contact em {
  color: #12163a;
}
.contact h2 {
  font-size: clamp(50px, 7vw, 100px);
}
.contact-bottom {
  display: flex;
  align-items: center;
  gap: 30px;
  margin-top: 50px;
  flex-wrap: wrap;
}
.email {
  font-size: clamp(16px, 2vw, 26px);
  margin-right: auto;
}
.contact .text-link {
  border-color: #3f4794;
}
footer {
  padding: 27px 5vw;
  display: flex;
  justify-content: space-between;
  gap: 20px;
  color: var(--muted);
  font: 10px monospace;
  flex-wrap: wrap;
}
dialog {
  background: var(--panel);
  color: var(--ink);
  border: 1px solid var(--line);
  padding: 45px;
  width: 700px;
  max-width: 92vw;
  max-height: 88vh;
}
dialog::backdrop {
  background: #000b;
  backdrop-filter: blur(5px);
}
dialog h3 {
  font-size: 17px;
  margin-top: 28px;
}
dialog p {
  font-size: 14px;
}
dialog .lead {
  font-size: 20px;
}
.close {
  position: absolute;
  right: 16px;
  top: 8px;
  background: none;
  color: var(--ink);
  border: 0;
  font-size: 30px;
}
.skip {
  position: absolute;
  left: 10px;
  top: -100px;
  z-index: 10;
  background: var(--accent);
  color: #111;
  padding: 10px;
}
.skip:focus {
  top: 10px;
}
[hidden] {
  display: none !important;
}
@media (min-width: 1600px) {
  header,
  footer {
    padding-left: max(5vw, calc((100vw - 1440px) / 2));
    padding-right: max(5vw, calc((100vw - 1440px) / 2));
  }
}
@media (max-width: 700px) {
  header {
    padding: 20px 6vw;
  }
  .brand-sub {
    display: none;
  }
  .hero-inner {
    padding: 60px 6vw;
  }
  .hero-top {
    margin-bottom: 24px;
  }
  .hero-top .small-note {
    display: none;
  }
  h1 {
    margin-bottom: 28px;
    font-size: clamp(30px, 7.6vw, 52px);
  }
  .hero-lead {
    font-size: 14px;
    margin-bottom: 32px;
  }
  .cta span {
    padding: 16px 22px;
    font-size: 11px;
    letter-spacing: 1px;
  }
  .marquee {
    font-size: 10px;
    letter-spacing: 1.5px;
    padding: 16px 0;
  }
  .stats {
    grid-template-columns: repeat(2, 1fr);
    gap: 40px 22px;
    padding: 55px 6vw;
  }
  .stat-label {
    letter-spacing: 1px;
    margin-bottom: 12px;
  }
  .section {
    padding: 60px 6vw;
  }
  .section-title {
    align-items: flex-start;
    flex-direction: column;
    gap: 5px;
  }
  /* Two cards stacked are taller than a phone screen, so the split panels
     stop pinning below 700px and the section scrolls as one column. */
  .work-panel {
    position: static;
    height: auto;
    overflow: visible;
    padding: 0;
    grid-template-columns: 1fr;
    z-index: auto;
  }
  .project-art {
    height: 210px;
  }
  .project-body p {
    min-height: 0;
  }
  .about {
    grid-template-columns: 1fr;
    gap: 25px;
  }
  .role,
  .leadership {
    grid-template-columns: 1fr;
    gap: 10px;
  }
  .role {
    padding: 18px 0;
  }
  .role-meta {
    flex-direction: row;
    justify-content: space-between;
  }
  .community-card li {
    flex-direction: column;
    gap: 2px;
  }
  .stack {
    grid-template-columns: 1fr;
    gap: 5px;
  }
  .summary-points {
    grid-template-columns: 1fr;
    gap: 16px;
  }
  .repo-list {
    grid-template-columns: 1fr;
  }
  .repo-tools {
    gap: 10px;
  }
  .repo-tools label {
    width: 100%;
  }
  .repo-tools input {
    width: 100%;
  }
  .repo-tools > span {
    margin-left: 0;
  }
  .contact-bottom {
    gap: 20px;
  }
  .email {
    width: 100%;
  }
  footer span:nth-child(2) {
    display: none;
  }
  dialog {
    padding: 30px 24px;
  }
}
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  .marquee-track {
    animation: none;
  }
  .js .reveal {
    transform: none;
    transition: none;
    animation: none;
  }
}
