/* ===========================================================================
   SECTIONS
   Each section uses a different layout family. In page order:
     hero          layered overlap
     about         split portrait and text
     skills        grouped tile grid
     projects      pinned horizontal pan
     github        instrument panel
     journey       scroll drawn timeline
     services      asymmetric bento
     testimonials  quote strip
     notes         editorial list
     contact       full width terminal form
   =========================================================================== */

/* --------------------------------------------------------------- reveals */
/* Set by JS only when GSAP is confirmed alive, so a failed script load can
   never leave content invisible. */

.js-motion [data-reveal="rise"] {
  opacity: 0;
  transform: translate3d(0, 28px, 0);
}

.js-motion [data-reveal="fade"] {
  opacity: 0;
}

.js-motion [data-reveal="mask"] > .line-inner {
  transform: translate3d(0, 105%, 0);
}

.line {
  display: block;
  overflow: hidden;
  padding-bottom: 0.12em;
  margin-bottom: -0.12em;
}

.line-inner {
  display: block;
  will-change: transform;
}

/* ------------------------------------------------------------------- nav */

/* Fully transparent. The bar sits over a full-bleed video, so any panel
   behind it would read as a band across the footage. Legibility comes from
   the text shadow in .nav[data-over-hero] instead.

   Translated by GSAP on scroll direction, so no transition on transform
   here: the two would fight. */
.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: var(--z-nav);
  height: 68px;
  display: flex;
  align-items: center;
  background: transparent;
  border-bottom: 1px solid transparent;
  will-change: transform;
  transition: border-color var(--d-fast) var(--e-transition),
    color var(--d-base) var(--e-transition);
}

/* Over the video: white with a soft shadow, which survives both the bright
   sky at the start of the clip and the near-black at the end. */
.nav[data-over-hero="true"] .nav__brand,
.nav[data-over-hero="true"] .nav__link,
.nav[data-over-hero="true"] .icon-btn {
  color: #fff;
  text-shadow: 0 1px 14px rgba(0, 0, 0, 0.55), 0 1px 3px rgba(0, 0, 0, 0.4);
}

.nav[data-over-hero="true"] .nav__brand span {
  color: #fff;
}

.nav[data-over-hero="true"] .icon-btn {
  border-color: rgba(255, 255, 255, 0.45);
}

.nav[data-over-hero="true"] .icon-btn:hover {
  border-color: #fff;
  color: #fff;
}

.nav[data-over-hero="true"] .nav__link::after {
  background: #fff;
}

.nav[data-scrolled="true"]:not([data-over-hero="true"]) {
  border-bottom-color: var(--rule);
}

.nav__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  width: 100%;
}

.nav__brand {
  font-family: var(--font-mono);
  font-size: var(--t-small);
  font-weight: 700;
  color: var(--ink);
  text-decoration: none;
  letter-spacing: -0.02em;
  white-space: nowrap;
}

.nav__brand span {
  color: var(--blue);
}

.nav__links {
  display: flex;
  align-items: center;
  gap: clamp(0.75rem, 1.8vw, 2rem);
}

.nav__link {
  font-family: var(--font-mono);
  font-size: var(--t-small);
  color: var(--ink-soft);
  text-decoration: none;
  white-space: nowrap;
  position: relative;
  padding-block: 0.35rem;
}

.nav__link::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  height: 1px;
  width: 100%;
  background: var(--blue);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform var(--d-fast) var(--e-transition);
}

.nav__link:hover,
.nav__link[aria-current="true"] {
  color: var(--ink);
}

.nav__link:hover::after,
.nav__link[aria-current="true"]::after {
  transform: scaleX(1);
  transform-origin: left;
}

.nav__actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.icon-btn {
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: var(--r);
  color: var(--ink-soft);
  cursor: pointer;
  font-size: 1.05rem;
  transition: color var(--d-fast) var(--e-transition),
    border-color var(--d-fast) var(--e-transition),
    transform var(--d-fast) var(--e-spring);
}

.icon-btn:hover {
  color: var(--blue);
  border-color: var(--blue);
  transform: translateY(-1px);
}

.nav__toggle {
  display: none;
}

@media (max-width: 860px) {
  .nav__links {
    position: fixed;
    inset: 68px 0 auto 0;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    /* The bar is transparent but the open panel cannot be, or its links sit
       unreadable on top of the video. */
    background: color-mix(in srgb, var(--paper) 94%, transparent);
    backdrop-filter: saturate(180%) blur(18px);
    border-bottom: 1px solid var(--rule);
    padding: 0.5rem var(--gutter) 1.5rem;
    transform: translate3d(0, -12px, 0);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--d-fast) var(--e-transition),
      transform var(--d-fast) var(--e-entrance), visibility var(--d-fast);
  }

  .nav[data-open="true"] .nav__links {
    opacity: 1;
    visibility: visible;
    transform: none;
  }

  .nav__link {
    width: 100%;
    padding-block: 0.85rem;
    border-bottom: 1px solid var(--rule);
    font-size: 1rem;
  }

  .nav__link::after {
    display: none;
  }

  .nav[data-over-hero="true"][data-open="true"] .nav__link {
    color: var(--ink);
    text-shadow: none;
  }

  .nav__toggle {
    display: grid;
  }
}

/* ----------------------------------------------------------- video hero */
/* A pinned stage holding a canvas, with two overlays keyed to footage time.
   The stage is the pin target, so it must be exactly one viewport tall:
   100svh rather than 100vh, because the dynamic toolbars on mobile browsers
   make 100vh taller than the space actually visible. */

.vhero {
  position: relative;
  background: #000;
}

.vhero__stage {
  position: relative;
  width: 100vw;
  height: 100svh;
  overflow: hidden;
  background: #000;
}

.vhero__canvas,
.vhero__poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}

/* ---- Overlay A: the name ---- */

.vhero__name {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  place-items: center;
  padding-inline: var(--gutter);
  pointer-events: none;
  text-align: center;
}

.vhero__name-text {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(3rem, 11vw, 11rem);
  line-height: 0.92;
  letter-spacing: -0.04em;
  color: #fff;
  /* The name sits on bright sky for most of its life, so it needs its own
     contrast rather than relying on the footage being dark. */
  text-shadow: 0 2px 40px rgba(0, 0, 0, 0.45), 0 1px 10px rgba(0, 0, 0, 0.35);
  text-wrap: balance;
}

/* SplitText wraps each character; the mask is what the rise animates out of. */
.vhero__char {
  display: inline-block;
  will-change: transform, opacity;
}

.vhero__word {
  display: inline-block;
  will-change: transform, opacity;
}

/* ---- Overlay B: the services block ---- */

.vhero__services {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: grid;
  place-items: center;
  padding: var(--gutter);
  pointer-events: none;
}

/* Exactly the colour of the sequence's last frame, so the cross-fade from
   footage to layout has no edge. */
.vhero__services-bg {
  position: absolute;
  inset: 0;
  background: #020002;
}

.vhero__services-inner {
  position: relative;
  text-align: center;
  max-width: 54rem;
}

.vhero__services-heading {
  margin: 0 0 clamp(1.5rem, 4vw, 2.75rem);
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(2rem, 6.5vw, 4.5rem);
  line-height: 1;
  letter-spacing: -0.04em;
  color: #fff;
}

/* A single centred column rather than a wrapping row. Five items of very
   different lengths wrap into ragged 2/3 rows, which reads as an accident;
   stacked, the block has one clean edge and the same shape at every width. */
.vhero__services-list {
  display: grid;
  justify-items: center;
  gap: clamp(0.45rem, 1.4vw, 0.85rem);
  list-style: none;
  margin: 0;
  padding: 0;
}

.vhero__services-list li {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(0.95rem, 2.1vw, 1.5rem);
  letter-spacing: -0.015em;
  color: #fff;
  will-change: transform, opacity;
}

/* Static fallback: reduced motion, Save-Data, or no GSAP. The stage becomes
   an ordinary image with the name over it, and the services block sits
   beneath on its own black panel rather than floating over the picture. */
.vhero[data-vhero-static="true"] .vhero__stage {
  height: 70svh;
}

.vhero[data-vhero-static="true"] .vhero__services {
  position: static;
  min-height: 70svh;
  padding-block: clamp(4rem, 12vw, 8rem);
}

.vhero[data-vhero-static="true"] .vhero__name-text {
  font-size: clamp(2.25rem, 9vw, 7rem);
}

@media (max-width: 860px) {
  .vhero__name-text {
    font-size: clamp(2.5rem, 13vw, 5rem);
  }
}

/* -------------------------------------------------------- section header */

/* ch on the wrapper resolved against the inherited body size, not the 56px
   headline inside it, which squeezed section titles to four lines. The measure
   belongs on the title, in its own em-relative units. */
.sec-head {
  max-width: 46rem;
  margin-bottom: clamp(2.5rem, 5vw, 4.5rem);
}

.sec-head__title {
  font-size: var(--t-h2);
  letter-spacing: -0.04em;
  max-width: 20ch;
}

.sec-head__body {
  margin-top: 1rem;
  color: var(--ink-soft);
  max-width: 52ch;
  font-size: 1.02rem;
}

/* ----------------------------------------------------------------- about */

.about__grid {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(2rem, 5vw, 5rem);
  align-items: start;
}

.about__portrait {
  position: relative;
  border-radius: var(--r);
  overflow: hidden;
  background: var(--paper-sunk);
  aspect-ratio: 4 / 5;
  max-width: 100%;
}

.about__portrait img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Resolves from blue duotone to full colour as the section scrolls past. */
  filter: grayscale(1) contrast(1.05);
  will-change: filter;
}

.about__portrait::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--blue);
  mix-blend-mode: color;
  opacity: var(--duotone, 0.85);
  pointer-events: none;
}

/* Monogram stand-in, shown only until a real portrait is uploaded. Built
   from the page's own blueprint language so it reads as a designed panel
   rather than a missing image. */
.about__portrait--empty {
  display: grid;
  place-items: center;
  background:
    linear-gradient(var(--blue-line) 1px, transparent 1px) 0 0 / 100% 2.5rem,
    linear-gradient(90deg, var(--blue-line) 1px, transparent 1px) 0 0 / 2.5rem 100%,
    var(--paper-sunk);
  border: 1px solid var(--rule);
}

.about__monogram {
  font-family: var(--font-display);
  font-size: clamp(4rem, 9vw, 7rem);
  font-weight: 600;
  letter-spacing: -0.06em;
  color: var(--blue);
  opacity: 0.9;
}

.about__meta {
  margin-top: 1rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.5rem;
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--ink-faint);
}

/* Docstring treatment: the about copy is presented the way the man who wrote
   it would document a module. The gutter rail is a real structural element. */
.about__doc {
  border-left: 1px solid var(--rule);
  padding-left: clamp(1.25rem, 3vw, 2.5rem);
}

.about__doc-open {
  font-family: var(--font-mono);
  font-size: var(--t-small);
  color: var(--green);
  margin-bottom: 1.25rem;
}

.about__doc h2 {
  font-size: var(--t-h2);
  letter-spacing: -0.04em;
  margin-bottom: 1.5rem;
}

.about__doc p {
  color: var(--ink-soft);
  max-width: var(--maxw-text);
  margin-bottom: 1.15rem;
  font-size: 1.05rem;
  line-height: 1.65;
}

.about__doc p strong {
  color: var(--ink);
  font-weight: 500;
}

.about__doc-close {
  font-family: var(--font-mono);
  font-size: var(--t-small);
  color: var(--ink-faint);
  margin-top: 1.5rem;
}

@media (max-width: 900px) {
  .about__grid { grid-template-columns: 1fr; }
  .about__portrait { max-width: 380px; }
  .about__doc { border-left: 0; padding-left: 0; }
}

/* ----------------------------------------------------------- disciplines */
/* The five disciplines as a definition list, which is what they are: a term
   and its meaning. Hairline-separated rows rather than cards, so the section
   reads as one block instead of five competing boxes. */

.disciplines {
  margin-top: clamp(3.5rem, 7vw, 6rem);
}

.disciplines__title {
  font-size: var(--t-h3);
  letter-spacing: -0.03em;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--rule);
  margin-bottom: clamp(1.5rem, 3vw, 2.5rem);
}

.disciplines__list {
  display: grid;
  gap: 0;
  margin: 0;
}

.discipline {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(1rem, 3vw, 3rem);
  padding-block: clamp(1.1rem, 2.2vw, 1.6rem);
  border-bottom: 1px solid var(--rule);
}

.discipline:last-child {
  border-bottom: 0;
}

.discipline__term {
  font-family: var(--font-mono);
  font-size: var(--t-small);
  font-weight: 500;
  color: var(--blue);
  margin: 0;
}

.discipline__def {
  margin: 0;
  color: var(--ink-soft);
  font-size: 1rem;
  line-height: 1.6;
  max-width: 60ch;
}

@media (max-width: 760px) {
  .discipline {
    grid-template-columns: 1fr;
    gap: 0.45rem;
  }
}

/* ---------------------------------------------------------------- skills */

.skills__group + .skills__group {
  margin-top: clamp(2.5rem, 5vw, 4rem);
}

.skills__group-head {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  padding-bottom: 0.85rem;
  margin-bottom: 1.5rem;
  position: relative;
}

/* Drawn by the timeline rather than present from the start, so the eye is led
   into the row. --rule-scale is animated from 0 to 1. */
.skills__group-head::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--rule);
  transform: scaleX(var(--rule-scale, 1));
  transform-origin: left;
}

.skills__group-name {
  font-family: var(--font-mono);
  font-size: var(--t-small);
  font-weight: 500;
  color: var(--ink);
}

.skills__group-blurb {
  font-size: var(--t-small);
  color: var(--ink-faint);
}

.skills__grid {
  display: grid;
  /* auto-fit, not auto-fill: empty tracks collapse so the row never ends in
     blank cells when a category has fewer entries than the row can hold. */
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--r);
  overflow: hidden;
}

.tile {
  position: relative;
  background: var(--paper);
  padding: 1.25rem 1.15rem;
  min-height: 124px;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  isolation: isolate;
  transition: background-color var(--d-fast) var(--e-transition);
}

.tile::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--blue-wash);
  opacity: 0;
  transition: opacity var(--d-fast) var(--e-transition);
}

.tile:hover::before {
  opacity: 1;
}

.tile__logo {
  width: 34px;
  height: 34px;
  position: relative;
}

.tile__logo img {
  position: absolute;
  inset: 0;
  width: 34px;
  height: 34px;
  object-fit: contain;
  transition: opacity var(--d-fast) var(--e-transition);
}

/* The grey mark is what ships in the HTML; showcase.js cross-fades to the
   brand colour as the tile settles. Hover is left to the lift and the
   proficiency line, so the two are not competing for the same signal. */
.tile__logo img.is-color {
  opacity: 0;
}

.tile:hover {
  transform: translateY(-2px);
}

.tile {
  transition: background-color var(--d-fast) var(--e-transition),
    transform var(--d-fast) var(--e-spring);
}

.tile__name {
  font-family: var(--font-mono);
  font-size: 0.95rem;
  font-weight: 500;
  letter-spacing: -0.02em;
}

/* Proficiency as evidence, never as a percentage bar. */
.tile__context {
  font-size: var(--t-micro);
  color: var(--ink-faint);
  line-height: 1.45;
  margin-top: auto;
  opacity: 0;
  transform: translate3d(0, 6px, 0);
  transition: opacity var(--d-fast) var(--e-transition),
    transform var(--d-fast) var(--e-entrance);
}

.tile:hover .tile__context,
.tile:focus-within .tile__context {
  opacity: 1;
  transform: none;
}

@media (hover: none) {
  .tile__context { opacity: 1; transform: none; }
}

.tile--core .tile__name {
  color: var(--blue);
}

@media (max-width: 560px) {
  .skills__grid { grid-template-columns: repeat(2, 1fr); }
  .tile { min-height: 132px; padding: 1.1rem 0.9rem; }
}

/* -------------------------------------------------------------- projects */

.projects {
  position: relative;
  background: var(--paper-sunk);
}

.projects__head {
  padding-block: clamp(4rem, 8vw, 7rem) clamp(2rem, 4vw, 3rem);
}

.pan {
  position: relative;
  overflow: hidden;
}

.pan__track {
  display: flex;
  gap: clamp(1.25rem, 2.5vw, 2rem);
  padding-inline: var(--gutter);
  align-items: stretch;
  will-change: transform;
}

@media (min-width: 1025px) {
  /* While the section is pinned the track occupies the viewport, so the cards
     sit centred in it rather than clinging to the top with dead space below. */
  .pan__track {
    min-height: 100dvh;
    align-items: center;
  }

  .pcard {
    max-height: 84dvh;
  }
}

.pcard {
  flex: 0 0 min(78vw, 620px);
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--r);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  transition: border-color var(--d-base) var(--e-transition),
    box-shadow var(--d-base) var(--e-transition);
}

.pcard:hover {
  border-color: var(--blue);
  box-shadow: var(--shadow-3);
  color: inherit;
}

.pcard__shot {
  position: relative;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--paper-sunk);
  border-bottom: 1px solid var(--rule);
}

.pcard__shot img {
  width: 100%;
  height: 114%;
  object-fit: cover;
  will-change: transform;
}

.pcard__body {
  padding: clamp(1.25rem, 2.2vw, 1.9rem);
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  flex: 1;
}

.pcard__top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
}

.pcard__title {
  font-size: clamp(1.4rem, 2.1vw, 1.9rem);
  letter-spacing: -0.035em;
}

.pcard__year {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--ink-faint);
}

.pcard__tagline {
  color: var(--blue);
  font-family: var(--font-mono);
  font-size: var(--t-small);
}

.pcard__problem {
  color: var(--ink-soft);
  font-size: 0.97rem;
  line-height: 1.6;
}

.pcard__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: auto;
  padding-top: 0.6rem;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.3rem 0.55rem;
  border: 1px solid var(--rule);
  border-radius: var(--r);
  font-family: var(--font-mono);
  font-size: 0.7rem;
  color: var(--ink-soft);
  background: var(--paper);
}

.chip img {
  width: 13px;
  height: 13px;
}

.pcard__stats {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--rule);
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--ink-faint);
  flex-wrap: wrap;
}

.pcard__stats i {
  font-size: 0.9rem;
  vertical-align: -1px;
}

.lang-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  display: inline-block;
  margin-right: 0.35rem;
  vertical-align: -1px;
}

.pcard__go {
  color: var(--blue);
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}

.pan__hint {
  display: none;
}

@media (max-width: 1024px) {
  /* No scroll hijack on touch. The gallery becomes an ordinary scroll-snap
     rail the visitor drives with their thumb. */
  .pan__track {
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    padding-bottom: 1.5rem;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
    min-height: 0;
    align-items: stretch;
  }

  .pcard {
    flex-basis: min(86vw, 460px);
    scroll-snap-align: start;
  }

  .pan__hint {
    display: block;
    padding-inline: var(--gutter);
    padding-bottom: 3rem;
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    color: var(--ink-faint);
  }
}

/* ---------------------------------------------------------------- github */

.gh__grid {
  display: grid;
  /* minmax(0, ...) rather than a bare fr: the default automatic minimum is
     min-content, and the heatmap's 27 columns were pushing the whole page
     wider than the phone viewport through this track. */
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: clamp(1.5rem, 3vw, 3rem);
  align-items: start;
}

.gh__grid > *,
.gh__stack > *,
.panel {
  min-width: 0;
}

.panel {
  border: 1px solid var(--rule);
  border-radius: var(--r);
  background: var(--paper-raised);
  padding: clamp(1.25rem, 2.2vw, 1.75rem);
}

.panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.25rem;
}

.panel__title {
  font-family: var(--font-mono);
  font-size: var(--t-small);
  font-weight: 500;
}

.panel__note {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--ink-faint);
}

/* Contribution grid */
.heat {
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: repeat(7, 1fr);
  gap: 3px;
  /* The grid is wider than a phone. It scrolls inside its own panel rather
     than widening the page. */
  overflow-x: auto;
  max-width: 100%;
  padding-bottom: 0.25rem;
  scrollbar-width: thin;
}

.heat__cell {
  width: 11px;
  height: 11px;
  border-radius: 1px;
  background: var(--paper-sunk);
  border: 1px solid var(--rule);
}

.heat__cell[data-level="1"] { background: #c8f0d8; border-color: #b3e8c8; }
.heat__cell[data-level="2"] { background: #7fdca6; border-color: #6bd398; }
.heat__cell[data-level="3"] { background: #34c176; border-color: #2bb26a; }
.heat__cell[data-level="4"] { background: var(--green); border-color: var(--green); }

[data-theme="dark"] .heat__cell[data-level="1"] { background: #10361f; border-color: #16482a; }
[data-theme="dark"] .heat__cell[data-level="2"] { background: #1a6438; border-color: #1f7642; }
[data-theme="dark"] .heat__cell[data-level="3"] { background: #26a05a; border-color: #2bb266; }
[data-theme="dark"] .heat__cell[data-level="4"] { background: #35d47a; border-color: #35d47a; }

.heat__legend {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.9rem;
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--ink-faint);
}

/* Language ring */
.ring-wrap {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 2.5vw, 2rem);
  flex-wrap: wrap;
}

.ring {
  flex: none;
  width: 148px;
  height: 148px;
  transform: rotate(-90deg);
}

.ring circle {
  fill: none;
  stroke-width: 14;
  stroke-linecap: butt;
}

.ring__legend {
  display: grid;
  gap: 0.5rem;
  flex: 1 1 150px;
  min-width: 0;
}

.ring__row {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  font-family: var(--font-mono);
  font-size: var(--t-micro);
}

.ring__row span:last-child {
  margin-left: auto;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

/* Commit feed */
.gh__stack {
  display: grid;
  gap: clamp(1.5rem, 3vw, 3rem);
  min-width: 0;
}

.commit {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0.75rem;
  padding-block: 0.6rem;
  border-top: 1px solid var(--rule);
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  align-items: baseline;
}

.commit:first-child {
  border-top: 0;
}

.commit__sha {
  color: var(--blue);
}

.commit__msg {
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.commit__meta {
  grid-column: 2;
  color: var(--ink-faint);
}

/* Skeleton state, shown until the data resolves so the section has a real
   loading feel rather than popping into place. */
.skeleton {
  background: linear-gradient(
    90deg,
    var(--paper-sunk) 0%,
    var(--rule) 50%,
    var(--paper-sunk) 100%
  );
  background-size: 200% 100%;
  animation: sweep 1.4s var(--e-transition) infinite;
  border-radius: var(--r);
  color: transparent;
}

@keyframes sweep {
  to { background-position: -200% 0; }
}

.gh__repo {
  text-decoration: none;
  color: inherit;
  transition: background-color var(--d-fast) var(--e-transition);
}

.gh__repo:hover {
  background: var(--blue-wash);
  color: inherit;
}

.gh__repo .commit__msg {
  color: var(--blue);
}

.gh__quiet {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--ink-faint);
  line-height: 1.7;
  max-width: 52ch;
  padding-block: 0.5rem 0.25rem;
}

.gh__empty {
  border: 1px dashed var(--rule-strong);
  border-radius: var(--r);
  padding: 2rem;
  text-align: center;
  color: var(--ink-soft);
  font-family: var(--font-mono);
  font-size: var(--t-small);
}

@media (max-width: 900px) {
  .gh__grid { grid-template-columns: 1fr; }
}

/* --------------------------------------------------------------- journey */

.tl {
  position: relative;
  padding-left: clamp(2rem, 5vw, 4rem);
  /* clip, not hidden: it contains the cards' entrance offset without turning
     the timeline into a scroll container. */
  overflow-x: clip;
}

/* Pre-animation state lives here rather than in a style attribute, so it only
   applies when the motion system is actually alive. */
.js-motion [data-tl-card] {
  opacity: 0;
  transform: translate3d(18px, 0, 0);
}

.tl__rail {
  position: absolute;
  left: clamp(0.4rem, 1.4vw, 1rem);
  top: 6px;
  bottom: 6px;
  width: 1px;
  background: var(--rule);
}

.tl__progress {
  position: absolute;
  inset: 0 auto 0 0;
  width: 1px;
  background: var(--blue);
  transform: scaleY(0);
  transform-origin: top;
  will-change: transform;
}

.tl__item {
  position: relative;
  padding-bottom: clamp(2.5rem, 5vw, 4rem);
}

.tl__item:last-child {
  padding-bottom: 0;
}

.tl__node {
  position: absolute;
  left: calc(clamp(2rem, 5vw, 4rem) * -1 + clamp(0.4rem, 1.4vw, 1rem) - 4px);
  top: 8px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--paper);
  border: 1px solid var(--rule-strong);
  transition: background-color var(--d-base) var(--e-transition),
    border-color var(--d-base) var(--e-transition),
    box-shadow var(--d-base) var(--e-transition);
}

/* Node ignites when the drawn rail reaches it. Green because it reports a
   real state change: this milestone is now reached. */
.tl__item.is-lit .tl__node {
  background: var(--green-signal);
  border-color: var(--green-signal);
  box-shadow: 0 0 0 4px var(--green-wash);
}

.tl__period {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--ink-faint);
}

.tl__title {
  font-size: var(--t-h3);
  letter-spacing: -0.03em;
  margin-top: 0.35rem;
}

.tl__org {
  font-family: var(--font-mono);
  font-size: var(--t-small);
  color: var(--blue);
  margin-top: 0.3rem;
}

.tl__desc {
  color: var(--ink-soft);
  margin-top: 0.7rem;
  max-width: 58ch;
  font-size: 0.98rem;
}

.tl__now {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--green);
  margin-top: 0.6rem;
}

/* -------------------------------------------------------------- services */
/* Bento with auto-fit tracks, so the cell count always equals the item count
   no matter how many services the control panel holds. The lead cell carries
   a real image so this is not a row of white text cards on white ground. */

.bento {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--r);
  overflow: hidden;
}

.bento__cell {
  background: var(--paper);
  padding: clamp(1.4rem, 2.4vw, 2.2rem);
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  min-height: 210px;
  transition: background-color var(--d-fast) var(--e-transition);
}

.bento__cell:hover {
  background: var(--paper-raised);
}

.bento__cell--feature {
  grid-column: 1 / -1;
  padding: 0;
  min-height: clamp(240px, 34vw, 340px);
  position: relative;
  overflow: hidden;
}

.bento__cell--feature img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.bento__feature-copy {
  position: relative;
  z-index: 1;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: clamp(1.5rem, 3vw, 2.5rem);
  /* Two axis scrim. The single bottom-up gradient left the body copy sitting
     on bright sky and failing contrast; this guarantees white text clears AA
     wherever the photograph happens to be light. */
  background:
    linear-gradient(to right, rgba(8, 12, 20, 0.9) 0%, rgba(8, 12, 20, 0.62) 48%, rgba(8, 12, 20, 0.12) 100%),
    linear-gradient(to top, rgba(8, 12, 20, 0.92) 0%, rgba(8, 12, 20, 0.62) 45%, rgba(8, 12, 20, 0.18) 100%);
  color: #fff;
}

.bento__feature-copy h3 {
  font-size: clamp(1.5rem, 3vw, 2.25rem);
  letter-spacing: -0.035em;
  color: #fff;
}

.bento__feature-copy p {
  margin-top: 0.6rem;
  max-width: 42ch;
  color: #fff;
  font-size: 1rem;
}

.bento__icon {
  font-size: 1.5rem;
  color: var(--blue);
  line-height: 1;
}

.bento__cell h3 {
  font-size: var(--t-h3);
  letter-spacing: -0.03em;
}

.bento__cell p {
  color: var(--ink-soft);
  font-size: 0.97rem;
  line-height: 1.6;
}

.bento__deliverable {
  margin-top: auto;
  padding-top: 0.9rem;
  border-top: 1px solid var(--rule);
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--ink-faint);
}

@media (max-width: 560px) {
  /* Explicit single column collapse rather than relying on auto-fit alone. */
  .bento { grid-template-columns: 1fr; }
  .bento__cell { min-height: 0; }
}

/* ---------------------------------------------------------- testimonials */
/* Two rails travelling in opposite directions, scrubbed by scroll. The
   counter-motion is what reads as depth; both rails moving the same way would
   look like one wide row that failed to line up. */

.tm-rails {
  display: grid;
  gap: clamp(1rem, 2vw, 1.5rem);
  margin-top: clamp(2.5rem, 5vw, 4rem);
}

.tm-rail {
  overflow: hidden;
  /* Fade the ends so cards enter and leave instead of being cut off by a
     hard edge, which is what makes a marquee look unfinished. */
  mask-image: linear-gradient(
    to right, transparent, #000 7%, #000 93%, transparent
  );
  -webkit-mask-image: linear-gradient(
    to right, transparent, #000 7%, #000 93%, transparent
  );
}

.tm-rail__track {
  display: flex;
  gap: clamp(1rem, 2vw, 1.5rem);
  width: max-content;
  will-change: transform;
}

.tm-card {
  flex: 0 0 min(80vw, 24rem);
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: clamp(1.25rem, 2.4vw, 1.75rem);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--r);
}

.tm-card__quote {
  margin: 0;
  font-size: 1rem;
  line-height: 1.55;
  letter-spacing: -0.012em;
  color: var(--ink);
}

.tm-card__who {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  margin-top: auto;
  padding-top: 0.75rem;
  border-top: 1px solid var(--rule);
}

.tm-card__name {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--ink);
}

.tm-card__role {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--ink-faint);
  overflow-wrap: anywhere;
}

.tm-avatar {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  object-fit: cover;
  flex: none;
  background: var(--paper-sunk);
}

.tm-avatar--mono {
  display: grid;
  place-items: center;
  background: var(--blue-wash);
  border: 1px solid var(--blue-line);
  color: var(--blue);
  font-family: var(--font-mono);
  font-size: 0.8rem;
  font-weight: 500;
}

/* No scroll-driven travel under reduced motion: the rails become ordinary
   horizontal scrollers the visitor drives. */
@media (prefers-reduced-motion: reduce) {
  .tm-rail {
    overflow-x: auto;
    mask-image: none;
    -webkit-mask-image: none;
  }

  .tm-rail__track {
    transform: none !important;
  }
}

/* ----------------------------------------------------------------- notes */

.notes__list {
  border-top: 1px solid var(--rule);
}

.note {
  display: grid;
  grid-template-columns: 8rem minmax(0, 1fr) auto;
  gap: clamp(1rem, 3vw, 2.5rem);
  align-items: baseline;
  padding-block: clamp(1.25rem, 2.4vw, 1.9rem);
  border-bottom: 1px solid var(--rule);
  text-decoration: none;
  color: inherit;
  transition: padding-left var(--d-base) var(--e-entrance);
}

.note:hover {
  padding-left: 0.9rem;
  color: inherit;
}

.note__date {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--ink-faint);
}

.note__title {
  font-size: clamp(1.1rem, 1.8vw, 1.45rem);
  letter-spacing: -0.03em;
}

.note__excerpt {
  color: var(--ink-soft);
  font-size: 0.95rem;
  margin-top: 0.4rem;
  max-width: 62ch;
}

.note__read {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--blue);
  white-space: nowrap;
}

@media (max-width: 700px) {
  .note { grid-template-columns: 1fr; gap: 0.4rem; }
  .note__read { display: none; }
}

/* --------------------------------------------------------------- contact */

.contact {
  background: var(--paper-sunk);
}

.contact__grid {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(2rem, 5vw, 5rem);
  align-items: start;
}

.contact__aside h2 {
  font-size: var(--t-h2);
  letter-spacing: -0.04em;
}

.contact__aside p {
  margin-top: 1rem;
  color: var(--ink-soft);
  max-width: 40ch;
}

.contact__links {
  margin-top: 2rem;
  display: grid;
  gap: 0.1rem;
}

.contact__link {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding-block: 0.7rem;
  border-bottom: 1px solid var(--rule);
  font-family: var(--font-mono);
  font-size: var(--t-small);
  text-decoration: none;
  color: var(--ink);
}

.contact__link i {
  color: var(--blue);
  font-size: 1.05rem;
}

.contact__link:hover {
  color: var(--blue);
}

@media (max-width: 900px) {
  .contact__grid { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------------- footer */

.footer {
  border-top: 1px solid var(--rule);
  padding-block: clamp(2.5rem, 5vw, 4rem);
}

.footer__inner {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 2rem;
  flex-wrap: wrap;
}

.footer__note {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--ink-faint);
  max-width: 40ch;
  line-height: 1.6;
}

.footer__links {
  display: flex;
  gap: 1.5rem;
  flex-wrap: wrap;
}

.footer__links a {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--ink-soft);
  text-decoration: none;
}

.footer__links a:hover {
  color: var(--blue);
}

/* ----------------------------------------------------- project detail page */

.pd-hero {
  padding-top: clamp(8rem, 12vw, 11rem);
  padding-bottom: clamp(2.5rem, 5vw, 4rem);
}

.pd-hero__top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 2rem;
  flex-wrap: wrap;
}

.pd-hero__title {
  font-size: clamp(2.5rem, 6vw, 4.75rem);
  letter-spacing: -0.045em;
  margin-top: 1rem;
}

.pd-hero__tagline {
  font-family: var(--font-mono);
  color: var(--blue);
  margin-top: 0.9rem;
  font-size: var(--t-small);
}

.pd-shot {
  border: 1px solid var(--rule);
  border-radius: var(--r);
  overflow: hidden;
  background: var(--paper-sunk);
  aspect-ratio: 16 / 9;
}

.pd-shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.pd-grid {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 4fr);
  gap: clamp(2rem, 5vw, 5rem);
  align-items: start;
}

.pd-body h2 {
  font-size: var(--t-h3);
  letter-spacing: -0.03em;
  margin-bottom: 1rem;
}

.pd-body h2:not(:first-child) {
  margin-top: clamp(2.5rem, 4vw, 3.5rem);
}

.pd-body p {
  color: var(--ink-soft);
  max-width: var(--maxw-text);
  margin-bottom: 1.1rem;
  line-height: 1.65;
}

.pd-challenge {
  border-left: 2px solid var(--blue);
  padding-left: 1.25rem;
  margin-bottom: 1.5rem;
}

.pd-challenge h3 {
  font-family: var(--font-mono);
  font-size: var(--t-small);
  font-weight: 500;
}

.pd-challenge p {
  margin-top: 0.4rem;
  margin-bottom: 0;
  font-size: 0.97rem;
}

.pd-facts {
  position: sticky;
  top: 92px;
  border: 1px solid var(--rule);
  border-radius: var(--r);
  padding: 1.5rem;
  background: var(--paper-raised);
}

.pd-fact {
  padding-block: 0.7rem;
  border-bottom: 1px solid var(--rule);
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  font-family: var(--font-mono);
  font-size: var(--t-micro);
}

.pd-fact:last-of-type {
  border-bottom: 0;
}

.pd-fact span:first-child {
  color: var(--ink-faint);
}

.pd-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 1.25rem;
}

.pd-gallery figure {
  margin: 0;
}

.pd-gallery img {
  width: 100%;
  border: 1px solid var(--rule);
  border-radius: var(--r);
}

.pd-gallery figcaption {
  margin-top: 0.6rem;
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--ink-faint);
}

@media (max-width: 900px) {
  .pd-grid { grid-template-columns: 1fr; }
  .pd-facts { position: static; }
}

/* ------------------------------------------------------------ error page */

.errpage {
  min-height: 100dvh;
  display: grid;
  place-items: center;
  text-align: center;
  padding: var(--gutter);
}

.errpage__code {
  font-family: var(--font-mono);
  font-size: clamp(3rem, 10vw, 6rem);
  color: var(--blue);
  letter-spacing: -0.05em;
}
