/* ============================================================
   matthewlehman.dev
   Plain CSS. No build step. Edit the tokens below to retune.
   ============================================================ */

/* ---------- 0. TYPEFACES ------------------------------------
   Self-hosted, latin subset only, 160 KB total across six files.
   No Google Fonts request — one less third party, and no
   render-blocking round trip to another origin.
   ------------------------------------------------------------ */
@font-face {
  font-family: 'Instrument Serif';
  font-style: normal; font-weight: 400; font-display: swap;
  src: url('fonts/instrumentserif-400.woff2') format('woff2');
}
/* Instrument Serif italic isn't loaded — nothing on the page is
   set in italic. If you add an <em>, re-add the face and drop
   instrumentserif-400-italic.woff2 back into /fonts. */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal; font-weight: 400; font-display: swap;
  src: url('fonts/ibmplexsans-400.woff2') format('woff2');
}
/* Plex Sans 500 is deliberately absent — nothing on the page uses
   it, so shipping it would be 45 KB of dead weight. */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal; font-weight: 400; font-display: swap;
  src: url('fonts/ibmplexmono-400.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal; font-weight: 500; font-display: swap;
  src: url('fonts/ibmplexmono-500.woff2') format('woff2');
}

/* ---------- 1. TOKENS ---------------------------------------
   Retune the whole site's purple from right here.
   ------------------------------------------------------------ */
:root {
  /* Core palette
     NOTE ON THE PURPLE: the obvious picks here would be #8b5cf6 and
     #a855f7 — but those are literally Tailwind's violet-500 and
     purple-500, the framework defaults every AI site builder ships
     with. Using them verbatim is the single loudest "generated by
     a bot" signal a site can send. These sit deliberately between
     the two: more saturated, pushed toward magenta, off the default
     swatch. Same family, not the same token. */
  --bg:            #0a0810;   /* near-black, cool purple tint */
  --bg-raise:      #100c1a;   /* panels / raised surfaces */
  --bg-sink:       #07050c;   /* footer / recessed areas */
  --accent:        #9542f0;   /* primary purple */
  --accent-light:  #b47cff;   /* lilac, for hovers */
  --accent-deep:   #6a1fb5;   /* pressed / deep states */
  /* TEXT-SAFE ACCENT. --accent above is the brand purple and stays
     exactly as it is — but as *text* it lands at 3.9-4.2:1 on these
     backgrounds, under the 4.5:1 WCAG AA floor for body-size type.
     This is the same hue lifted just far enough to clear it (4.6-5.0:1
     on every surface the site actually uses). Rule of thumb: --accent
     for borders, bars, rules and gradients; --accent-text for anything
     a person reads. */
  --accent-text:   #a355f3;
  --border:        #241d33;   /* hairline, muted purple-gray */
  --border-lit:    #3b2f57;   /* hairline on hover/focus */
  --text:          #e9e6f2;
  --muted:         #8b83a3;

  /* Keep in sync with --accent. Lets us fade the accent cheaply. */
  --accent-rgb:    149, 66, 240;

  /* Type — a deliberate pairing, not a fallback stack.
     Instrument Serif: high-contrast editorial serif. Carries the
       photography half of the site and gives display type a voice
       a system grotesque can't.
     IBM Plex Sans / Mono: one superfamily, drawn for engineering
       documentation. Carries the developer half — Plex Mono takes
       every label, URL and nav item, Plex Sans takes body copy.
     The two faces map onto the two disciplines the site is about.
     That's the reason for the pairing, not decoration. */
  --font-display: 'Instrument Serif', 'Iowan Old Style', Georgia, serif;
  --font-sans:    'IBM Plex Sans', -apple-system, BlinkMacSystemFont,
                  'Segoe UI', Roboto, sans-serif;
  --font-mono:    'IBM Plex Mono', ui-monospace, 'Cascadia Mono',
                  Consolas, monospace;

  /* Type scale — ~1.25 ratio, fluid at the top end */
  --t-xs:   .6875rem;
  --t-sm:   .8125rem;
  --t-base: .9375rem;
  --t-md:   1.0625rem;
  --t-lg:   clamp(1.25rem, 2vw, 1.5rem);
  --t-xl:   clamp(1.75rem, 3.2vw, 2.5rem);
  --t-2xl:  clamp(2.25rem, 5.5vw, 4rem);
  --t-3xl:  clamp(2.75rem, 10vw, 8rem);

  /* Spacing scale — margins and padding come from here so the
     vertical rhythm stays consistent between sections. */
  --s-1: .25rem;  --s-2: .5rem;   --s-3: .75rem;  --s-4: 1rem;
  --s-5: 1.5rem;  --s-6: 2rem;    --s-7: 3rem;    --s-8: 4rem;
  --s-9: 6rem;    --s-10: 8rem;

  /* Layout */
  --wrap:      1180px;
  --gutter:    clamp(1.25rem, 5vw, 3rem);
  --section-y: clamp(5rem, 12vh, 9rem);

  /* Motion */
  --ease:  cubic-bezier(.22, 1, .36, 1);
  --fast:  .2s;
  --slow:  .6s;

  /* HERO SCROLL LENGTH ---------------------------------------
     Total height of the scroll track. The video is scrubbed
     across (this value - 100vh) of scrolling.
       150vh = very quick, hero is gone almost immediately
       200vh = current — snappy, one screen of scrubbing
       300vh = slower
       500vh = long cinematic fall
     ---------------------------------------------------------- */
  --hero-scroll: 200vh;
}

/* ---------- 2. RESET ---------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  /* Anchors land below the fixed nav */
  scroll-padding-top: 5rem;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

h1, h2, h3 {
  margin: 0;
  line-height: 1.1;
  font-weight: 400;
  letter-spacing: -0.02em;
  /* Evens out ragged line breaks in headings */
  text-wrap: balance;
}
p {
  margin: 0 0 1.1em;
  /* Stops single-word orphans on the last line */
  text-wrap: pretty;
}
p:last-child { margin-bottom: 0; }
ul { margin: 0; padding: 0; list-style: none; }
img, video { display: block; max-width: 100%; }
a { color: var(--accent-light); text-decoration: none; }

::selection { background: rgba(var(--accent-rgb), .35); color: #fff; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

/* ---------- 3. SHARED PIECES -------------------------------- */
.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* Crisp small-caps label — used all over */
.label {
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 500;
}
.label--num { color: var(--accent-text); }

/* Fixed dot-grid texture.
   Sits ABOVE the opaque section backgrounds (which have to be opaque
   so they can cover the sticky hero video) but below the nav, so the
   grid reads as one continuous overlay across the whole page.
   Turn it down or off with `opacity` here. */
.texture {
  position: fixed;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  background-image: radial-gradient(rgba(var(--accent-rgb), .22) 1px, transparent 1px);
  background-size: 34px 34px;
  /* Fade the grid out toward the edges so it reads as texture, not wallpaper */
  -webkit-mask-image: radial-gradient(ellipse 85% 65% at 50% 45%, #000 10%, transparent 100%);
          mask-image: radial-gradient(ellipse 85% 65% at 50% 45%, #000 10%, transparent 100%);
  opacity: .55;
}

/* ---------- 4. NAV ------------------------------------------ */
.nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  border-bottom: 1px solid transparent;
  transition: background var(--fast) linear,
              border-color var(--fast) linear,
              backdrop-filter var(--fast) linear;
}
/* .is-stuck is added by JS once you scroll past the first viewport-ish */
.nav.is-stuck {
  background: rgba(10, 8, 16, .78);
  backdrop-filter: blur(14px) saturate(1.4);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
  border-bottom-color: var(--border);
}

.nav__inner {
  max-width: var(--wrap);
  margin-inline: auto;
  padding: 1.1rem var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

/* The ML monogram. Transparent PNG, so it sits straight on the
   nav background with no plate behind it. */
.wordmark {
  display: block;
  line-height: 0;
  transition: filter var(--fast) var(--ease);
}
.wordmark img {
  display: block;
  height: clamp(19px, 2.2vw, 25px);
  width: auto;
}
.wordmark:hover { filter: brightness(1.25); }

.nav__links { display: flex; gap: 2rem; }
.nav__links a {
  position: relative;
  font-family: var(--font-mono);
  font-size: .8125rem;
  letter-spacing: .06em;
  color: var(--muted);
  padding: .25rem 0;
  transition: color var(--fast) var(--ease);
}
.nav__links a::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0;
  width: 100%; height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform .3s var(--ease);
}
.nav__links a:hover,
.nav__links a.is-active { color: var(--text); }
.nav__links a:hover::after,
.nav__links a.is-active::after { transform: scaleX(1); transform-origin: left; }
.nav__links a.is-active { color: var(--accent-light); }

/* Mobile toggle (hidden on desktop) */
.nav__toggle {
  display: none;
  width: 40px; height: 40px;
  padding: 0;
  background: none;
  border: 1px solid var(--border);
  border-radius: 4px;
  cursor: pointer;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
}
.nav__toggle span {
  display: block;
  width: 16px; height: 1.5px;
  background: var(--text);
  transition: transform var(--fast) var(--ease), opacity var(--fast) linear;
}
.nav__toggle[aria-expanded="true"] span:first-child { transform: translateY(3.25px) rotate(45deg); }
.nav__toggle[aria-expanded="true"] span:last-child  { transform: translateY(-3.25px) rotate(-45deg); }

.nav__mobile {
  display: none;
  flex-direction: column;
  padding: .5rem var(--gutter) 1.25rem;
  background: rgba(10, 8, 16, .96);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--border);
}
.nav__mobile a {
  font-family: var(--font-mono);
  font-size: .9rem;
  color: var(--text);
  padding: .8rem 0;
  border-bottom: 1px solid var(--border);
}
.nav__mobile a:last-child { border-bottom: none; }

/* ---------- 5. HERO ----------------------------------------- */
.hero-track {
  position: relative;
  height: var(--hero-scroll);   /* tall track = scrub distance */
  z-index: 1;
}

.hero-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;               /* dodges mobile URL-bar resize */
  overflow: hidden;
  background: var(--bg-sink);   /* no white flash before video paints */
}

.hero-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Video decodes to a slightly cool cast to sit inside the purple palette */
  filter: saturate(.9) contrast(1.05);
}

/* Darkening + vignette so text stays readable over any frame */
.hero-scrim {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 80% 60% at 50% 45%, transparent 0%, rgba(10, 8, 16, .55) 100%),
    linear-gradient(to bottom, rgba(10, 8, 16, .55) 0%, rgba(10, 8, 16, .15) 35%, rgba(10, 8, 16, .9) 100%);
}

/* Anchored to the lower-left of the frame, not floated dead centre.
   Centring everything is the single most template-looking move a
   hero can make; sitting the name on the baseline of the frame
   reads like a title card instead. */
.hero-overlay {
  position: absolute;
  inset: 0;
  z-index: 2;
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  display: grid;
  align-content: end;
  padding: 0 var(--gutter) clamp(var(--s-7), 11vh, var(--s-9));
  /* JS writes --hero-fade (1 → 0) as you scroll */
  opacity: var(--hero-fade, 1);
  transform: translateY(calc((1 - var(--hero-fade, 1)) * -32px));
  will-change: opacity, transform;
}

.hero-label {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-bottom: var(--s-4);
  color: var(--accent-light);
}
/* Short rule leading into the label — an anchor for the left edge */
.hero-label::before {
  content: "";
  width: clamp(28px, 5vw, 60px);
  height: 1px;
  background: currentColor;
  opacity: .55;
}

.hero-title {
  font-family: var(--font-display);
  font-size: clamp(2.75rem, 9vw, 6.5rem);
  font-weight: 400;          /* Instrument Serif ships one weight */
  line-height: .95;
  letter-spacing: -0.015em;
  /* THE ONE GLOW ON THE SITE. Everything else uses neutral shadow
     or a border for depth — a glow that appears once is an accent,
     a glow on every card is a template. */
  text-shadow:
    0 0 44px rgba(var(--accent-rgb), .28),
    0 2px 22px rgba(0, 0, 0, .7);
}

.hero-tagline {
  margin: var(--s-4) 0 0;
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  letter-spacing: .05em;
  color: var(--muted);
}

/* Asymmetric split once there's room: label across the top, name
   on the baseline at left, tagline sitting on the same baseline
   at right. */
@media (min-width: 900px) {
  .hero-overlay {
    grid-template-columns: auto 1fr;
    align-items: end;
    column-gap: var(--s-6);
  }
  .hero-label   { grid-column: 1 / -1; }
  .hero-title   { grid-column: 1; }
  .hero-tagline {
    grid-column: 2;
    justify-self: end;
    text-align: right;
    margin-bottom: .55em;
  }
}

/* ---------- 6. SECTIONS ------------------------------------- */
.section {
  position: relative;
  z-index: 2;
  padding-block: var(--section-y);
  background: var(--bg);
  border-top: 1px solid var(--border);
}
/* The section right after the hero covers the sticky video as it scrolls up */
#about { border-top: none; }

/* Base head. Each section then overrides it — they deliberately
   do NOT all look like the same ruled title bar. */
.section__head {
  display: flex;
  align-items: baseline;
  gap: var(--s-4);
  margin-bottom: clamp(var(--s-6), 6vw, var(--s-8));
  padding-bottom: var(--s-4);
  border-bottom: 1px solid var(--border);
}
.section__title {
  font-family: var(--font-display);
  font-size: var(--t-xl);
  font-weight: 400;
  letter-spacing: -0.01em;
}

/* ---------- 7. ABOUT ----------------------------------------
   Twelve-column grid. The head becomes a sticky rail pinned in
   the left margin while the copy scrolls past it, and the copy
   starts at column 4 — off-axis on purpose.
   ------------------------------------------------------------ */
#about .wrap {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--s-6);
  row-gap: var(--s-7);
}
#about .section__head {
  grid-column: 1 / span 2;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s-2);
  margin-bottom: 0;
  padding-bottom: 0;
  border-bottom: none;
  position: sticky;
  top: 7rem;
  align-self: start;
}
#about .section__title { font-size: var(--t-lg); }

/* Spans 7 of 12 rather than 9 — with the facts rail gone there's
   no second column, and a narrower measure reads better than one
   long line stretched across the grid. */
.about { grid-column: 4 / span 7; }
.about__lead {
  font-size: var(--t-md);
  line-height: 1.7;
  color: #cdc7dd;
  max-width: 58ch;
}
/* First line gets display treatment — a real entry point rather
   than four identical paragraphs. */
.about__lead p:first-child {
  font-family: var(--font-display);
  font-size: var(--t-xl);
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--text);
  margin-bottom: var(--s-5);
}
.about__lead strong { font-weight: 400; color: var(--text); }
.about__lead p:first-child strong { color: var(--accent-light); }


/* ---------- 8. WORK — HORIZONTAL SCROLL GALLERY -------------
   .work-track is tall (height is set by JS, computed from the
   real width of the panel row). .work-sticky pins one full
   viewport while the row translates sideways underneath.
   ------------------------------------------------------------ */
.work {
  position: relative;
  z-index: 2;
  background: var(--bg);
  border-top: 1px solid var(--border);
}

.work-track {
  position: relative;
  /* JS sets an explicit pixel height here. This is only the
     pre-JS fallback so the section isn't collapsed on load. */
  height: 300vh;
}

.work-sticky {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  display: flex;
  flex-direction: column;
  /* Clips the overflowing row. Safe on the sticky element itself —
     it's only *ancestors* with overflow that break stickiness. */
  overflow: hidden;
}

.work-head {
  flex-shrink: 0;
  display: flex;
  align-items: baseline;
  gap: 1rem;
  /* Top padding clears the fixed nav */
  padding-top: clamp(4.25rem, 9vh, 5.5rem);
  padding-bottom: 1.1rem;
  border-bottom: 1px solid var(--border);
}

/* Horizontal progress bar — replaces the old scroll cue */
.work-progress {
  position: relative;
  margin-left: auto;
  width: clamp(70px, 18vw, 220px);
  height: 2px;
  background: var(--border);
  border-radius: 2px;
  overflow: hidden;
}
.work-progress i {
  position: absolute;
  inset: 0;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
}

/* The row itself. width:max-content is what lets it exceed the
   viewport so there's something to translate. flex:1 hands it all
   the height left over under the head. */
.gallery {
  flex: 1;
  min-height: 0;
  display: flex;
  width: max-content;
  gap: 0;
  will-change: transform;
}

/* Each project fills the window. --panel-w is set by JS to
   window.innerWidth — 100vw would be a few px too wide on desktop
   because vw ignores the scrollbar, and that error compounds
   across four panels until nothing lines up with the edges. */
.panel {
  position: relative;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: var(--panel-w, 100vw);
  height: 100%;
  background: var(--bg-raise);
  /* Hairline divider between slides instead of a card outline */
  border: none;
  border-right: 1px solid var(--border);
  border-radius: 0;
  color: inherit;
  overflow: hidden;
  transition: background var(--fast) var(--ease);
}
.panel:last-child { border-right: none; }
/* Accent hairline that sweeps the top edge on hover */
.panel::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 1px;
  background: linear-gradient(to right, transparent, var(--accent), transparent);
  transform: scaleX(0);
  transition: transform .45s var(--ease);
  z-index: 2;
}
.panel:hover { background: #130e20; }
.panel:hover::before,
.panel:focus-visible::before { transform: scaleX(1); }

/* Small editorial index above the title. This used to be a huge
   outlined numeral in the corner — the preview window occupies
   that space now, and two big graphic elements fought. */
.panel__index {
  display: block;
  margin-bottom: .9rem;
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  letter-spacing: .18em;
  color: var(--accent-text);
}

/* Copy on the left, preview window on the right */
.panel__body {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
  align-items: center;
  gap: clamp(2rem, 5vw, 5rem);
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding: clamp(1.5rem, 5vh, 3rem) var(--gutter);
}
.panel__copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  min-width: 0;
}
/* Even-numbered panels flip: preview left, copy right. Four
   identical compositions in a row is the thing that makes a
   gallery feel generated. */
@media (min-width: 901px) {
  .panel:nth-child(even) .panel__body {
    grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
  }
  .panel:nth-child(even) .panel__copy { order: 2; }
  .panel:nth-child(even) .preview {
    order: 1;
    transform: perspective(1400px) rotateY(5deg);
    transform-origin: right center;
  }
  .panel:nth-child(even):hover .preview {
    transform: perspective(1400px) rotateY(0deg);
  }
}

.panel__title {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 3vw, 2.75rem);
  font-weight: 400;
  line-height: 1.04;
  letter-spacing: -0.015em;
  max-width: 14ch;
  margin-bottom: var(--s-4);
  transition: color var(--fast) var(--ease);
}
.panel:hover .panel__title { color: var(--accent-light); }
.panel__sub {
  display: block;
  margin-top: .35rem;
  font-family: var(--font-mono);
  font-size: .75rem;
  font-weight: 400;
  color: var(--muted);
  letter-spacing: .06em;
}
.panel__desc {
  font-size: clamp(.9375rem, 1.5vw, 1.125rem);
  color: var(--muted);
  max-width: 46ch;
  margin-bottom: 1.75rem;
}

.tags { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: 1.35rem; }
.tags li {
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
  padding: .3rem .6rem;
  border: 1px solid var(--border);
  border-radius: 3px;
  transition: border-color var(--fast) var(--ease), color var(--fast) var(--ease);
}
.panel:hover .tags li { border-color: var(--border-lit); color: #a79dc0; }

.panel__link {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  width: fit-content;
  margin-top: 1.75rem;
  padding-top: 1rem;
  border-top: 1px solid var(--border);
  font-family: var(--font-mono);
  font-size: .8125rem;
  letter-spacing: .04em;
  color: var(--accent-text);
  transition: color var(--fast) linear;
}
.panel:hover .panel__link { color: var(--accent-light); }
.panel__arrow { transition: transform var(--fast) var(--ease); }
.panel:hover .panel__arrow { transform: translate(2px, -2px); }

/* ---------- 8b. PREVIEW WINDOW ------------------------------
   A browser-chrome mockup. Ships with a CSS wireframe inside so
   it looks finished with no assets; drop a real screenshot in
   and the wireframe hides itself.
   ------------------------------------------------------------ */
.preview {
  position: relative;
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  background: var(--bg-sink);
  box-shadow: 0 40px 70px -40px rgba(0, 0, 0, .95);
  /* Slight turn away from the viewer that squares up on hover —
     the motion signals "this one's in focus", not decoration. */
  transform: perspective(1400px) rotateY(-5deg);
  transform-origin: left center;
  transition: transform .55s var(--ease), border-color var(--fast) var(--ease);
}
.panel:hover .preview {
  transform: perspective(1400px) rotateY(0deg);
  border-color: var(--border-lit);
}

.preview__bar {
  display: flex;
  align-items: center;
  gap: .7rem;
  padding: .55rem .7rem;
  background: #15101f;
  border-bottom: 1px solid var(--border);
}
.preview__dots { display: flex; gap: .3rem; flex-shrink: 0; }
.preview__dots i {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--border-lit);
}
.preview__url {
  flex: 1;
  min-width: 0;
  padding: .22rem .6rem;
  border-radius: 3px;
  background: var(--bg-sink);
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* 2:1 matches the real screenshots (2.09 and 2.23) closely enough
   that cover barely crops them, and it reads like an actual browser
   window rather than a squarish card. */
.preview__screen {
  position: relative;
  aspect-ratio: 2 / 1;
  overflow: hidden;
  background: linear-gradient(165deg, #100c1a, #0a0810);
}
.preview__screen img {
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: top center;
  transition: transform .6s var(--ease);
}
.panel:hover .preview__screen img { transform: scale(1.02); }
/* Uncommenting the <img> automatically retires the wireframe */
.preview__screen img + .wire { display: none; }

/* Badge variant: a square logo, not a screenshot. Centre and contain
   it — cover would crop a circular badge into a meaningless slice. */
/* Stays a plain block, not a grid: with `place-items: center` the
   image is no longer stretched, so `height: 100%` resolves to auto
   and the badge renders at its intrinsic 450px and overflows the
   box. object-fit:contain does the centring instead, and padding
   keeps the badge off the window edges. */
.preview__screen--badge {
  padding: clamp(.6rem, 2vh, 1.4rem);
  background:
    radial-gradient(circle at 50% 50%, rgba(var(--accent-rgb), .16), transparent 62%),
    linear-gradient(165deg, #100c1a, #0a0810);
}
.preview__screen--badge img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  filter: drop-shadow(0 14px 30px rgba(0, 0, 0, .65));
}
.panel:hover .preview__screen--badge img { transform: scale(1.03); }

/* ---- Placeholder wireframes ----
   Composable primitives, not four bespoke layouts. Each panel
   arranges them differently so the previews don't all read as
   the same generic box. */
.wire {
  --wire-gap: clamp(.35rem, 1vw, .6rem);
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  gap: var(--wire-gap);
  padding: clamp(.5rem, 1.4vw, .95rem);
}
.wire--split { flex-direction: row; }
.wire span, .wire div { border-radius: 3px; }

.w-bar {
  flex: 0 0 9%;
  min-height: 7px;
  background: var(--bg-raise);
  border: 1px solid var(--border);
}
.w-hero {
  flex: 1;
  background: rgba(var(--accent-rgb), .13);
  border: 1px solid rgba(var(--accent-rgb), .2);
}
.w-hero--short { flex: 0 0 30%; }

.w-row {
  display: flex;
  gap: var(--wire-gap);
  flex: 0 0 26%;
  min-height: 0;
}
.w-row--grow { flex: 1; }

.w-tile {
  flex: 1;
  min-width: 0;
  background: var(--bg-raise);
  border: 1px solid var(--border);
}
.w-tile--accent {
  flex: 1.7;
  background: rgba(var(--accent-rgb), .13);
  border-color: rgba(var(--accent-rgb), .2);
}

.w-side {
  flex: 0 0 20%;
  background: var(--bg-raise);
  border: 1px solid var(--border);
}
.w-main {
  flex: 1;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--wire-gap);
}
.w-stack {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--wire-gap);
}
.w-line {
  flex: 0 0 5px;
  background: var(--border-lit);
  border-radius: 2px;
}
.w-line--short { max-width: 60%; }

/* ---------- 9. SKILLS ---------------------------------------
   An index on the left, a detail panel on the right. Browsing the
   list re-renders the panel.
   ------------------------------------------------------------ */
/* Right-aligned under a top rule, mirroring About's left rail.
   (Not row-reverse — that renders the number after the title and
   reads as "Skills 03".) */
#skills .section__head {
  justify-content: flex-end;
  padding-bottom: 0;
  padding-top: var(--s-4);
  border-bottom: none;
  border-top: 1px solid var(--border);
}

.skills {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
  gap: clamp(var(--s-6), 5vw, var(--s-8));
  align-items: start;
}

/* ---- index ---- */
.skills__index {
  display: flex;
  flex-direction: column;
  border-top: 1px solid var(--border);
}
.skills__group {
  margin: var(--s-5) 0 var(--s-2);
  color: var(--accent-text);
}
.skills__group:first-child { margin-top: var(--s-4); }

.skill {
  position: relative;
  display: block;
  width: 100%;
  padding: var(--s-3) 0 var(--s-3) var(--s-4);
  border: 0;
  border-bottom: 1px solid var(--border);
  background: none;
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  letter-spacing: .01em;
  color: var(--muted);
  text-align: left;
  cursor: pointer;
  transition: color var(--fast) var(--ease),
              padding-left var(--fast) var(--ease);
}
/* Marker rule that grows in on the active row */
.skill::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  width: 9px;
  height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--fast) var(--ease);
}
.skill:hover { color: var(--text); }
.skill[aria-selected="true"] {
  color: var(--accent-light);
  padding-left: var(--s-6);
}
.skill[aria-selected="true"]::before { transform: scaleX(1); }

/* ---- detail panel ---- */
.skills__detail {
  position: relative;
  padding: clamp(var(--s-5), 3vw, var(--s-7));
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-raise);
  /* JS removes and re-adds .is-in on every change to restart this */
  opacity: 0;
  transform: translateY(8px);
  transition: opacity .35s var(--ease), transform .45s var(--ease);
}
.skills__detail.is-in { opacity: 1; transform: none; }

.skills__num {
  position: absolute;
  top: clamp(var(--s-4), 2.5vw, var(--s-6));
  right: clamp(var(--s-4), 2.5vw, var(--s-6));
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  letter-spacing: .18em;
  /* Was --border-lit, which is a hairline colour: 1.6:1 as type, i.e.
     barely there for anyone. --muted keeps it quiet and secondary
     while clearing AA. */
  color: var(--muted);
}
.skills__title {
  font-family: var(--font-display);
  font-size: var(--t-xl);
  font-weight: 400;
  line-height: 1.05;
  letter-spacing: -0.015em;
  margin-bottom: var(--s-3);
  padding-right: var(--s-6);
}
.skills__desc {
  font-size: var(--t-base);
  line-height: 1.7;
  color: var(--muted);
  max-width: 44ch;
  margin-bottom: var(--s-5);
}

.skills__used {
  padding-top: var(--s-4);
  border-top: 1px solid var(--border);
}
.skills__projects {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-4);
  margin-top: var(--s-3);
}
.skills__projects a {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  color: var(--text);
  border-bottom: 1px solid var(--border-lit);
  padding-bottom: 2px;
  transition: color var(--fast) var(--ease),
              border-color var(--fast) var(--ease);
}
.skills__projects a:hover {
  color: var(--accent-light);
  border-bottom-color: var(--accent);
}


/* ---------- 11. FOOTER --------------------------------------
   Carries the only contact details on the site now that the
   contact section is gone, so it gets real weight rather than
   being a copyright strip.
   ------------------------------------------------------------ */
.footer {
  position: relative;
  z-index: 2;
  background: var(--bg-sink);
  border-top: 1px solid var(--border);
  padding-block: clamp(var(--s-7), 10vh, var(--s-9));
}

.footer__reach {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-4) var(--s-6);
  padding-bottom: var(--s-6);
  border-bottom: 1px solid var(--border);
}

.footer__direct {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s-3);
  min-width: 0;
}

/* Phone sits under the email in mono — a step down in the
   hierarchy, not a competing headline. */
.footer__phone {
  font-family: var(--font-mono);
  font-size: clamp(.9375rem, 1.6vw, 1.125rem);
  letter-spacing: .06em;
  color: var(--muted);
  transition: color var(--fast) var(--ease);
}
.footer__phone:hover { color: var(--accent-light); }

/* Set in the display face — this is the call to action now */
.footer__email {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 4.5vw, 3rem);
  font-weight: 400;
  line-height: 1.05;
  letter-spacing: -0.015em;
  color: var(--text);
  /* Underline that grows in from the left on hover */
  background-image: linear-gradient(var(--accent), var(--accent));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1px;
  transition: background-size .45s var(--ease), color var(--fast) var(--ease);
}
.footer__email:hover {
  color: var(--accent-light);
  background-size: 100% 1px;
}

.footer__social {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  letter-spacing: .06em;
  color: var(--muted);
  white-space: nowrap;
  transition: color var(--fast) var(--ease);
}
.footer__social:hover { color: var(--accent-light); }

.footer__meta {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-4);
  padding-top: var(--s-5);
}

/* ---------- 11b. TYPE ANIMATIONS ----------------------------
   JS wraps each character of a [data-split] element in .char and
   sets --i to its index. The delay is derived from that, so the
   line rises left-to-right with no per-element bookkeeping.
   ------------------------------------------------------------ */
.split .char {
  display: inline-block;
  opacity: 0;
  transform: translateY(0.5em) rotate(2deg);
  transition: opacity .5s linear, transform .75s var(--ease);
  transition-delay: calc(var(--i) * 26ms);
}
.split.is-in .char {
  opacity: 1;
  transform: none;
}
/* Section titles are shorter, so they can run tighter */
.section__title.split .char { transition-delay: calc(var(--i) * 34ms); }

/* Autofocus brackets — two opposite corners snap in over a preview
   on hover, the way a camera confirms focus. On-theme, and it marks
   which panel the pointer is actually on. */
.preview__screen::before,
.preview__screen::after {
  content: "";
  position: absolute;
  z-index: 2;
  width: 22px;
  height: 22px;
  border: 1.5px solid var(--accent-light);
  opacity: 0;
  pointer-events: none;
  /* The brackets sit on top of arbitrary screenshots — without a
     shadow they vanish against a light one. */
  filter: drop-shadow(0 0 3px rgba(0, 0, 0, .9));
  transition: opacity .25s linear, transform .4s var(--ease);
}
.preview__screen::before {
  top: 12px; left: 12px;
  border-right: 0; border-bottom: 0;
  transform: translate(-8px, -8px);
}
.preview__screen::after {
  bottom: 12px; right: 12px;
  border-left: 0; border-top: 0;
  transform: translate(8px, 8px);
}
.panel:hover .preview__screen::before,
.panel:hover .preview__screen::after {
  opacity: 1;
  transform: none;
}

/* Frame counter in the work head */
.work-count {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.work-count em {
  font-style: normal;
  margin: 0 .35em;
  color: var(--muted);
}
/* Progress bar follows the counter, so it no longer needs the push */
.work-progress { margin-left: var(--s-4); }

/* Magnetic elements need a springy return when the pointer leaves.
   While the pointer is inside, JS writes an inline transform each
   frame and this transition is effectively bypassed. */
.is-magnetic {
  transition: transform .45s cubic-bezier(.34, 1.56, .64, 1);
}

/* Footer clock */
.footer__local { display: inline-flex; gap: .6rem; align-items: baseline; }
.footer__clock {
  color: var(--accent-light);
  font-variant-numeric: tabular-nums;
}

/* ---------- 12. SCROLL REVEAL ------------------------------- */
/* Kept deliberately small. The identical fade-and-slide-up applied
   to every element on a page is one of the most recognisable
   generated-site tells; at 10px it reads as the copy settling
   rather than as an effect, and the staggered cascade that was
   here is gone (it only delayed reading). */
.reveal {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity .45s var(--ease), transform .45s var(--ease);
}
.reveal.is-visible { opacity: 1; transform: none; }

/* ---------- 13. RESPONSIVE ---------------------------------- */
/* ---- Work panels below 900px: stack, don't hide ----
   The panel is a fixed height (one viewport) with overflow hidden,
   so stacking copy above the preview only works if the vertical
   budget actually adds up. Everything below is trimmed to make
   room: tighter margins, smaller padding, capped preview. */
@media (max-width: 900px) {
  .panel__body {
    grid-template-columns: 1fr;
    align-content: center;
    gap: clamp(1.1rem, 3vh, 1.75rem);
    padding-block: clamp(1rem, 3vh, 2rem);
  }
  .preview {
    max-width: 520px;
    /* The 3D turn eats horizontal space and reads as a rendering
       glitch on a narrow screen. */
    transform: none;
  }
  .panel:hover .preview { transform: none; }

  .panel__index { margin-bottom: .5rem; }
  .panel__title { margin-bottom: .6rem; }
  .panel__desc  { margin-bottom: 1rem; }
  .tags         { margin-bottom: 0; }
  .panel__link  { margin-top: 1rem; padding-top: .75rem; }
}

/* Landscape phones and short windows: there is genuinely no room
   for both, so the copy wins and the preview steps out. */
@media (max-width: 900px) and (max-height: 700px) {
  .preview { display: none; }
}

/* ---- Short viewports (landscape phones, split-screen windows) ----
   Panels are one viewport tall with overflow hidden, so when the
   viewport is short the copy gets clipped at the bottom. Measured
   at 844x390 the content ran 298px inside a 272px panel. These
   reclaim the difference: a slimmer nav, a tighter head, and less
   air between the copy blocks. */
@media (max-height: 620px) {
  .nav__inner { padding-top: .55rem; padding-bottom: .55rem; }
  .nav__toggle { width: 34px; height: 34px; }

  .work-head {
    padding-top: 3.5rem;
    padding-bottom: .6rem;
  }
  .panel__body {
    padding-block: .5rem;
    gap: .75rem;
  }
  /* The index is the most expendable line when space is this tight */
  .panel__index { display: none; }
  .panel__title {
    font-size: clamp(1.25rem, 3vw, 1.75rem);
    margin-bottom: .5rem;
  }
  .panel__desc {
    font-size: .9375rem;
    line-height: 1.5;
    margin-bottom: .75rem;
  }
  .panel__link { margin-top: .75rem; padding-top: .5rem; }
}

/* The About rail and the twelve-column grid only make sense with
   room for a margin column. Below this it's a single stack. */
@media (max-width: 980px) {
  #about .wrap {
    display: block;
  }
  #about .section__head {
    position: static;
    flex-direction: row;
    align-items: baseline;
    gap: var(--s-4);
    margin-bottom: var(--s-6);
    padding-bottom: var(--s-4);
    border-bottom: 1px solid var(--border);
  }
}

@media (max-width: 860px) {
  .about__lead p:first-child { font-size: var(--t-lg); }
  #skills .section__head {
    flex-direction: row;
    justify-content: flex-start;
    text-align: left;
  }
  /* Index above, panel below — side-by-side gets too narrow to read */
  .skills { grid-template-columns: 1fr; }
}

@media (max-width: 680px) {
  :root {
    /* 11px uppercase mono is fine on a desktop monitor and mean on a
       phone. 12px is the practical floor for small caps at arm's
       length — this lifts every .label, counter and caption at once. */
    --t-xs: .75rem;
    /* Shorter scrub on phones — less thumb-mileage to get through the hero */
    --hero-scroll: 170vh;
  }
  .nav__links { display: none; }
  .nav__toggle { display: flex; }
  .nav__mobile[data-open="true"] { display: flex; }
  /* The footage is dark to begin with, and object-fit:cover crops a
     16:9 frame into a tall portrait viewport — you end up zoomed
     into the darkest part of the lens. Lift the video and ease off
     the scrim so there's actually something to see on a phone. */
  .hero-video { filter: saturate(.95) contrast(1.02) brightness(1.45); }
  .hero-scrim {
    background:
      radial-gradient(ellipse 95% 70% at 50% 42%, transparent 0%, rgba(10, 8, 16, .32) 100%),
      linear-gradient(to bottom, rgba(10, 8, 16, .42) 0%, rgba(10, 8, 16, .04) 30%, rgba(10, 8, 16, .88) 100%);
  }

  .footer__meta { flex-direction: column; gap: .35rem; }
  .footer__reach { flex-direction: column; align-items: flex-start; }
  /* Hide the progress bar on phones — the head row gets cramped */
  .work-progress { display: none; }
  .panel__title { max-width: none; }
}

/* ---------- 13b. TOUCH TARGETS ------------------------------
   Several links here are single-line text: visually correct, but
   only ~20px tall, well under the 44px Apple and Google both ask
   for. Rather than padding them — which would push the footer
   apart and drag the email's animated underline away from the
   text — an invisible pseudo-element expands the hit area while
   the type stays exactly where it is.

   Scoped to `pointer: coarse` so mouse users keep the tighter
   visual rhythm.
   ------------------------------------------------------------ */
@media (pointer: coarse) {
  /* Small caps at 11px is a width-independent problem — it's about
     viewing distance, not viewport size. A phone held in landscape
     is 844px wide and still needs the bump. */
  :root { --t-xs: .75rem; }

  .wordmark,
  .footer__email,
  .footer__phone,
  .footer__social,
  .skills__projects a,
  .nav__links a,
  .panel__link { position: relative; }

  .wordmark::after,
  .footer__email::after,
  .footer__phone::after,
  .footer__social::after,
  .skills__projects a::after,
  .panel__link::after {
    content: "";
    position: absolute;
    inset: -13px -8px;
  }
  /* .nav__links a::after is already the underline, so the expander
     goes on ::before here. */
  .nav__links a::before {
    content: "";
    position: absolute;
    inset: -12px -10px;
  }

  /* Menu button back to a full 44px even on short viewports, where
     the landscape rules otherwise shrink it to 34. */
  .nav__toggle { width: 44px; height: 44px; }

  /* Roomier rows in the skills index — it's the one list people
     actually tap through. */
  .skill { padding-block: var(--s-4); }

  /* Hover-only affordances don't apply on touch; the tilt in
     particular just looks like a rendering mistake when it never
     resolves. */
  .preview { transform: none; }
  .preview__screen::before,
  .preview__screen::after { display: none; }
}

/* ---------- 14. REDUCED MOTION ------------------------------
   Kill the scrub entirely: the track collapses to one screen and
   the video sits on its first frame / poster.
   ------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  :root { --hero-scroll: 100vh; }
  html { scroll-behavior: auto; }
  .reveal { opacity: 1; transform: none; transition: none; }
  /* Split text is never applied under reduced motion (JS skips it),
     but guard the styles too in case the preference flips at runtime */
  .split .char { opacity: 1; transform: none; transition: none; }
  .preview__screen::before,
  .preview__screen::after { display: none; }

  /* Work gallery falls back to a plain wrapped grid — no pinning,
     no sideways travel. JS skips setting the track height too. */
  .work-track { height: auto !important; }
  .work-sticky {
    position: relative;
    height: auto;
    overflow: visible;
    padding-block: var(--section-y);
  }
  .gallery {
    transform: none !important;
    width: auto;
    flex-wrap: wrap;
    gap: 1.25rem;
  }
  .panel {
    width: min(520px, 100%);
    height: auto;
    border: 1px solid var(--border);
    border-radius: 8px;
  }
  .panel__body {
    grid-template-columns: 1fr;
    padding: clamp(1.4rem, 3vw, 2.1rem);
  }
  .panel__title { font-size: clamp(1.25rem, 2.4vw, 1.6rem); max-width: none; }
  /* Panels are height:auto here, so the preview can't overflow —
     no reason to drop it. Just stop it turning. */
  .preview { transform: none; }
  .work-head { padding-top: 0; }
  .work-progress { display: none; }

  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}
