/* ════════════════════════════════════════════════════════════════
   tres.studio — stylesheet
   Strict palette: white / black / yellow
   Type: Bebas Neue (display) / DM Sans (body)

   Sections:
     01. Reset + root variables + base
     02. Background (dot grid)
     03. Custom cursor
     04. Nav
     05. Page transition
     06. Home — hero + selected work index + categories
     07. Category page
     08. Project detail + gallery (jello-zoom + lightbox)
     09. Lab
     10. About
     11. Footer
     12. Utilities (reveal, loading)
     13. Motion + mobile breakpoints
   ════════════════════════════════════════════════════════════════ */

/* ── 01. Reset + root ─────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  --white:     #FFFFFF;
  --black:     #0A0A0A;
  --ink:       #0A0A0A;
  --ink-soft:  #6B6B6B;
  --ink-faint: #B5B5B5;
  --yellow:    #F5CB5C;
  --line:      #ECECEC;

  --display:   'Bebas Neue', sans-serif;
  --body:      'DM Sans', system-ui, sans-serif;

  --pad:       clamp(1.5rem, 4vw, 3rem);
  --pad-lg:    clamp(2rem, 8vw, 7rem);

  --ease:      cubic-bezier(.2, .8, .2, 1);
  --ease-out:  cubic-bezier(.16, 1, .3, 1);

  --max-content: 1600px;
}

html { scroll-behavior: auto; }
body {
  background: var(--white);
  color: var(--ink);
  font-family: var(--body);
  font-weight: 300;
  font-size: 15px;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  min-height: 100vh;
  cursor: none;
}
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: none; cursor: none; }
img { display: block; max-width: 100%; }


/* ── 02. Background — static dot grid + reactive flow field ──── */
/* Pages that include flow-field.js get their .dot-grid swapped out
   for a <canvas class="flow-field"> at runtime — the canvas paints
   short line segments that swing away from the cursor (and from the
   ball, on the homepage). Pages without the script keep the static
   dots. The dot-grid styles stay as the no-JS / reduced-motion
   fallback. */
.dot-grid {
  position: fixed;
  inset: -80px;
  z-index: 0;
  pointer-events: none;
  background-image: radial-gradient(circle, rgba(10, 10, 10, 0.11) 1.2px, transparent 1.6px);
  background-size: 32px 32px;
}
.dot-grid--dark {
  background-image: radial-gradient(circle, rgba(255, 255, 255, 0.09) 1.2px, transparent 1.6px);
}
.flow-field {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}
/* Boids + constellation overlay (homepage hero). z:1 puts it above the
   flow field (z:0) and below .content (z:2). pointer-events:none so it
   never intercepts drag/click on letters or the dot. */
.hero-fx {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}


/* ── 03. Custom cursor ────────────────────────────────────────── */
.cursor {
  position: fixed;
  top: 0; left: 0;
  width: 6px;
  height: 6px;
  background: var(--ink);
  border-radius: 50%;
  pointer-events: none;
  z-index: 9999;
  transform: translate(-50%, -50%);
  transition: width 0.4s var(--ease), height 0.4s var(--ease), background-color 0.3s;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.cursor.hover {
  width: 52px;
  height: 52px;
  background: var(--yellow);
}
/* Label inside the expanded cursor — appears on hover over major
   interactive surfaces (galleries, project rows, drag handles, etc.).
   scroll.js's LABEL_MAP decides the text per element type. The
   appear transition has a delay so the label waits for the circle to
   grow before fading in; the disappear transition has no delay so it
   gets out of the way fast. */
.cursor-label {
  font-family: var(--display);
  font-size: 0.5rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0;
  transform: translateY(2px);
  transition: opacity 0.18s var(--ease), transform 0.2s var(--ease);
  white-space: nowrap;
  pointer-events: none;
}
.cursor.has-label .cursor-label {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.22s var(--ease) 0.18s, transform 0.25s var(--ease) 0.18s;
}
.cursor-trail {
  position: fixed;
  top: 0; left: 0;
  width: 30px;
  height: 30px;
  border: 1px solid rgba(10, 10, 10, 0.2);
  border-radius: 50%;
  pointer-events: none;
  z-index: 9998;
  transform: translate(-50%, -50%);
  transition: opacity 0.3s, border-color 0.3s, width 0.4s var(--ease), height 0.4s var(--ease);
}
.cursor-trail.hide { opacity: 0; }

body.lab-body .cursor { background: var(--yellow); }
body.lab-body .cursor.hover { background: var(--white); }
body.lab-body .cursor-trail { border-color: rgba(255, 255, 255, 0.25); }

@media (hover: none) {
  body, a, button { cursor: auto !important; }
  .cursor, .cursor-trail { display: none !important; }
}

/* Focus — keyboard users only. With cursor: none on desktop the custom
   cursor doesn't render for keyboard nav, so the focus ring is the only
   feedback. Ink on light pages, yellow on the dark lab page (and lightbox
   arrows, which sit on a dark backdrop). :focus is killed for click users
   — :focus-visible already gates by input modality. */
:focus { outline: none; }
:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
  border-radius: 2px;
}
.lab-body :focus-visible,
.lightbox-arrow:focus-visible {
  outline-color: var(--yellow);
}

/* Skip-to-content — hidden off-screen until focused. Lets keyboard /
   screen-reader users jump past the fixed nav. Tabbing onto the page
   from the URL bar lands here first. */
.skip {
  position: fixed;
  top: 0.75rem;
  left: 0.75rem;
  z-index: 100000;
  background: var(--yellow);
  color: var(--black);
  padding: 0.65rem 1.1rem;
  font-family: var(--display);
  font-size: 0.65rem;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  border-radius: 2px;
  transform: translateY(-200%);
  transition: transform 0.25s var(--ease);
}
.skip:focus,
.skip:focus-visible {
  transform: translateY(0);
  outline: none;
}


/* ── 04. Nav ──────────────────────────────────────────────────── */
.nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1.5rem var(--pad);
  pointer-events: none;
  mix-blend-mode: difference;
  color: var(--white);
}
.nav > * { pointer-events: auto; }
.nav .mark {
  font-family: var(--display);
  font-size: 0.95rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}
.nav .mark .y { color: var(--yellow); }
.nav-links { display: flex; gap: 1.8rem; }
.nav-links a {
  font-size: 0.6rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  font-weight: 400;
  transition: opacity 0.25s;
}
.nav-links a:hover { opacity: 0.55; }


/* ── 05. Page transition (soft horizontal swipe) ──────────────── */
.page-transition {
  position: fixed;
  inset: 0;
  z-index: 99999;
  pointer-events: none;
  background: var(--line);
  transform: translateX(-100%);
  will-change: transform;
}
.page-transition.covering {
  transition: transform 0.6s cubic-bezier(0.5, 0, 0.2, 1);
}
.page-transition.revealing {
  transition: transform 0.78s cubic-bezier(0.2, 0, 0.2, 1);
}
.pt-pressed {
  transition: transform 0.18s cubic-bezier(0.4, 0, 0.2, 1) !important;
  transform: scale(0.98) !important;
}
body.pt-locking, body.pt-locking * { cursor: none !important; }
body.pt-locking { overflow: hidden; }

/* During the transition, freeze every transition/animation on the page so
   the panel is the only thing moving. This is what makes the swipe glide
   on content pages the same way it does on the (already-still) lab page. */
body.pt-locking .proj-item,
body.pt-locking .index-row,
body.pt-locking .cat-link,
body.pt-locking .gallery-item,
body.pt-locking .reveal,
body.pt-locking .hero-name,
body.pt-locking .hero-name .y-dot {
  transition: none !important;
  animation: none !important;
}


/* ── 06. Home — hero + index + categories ─────────────────────── */
.hero {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 100vh;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.hero-inner {
  text-align: center;
  padding: 0 var(--pad);
}
.hero-name {
  font-family: var(--display);
  font-size: clamp(6rem, 26vw, 28rem);
  line-height: 0.85;
  letter-spacing: -0.018em;
  color: var(--ink);
  position: relative;
  animation: hero-in 1.4s var(--ease-out) both;
}
/* Bebas Neue lacks a tight T-R kerning pair, leaving a visible gap at this
   size. Pulling the first letter's right margin negative compensates. The
   matching leftward translate shifts the T itself the same amount so the
   visible T-R join stays tight — the translate and margin-right MUST stay
   in sync or a visible gap opens between T and R.
   Targets .letter (added by letters.js) when wrapping is done, AND the
   raw ::first-letter for the brief window before that runs. */
.hero-name::first-letter,
.hero-name .letter:first-child {
  letter-spacing: -0.07em;
  margin-right: -0.07em;
}
.hero-name .letter:first-child {
  transform: translateX(-0.07em);
}
.hero-name .y-dot {
  position: absolute;
  bottom: 0.04em;
  right: -0.18em;
  width: 0.14em;
  height: 0.14em;
  background: var(--yellow);
  border-radius: 50%;
  /* Hero has pointer-events:none so clicks pass through to content
     underneath; the dot opts back in so it can be grabbed. */
  pointer-events: auto;
  /* Touch hardening for iOS — without these the OS treats a touch on
     the dot as scroll intent and cancels pointer events before the
     drag commits. */
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  /* Class-toggled visibility — dot.js adds/removes .is-visible based
     on scroll position. Transitions both directions smoothly, no
     keyframe restart flicker. */
  transform: scale(0);
  opacity: 0;
  transform-origin: center;
  transition:
    transform 0.85s var(--ease-out),
    opacity 0.55s var(--ease-out);
}
/* Invisible hit-area extender — the dot is ~14px on iPhones, well
   below iOS's 44px touch-target minimum. ::before extends the
   tappable area outward without changing the visual size. Catches
   taps for the parent .y-dot via event bubbling. */
.hero-name .y-dot::before {
  content: '';
  position: absolute;
  inset: -16px;
  border-radius: 50%;
}
.hero-name .y-dot.is-visible {
  transform: scale(1);
  opacity: 1;
}

/* ─── Free dot (homepage only) ──────────────────────────────────
   The dot starts captive inside .hero-name. On first interaction
   (mousedown/touch) dot.js promotes it to position:fixed and adds
   .y-dot--free — zero-gravity physics take over from there. The
   dot stays wherever the user leaves it. Releasing with the dot
   over TRES glides it home and triggers the letter wave. */
.hero-name .y-dot--free {
  position: fixed;
  /* Sits at hero's stacking level so .content (z:2) cleanly covers
     it when the user scrolls past the hero — keeps the playground
     from floating in front of the index/category sections. */
  z-index: 1;
  cursor: grab;
  bottom: auto;
  right: auto;
  /* Lock to its captive size so it doesn't reflow when extracted */
  width: clamp(0.84rem, 3.64vw, 3.92rem);   /* = 0.14em × hero-name font clamp */
  height: clamp(0.84rem, 3.64vw, 3.92rem);
  /* Free dot is positioned per-frame by JS — disable the captive
     transitions so they don't fight the physics. */
  transition: none;
  will-change: transform, left, top;
  touch-action: none;
}
.hero-name .y-dot--grabbing { cursor: grabbing; }


/* Smashable letter spans (added by letters.js inside .hero-name and
   .cat-link .label). When smashed, visibility:hidden hides the original
   while preserving the layout box — the dot's bounce-collision rects
   still match, the .hero-name still has its full width, and only the
   physics clone (.letter-chunk) actually moves. */
.hero-name .letter,
.cat-link .label .letter {
  display: inline-block;
  transform-origin: center center;
  transition: opacity 0.15s;
}
.hero-name .letter.smashed,
.cat-link .label .letter.smashed {
  visibility: hidden;
}
/* TRES letters are individually grabbable. The parent .hero has
   pointer-events: none for click-through; we opt back in per-letter.
   Cat-link letters intentionally inherit the parent <a>'s pointer
   behavior so single clicks still navigate to the category. The
   -webkit-* hardening prevents iOS from showing the long-press menu
   or selecting text during drag. */
.hero-name .letter {
  pointer-events: auto;
  cursor: grab;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
.hero-name .letter:active {
  cursor: grabbing;
}

/* Letter chunk — the visible clone created on smash. Pointer events
   stay on so a chunk lying on the page can be re-grabbed and tossed
   around again. z-index sits at the hero's stacking level so .content
   covers chunks cleanly when scrolling past the hero. */
.letter-chunk {
  position: fixed;
  display: inline-block;
  pointer-events: auto;
  cursor: grab;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  z-index: 1;
  will-change: transform;
  transform-origin: center center;
  /* No transition — physics drives transform each frame. */
}
.letter-chunk.dragging {
  cursor: grabbing;
}

@keyframes hero-in {
  from { opacity: 0; letter-spacing: -0.005em; }
  to   { opacity: 1; letter-spacing: -0.018em; }
}
.hero-spacer {
  height: 100vh;
  pointer-events: none;
}

.content {
  position: relative;
  z-index: 2;
  background: var(--white);
}

/* Selected work — numbered list */
.index {
  padding: 10rem var(--pad-lg) 6rem;
  max-width: var(--max-content);
  margin: 0 auto;
}
.index-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding-bottom: 2.5rem;
  margin-bottom: 4rem;
  border-bottom: 1px solid var(--line);
  font-size: 0.6rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.index-row {
  display: grid;
  grid-template-columns: 60px 1fr auto auto;
  align-items: center;
  gap: 2rem;
  padding: 2.2rem 0;
  border-bottom: 1px solid var(--line);
  position: relative;
  transition: padding 0.65s var(--ease);
}
.index-row:hover { padding-left: 2rem; }
.index-row .num {
  font-family: var(--display);
  font-size: 0.85rem;
  letter-spacing: 0.05em;
  color: var(--ink-faint);
}
.index-row .name {
  font-family: var(--display);
  font-size: clamp(1.6rem, 3vw, 2.6rem);
  letter-spacing: 0.015em;
  line-height: 0.95;
  transition: color 0.4s, transform 0.65s var(--ease);
}
.index-row:hover .name { color: var(--ink); }
.index-row .loc,
.index-row .year {
  font-size: 0.6rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.index-row .year { font-family: var(--display); }

/* Yellow tick that slides in on hover */
.index-row::before {
  content: '';
  position: absolute;
  left: -2rem;
  top: 50%;
  width: 1.4rem;
  height: 2px;
  background: var(--yellow);
  transform: translateY(-50%) scaleX(0);
  transform-origin: left;
  transition: transform 0.65s var(--ease);
}
.index-row:hover::before { transform: translateY(-50%) scaleX(1); }

/* Floating thumbnail preview, tracks the cursor */
.index-row .preview {
  position: fixed;
  top: 0; left: 0;
  width: 220px;
  aspect-ratio: 4/3;
  background: var(--line);
  pointer-events: none;
  opacity: 0;
  transform: scale(0.92) rotate(-2deg);
  transition: opacity 0.4s var(--ease), transform 0.5s var(--ease);
  z-index: 50;
  overflow: hidden;
  will-change: transform, opacity;
}
.index-row .preview img { width: 100%; height: 100%; object-fit: cover; }
.index-row:hover .preview {
  opacity: 1;
  transform: scale(1) rotate(0deg);
}

/* Category entry — 3-up minimal */
.cats {
  padding: 8rem var(--pad-lg) 6rem;
  border-top: 1px solid var(--line);
  max-width: var(--max-content);
  margin: 0 auto;
}
.cats-head {
  font-size: 0.6rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 4rem;
}
.cats-list { display: flex; flex-direction: column; }

/* ── Category accordion — buttons that drop down a panel inline ──
   Each cat-link is paired with a .cat-panel sibling. Clicking the
   button toggles .open on both; CSS handles the rest.
   The grid-template-rows: 0fr → 1fr trick gives a true smooth
   height transition without measuring/setting pixel heights. */
.cat-link {
  display: grid;
  grid-template-columns: 60px 1fr auto 2.4rem;
  align-items: center;
  gap: 2rem;
  padding: 3rem 0;
  border-top: 1px solid var(--line);
  position: relative;
  width: 100%;
  font: inherit;
  color: inherit;
  background: transparent;
  border-left: none;
  border-right: none;
  border-bottom: none;
  text-align: left;
  cursor: none;
  transition: padding 0.65s var(--ease);
}
.cat-link:hover { padding-left: 2rem; }
.cat-link.open { padding-left: 2rem; }
/* Last cat-link in the column needs a bottom border so the stack
   reads as a self-contained list. The previous-sibling :last-child
   trick doesn't apply now that panels sit between links. */
.cats-list > .cat-link:last-of-type,
.cats-list > .cat-panel:last-child + .cat-link {
  border-bottom: 1px solid var(--line);
}
/* When the very last child is an OPEN panel, its parent cat-link
   inherits no bottom-border from the rules above, so add one on
   the panel itself when it's the last thing in the stack. */
.cats-list > .cat-panel:last-child {
  border-bottom: 1px solid var(--line);
}
.cat-link .num {
  font-family: var(--display);
  font-size: 0.85rem;
  letter-spacing: 0.05em;
  color: var(--ink-faint);
  transition: color 0.35s;
}
.cat-link.open .num { color: var(--yellow); }
.cat-link .label {
  font-family: var(--display);
  font-size: clamp(3rem, 8vw, 7rem);
  letter-spacing: 0.005em;
  line-height: 0.85;
  transition: color 0.3s;
}
.cat-link:hover .label,
.cat-link.open .label { color: var(--yellow); }
.cat-link .meta {
  font-size: 0.6rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-soft);
  text-align: right;
}

/* Chevron — CSS-drawn down arrow that rotates on .open. align-self
   center sits it on the cat-link's vertical midline regardless of
   how tall the row is. */
.cat-chev {
  position: relative;
  width: 14px;
  height: 14px;
  align-self: center;
  justify-self: end;
}
.cat-chev::before {
  content: '';
  position: absolute;
  inset: 0;
  margin: auto;
  width: 10px;
  height: 10px;
  border-right: 1.5px solid var(--ink-faint);
  border-bottom: 1.5px solid var(--ink-faint);
  transform: rotate(45deg) translate(-2px, -2px);
  transform-origin: center;
  transition: transform 0.55s var(--ease), border-color 0.35s;
}
.cat-link:hover .cat-chev::before { border-color: var(--ink); }
.cat-link.open .cat-chev::before {
  border-color: var(--yellow);
  transform: rotate(-135deg) translate(-2px, -2px);
}

/* ── Cat panel — the accordion drop-down ─────────────────────────
   grid-template-rows: 0fr → 1fr is the modern recipe for smooth
   height: auto transitions. .panel-inner inside MUST have
   overflow: hidden + min-height: 0 for the trick to work. */
.cat-panel {
  display: grid;
  grid-template-rows: 0fr;
  /* Opening curve — slow start, fast middle, gentle landing.
     Closing curve overrides via :not(.open) below — snappier. */
  transition: grid-template-rows 0.72s cubic-bezier(.16, 1, .3, 1);
}
.cat-panel:not(.open) {
  transition: grid-template-rows 0.48s cubic-bezier(.6, 0, .25, 1);
}
.cat-panel.open {
  grid-template-rows: 1fr;
}
.cat-panel > .panel-inner {
  overflow: hidden;
  min-height: 0;
  /* Subtle scale + fade on the inner block — adds the "pop" the
     plain height transition can't give. Plays alongside the height
     transition; the scale is a transform, no layout cost. */
  transform: scale(0.985) translateY(-6px);
  opacity: 0;
  transition:
    transform 0.6s cubic-bezier(.34, 1.5, .64, 1) 0.06s,
    opacity 0.4s ease 0.06s;
  transform-origin: top center;
}
.cat-panel.open > .panel-inner {
  transform: scale(1) translateY(0);
  opacity: 1;
}

/* ── Panel grid — 2-up cards (1-up on mobile) ────────────────── */
.panel-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: clamp(2.5rem, 4vw, 4rem) clamp(1.5rem, 3vw, 3rem);
  padding: 3.5rem 0 4.5rem;
}
@media (max-width: 760px) {
  .panel-grid {
    grid-template-columns: 1fr;
    gap: 2.5rem;
    padding: 2.2rem 0 3rem;
  }
}

.panel-card {
  display: block;
  text-decoration: none;
  color: inherit;
  position: relative;
  cursor: none;
}
.panel-card-media {
  position: relative;
  overflow: hidden;
  aspect-ratio: 4/3;
  background: var(--line);
  border-radius: 6px;
  /* Subtle base shadow — lifts the card off the page just enough */
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
  transition: box-shadow 0.45s var(--ease), transform 0.45s var(--ease);
}
.panel-card-media img,
.panel-card-media svg {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* The unroll-reveal: image starts clipped to the top edge of its
   own box (visually invisible), then expands downward when the
   card gets .in. Combined with a tiny scale-down it looks like a
   paper scroll being unrolled. Per-card stagger comes from --i. */
.panel-card-media img,
.panel-card-media > svg {
  clip-path: inset(0 0 100% 0);
  transform: scale(1.06);
  transition:
    clip-path 1.05s cubic-bezier(.16, 1, .3, 1) calc(var(--i, 0) * 95ms + 80ms),
    transform 1.35s cubic-bezier(.16, 1, .3, 1) calc(var(--i, 0) * 95ms + 80ms);
}
.panel-card.in .panel-card-media img,
.panel-card.in .panel-card-media > svg {
  clip-path: inset(0);
  transform: scale(1);
}
.panel-card-name {
  font-family: var(--display);
  font-size: clamp(1.4rem, 1.9vw, 2rem);
  letter-spacing: 0.015em;
  line-height: 1;
  font-weight: 400;
  margin-top: 1.15rem;
  opacity: 0;
  transform: translateY(10px);
  transition:
    opacity 0.55s ease calc(var(--i, 0) * 95ms + 380ms),
    transform 0.55s ease calc(var(--i, 0) * 95ms + 380ms),
    color 0.3s ease;
}
.panel-card-info {
  font-size: 0.6rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-top: 0.5rem;
  opacity: 0;
  transform: translateY(10px);
  transition:
    opacity 0.55s ease calc(var(--i, 0) * 95ms + 460ms),
    transform 0.55s ease calc(var(--i, 0) * 95ms + 460ms);
}
.panel-card.in .panel-card-name,
.panel-card.in .panel-card-info {
  opacity: 1;
  transform: translateY(0);
}

/* Hover — bigger lift on the media, name flips yellow. Transform
   overrides the entrance scale once the card is .in (specificity
   wins because :hover comes after .in in the cascade). */
@media (hover: hover) {
  .panel-card:hover .panel-card-media {
    transform: translateY(-4px);
    box-shadow: 0 14px 36px rgba(0, 0, 0, 0.13);
  }
  .panel-card:hover .panel-card-media img,
  .panel-card:hover .panel-card-media > svg {
    transform: scale(1.045);
    transition: transform 1.3s cubic-bezier(.2, .8, .2, 1);
  }
  .panel-card:hover .panel-card-name { color: var(--yellow); }
}

/* ── Lab variant — dark themed panel that drops down out of the
   white page. The lab is the "different" one in the lineup, so
   it gets its own visual treatment: black inner panel with rounded
   corners, white text, glassy card borders. */
.cat-panel[data-cat="lab"] > .panel-inner {
  background: var(--black);
  color: var(--white);
  border-radius: 14px;
  margin: 1.4rem 0 2.4rem;
  padding: 1.6rem clamp(1.4rem, 3vw, 2.8rem) 2rem;
  box-shadow: 0 22px 50px rgba(0, 0, 0, 0.18);
}
.panel-grid--lab { padding: 2.4rem 0 1.4rem; }
.cat-panel[data-cat="lab"] .panel-card-name { color: var(--white); }
.cat-panel[data-cat="lab"] .panel-card-info { color: rgba(255, 255, 255, 0.55); }
.cat-panel[data-cat="lab"] .panel-card-media {
  background: rgba(255, 255, 255, 0.025);
  border: 1px solid rgba(255, 255, 255, 0.08);
  box-shadow: none;
  transition: border-color 0.45s var(--ease), background-color 0.45s var(--ease), transform 0.45s var(--ease), box-shadow 0.45s var(--ease);
}
@media (hover: hover) {
  .cat-panel[data-cat="lab"] .panel-card:hover .panel-card-media {
    background: rgba(245, 203, 92, 0.04);
    border-color: rgba(245, 203, 92, 0.45);
    transform: translateY(-4px);
    box-shadow: 0 14px 36px rgba(245, 203, 92, 0.08);
  }
  .cat-panel[data-cat="lab"] .panel-card:hover .panel-card-name { color: var(--yellow); }
}

.panel-empty {
  padding: 3rem 0 4rem;
  text-align: center;
  color: var(--ink-faint);
  font-size: 0.6rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
}

@media (max-width: 900px) {
  .cat-link { grid-template-columns: 40px 1fr 1.6rem; }
  .cat-link .meta { display: none; }
  .cat-link .cat-chev { width: 12px; height: 12px; }
  .cat-chev::before { width: 8px; height: 8px; border-width: 1.3px; }
}


/* ── 07. Category page ────────────────────────────────────────── */
.cat-intro {
  padding: 10rem var(--pad-lg) 4rem;
  display: grid;
  grid-template-columns: 5fr 7fr;
  gap: 4rem;
  align-items: end;
  max-width: var(--max-content);
  margin: 0 auto;
}
.cat-intro-tag {
  font-size: 0.6rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 2rem;
}
.cat-intro-tag .y { color: var(--yellow); }
.cat-intro-name {
  font-family: var(--display);
  font-size: clamp(4rem, 12vw, 14rem);
  line-height: 0.85;
  letter-spacing: -0.01em;
}
.cat-intro-meta {
  text-align: right;
  align-self: end;
  padding-bottom: 0.8rem;
  font-size: 0.6rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.cat-intro-meta strong {
  display: block;
  font-family: var(--display);
  font-size: 1rem;
  letter-spacing: 0.05em;
  margin-bottom: 0.4em;
  color: var(--ink);
  font-weight: 400;
}

/* Project grid — clean 2-column, no editorial offsets. */
.proj-list {
  padding: 4rem var(--pad-lg) 8rem;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  column-gap: clamp(1.5rem, 3vw, 3rem);
  row-gap: clamp(4rem, 7vw, 7rem);
  max-width: var(--max-content);
  margin: 0 auto;
}
.proj-item {
  display: block;
  position: relative;
  will-change: transform, opacity;
  transform-origin: center center;
}
/* Subtle staircase: every other tile drops a touch so the rows aren't
   rigidly aligned. Keeps an editorial feel without leaving any tile
   stranded alone in the middle of the page. */
.proj-item:nth-child(even) { margin-top: clamp(2rem, 4vw, 4rem); }

.proj-img {
  width: 100%;
  overflow: hidden;
  background: var(--line);
}
.proj-img img {
  width: 100%;
  height: auto;
  display: block;
  object-fit: cover;
  transition: transform 1.5s var(--ease);
}
.proj-item:hover .proj-img img { transform: scale(1.04); }

.proj-meta {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-top: 1rem;
  padding: 0 0.2rem;
}
.proj-meta .name {
  font-family: var(--display);
  font-size: clamp(1.1rem, 1.6vw, 1.4rem);
  letter-spacing: 0.015em;
  line-height: 1;
}
.proj-meta .info {
  font-size: 0.6rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-soft);
}


/* ── 08. Project detail + gallery ─────────────────────────────── */
.detail {
  margin-top: 6rem;
  padding: 0 var(--pad-lg) 10rem;
  max-width: var(--max-content);
  margin-left: auto;
  margin-right: auto;
}
.detail-head {
  display: grid;
  grid-template-columns: 5fr 7fr;
  gap: 5rem;
  align-items: end;
  padding: 2rem 0 6rem;
  border-bottom: 1px solid var(--line);
}
.detail-eyebrow {
  font-size: 0.6rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 1.5rem;
}
.detail-title {
  font-family: var(--display);
  font-size: clamp(3rem, 7vw, 6rem);
  line-height: 0.88;
  letter-spacing: -0.002em;
}
.detail-loc {
  font-size: 0.6rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-top: 1.5rem;
}
.detail-loc .y { color: var(--yellow); }
.detail-desc {
  font-size: 1rem;
  line-height: 1.85;
  color: var(--ink-soft);
  max-width: 52ch;
  margin-left: auto;
}
.detail-desc p { margin: 0; }
.detail-credit {
  margin-top: 2.5rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line);
  font-size: 0.6rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-faint);
  line-height: 1;
}
.detail-credit span,
.detail-credit a {
  font-family: var(--display);
  font-weight: 400;
  font-size: 1.4em;
  letter-spacing: 0.08em;
  color: var(--ink);
  margin-left: 0.5em;
  /* Hidden link: no underline, no color shift on hover. Cursor changes
     to pointer (default <a> behavior) so the link is still discoverable
     by hover, but visually it reads as plain credit text. */
  text-decoration: none;
}
.detail-back {
  display: inline-block;
  margin-top: 2rem;
  font-size: 0.6rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-soft);
  transition: color 0.2s, transform 0.4s var(--ease);
}
.detail-back:hover { color: var(--ink); transform: translateX(-6px); }

/* ── Detail prev/next nav — sits between gallery and back link.
   Two stacked links (name + Prev/Next eyebrow + arrow), pushed to
   opposite ends with a baseline rule between them. Wraps around the
   category list so end → start.  */
.detail-nav {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2rem;
  margin-top: 6rem;
  padding: 2.5rem 0 1.5rem;
  border-top: 1px solid var(--line);
}
.detail-nav-link {
  display: grid;
  grid-template-areas:
    "meta meta"
    "name name";
  gap: 0.4rem 0.6rem;
  padding: 0.5rem 0;
  color: var(--ink-soft);
  transition: color 0.3s, transform 0.55s var(--ease);
}
.detail-nav-link--prev { text-align: left;  grid-template-areas: "arrow meta" "arrow name"; grid-template-columns: auto 1fr; }
.detail-nav-link--next { text-align: right; grid-template-areas: "meta arrow" "name arrow"; grid-template-columns: 1fr auto; justify-self: end; }
.detail-nav-arrow {
  grid-area: arrow;
  align-self: center;
  font-family: var(--display);
  font-size: 1.6rem;
  color: var(--ink-faint);
  transition: color 0.3s, transform 0.45s var(--ease);
}
.detail-nav-meta {
  grid-area: meta;
  font-size: 0.55rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.detail-nav-name {
  grid-area: name;
  font-family: var(--display);
  font-size: clamp(1.4rem, 2.2vw, 2rem);
  letter-spacing: 0.01em;
  line-height: 1;
  color: var(--ink);
  transition: color 0.3s;
}
.detail-nav-link:hover .detail-nav-name { color: var(--yellow); }
.detail-nav-link:hover .detail-nav-arrow { color: var(--yellow); }
.detail-nav-link--prev:hover .detail-nav-arrow { transform: translateX(-4px); }
.detail-nav-link--next:hover .detail-nav-arrow { transform: translateX(4px); }


/* ── Reading progress — 1px yellow bar at top of project pages, fills
   left→right as you scroll. transform: scaleX is GPU-cheap and avoids
   layout. JS in project.html drives the scale per frame. */
.read-progress {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 2px;
  background: var(--yellow);
  z-index: 9998;
  transform: scaleX(0);
  transform-origin: left center;
  pointer-events: none;
  will-change: transform;
  opacity: 0.9;
}


/* ── Empty / not-found state (category + project) ──────────────── */
.empty-cat {
  max-width: var(--max-content);
  margin: 0 auto;
  padding: 12rem var(--pad-lg) 8rem;
}
.empty-cat-tag {
  font-size: 0.6rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 1.5rem;
}
.empty-cat-tag .y { color: var(--yellow); }
.empty-cat-name {
  font-family: var(--display);
  font-size: clamp(3rem, 9vw, 9rem);
  line-height: 0.85;
  letter-spacing: -0.005em;
  margin-bottom: 2rem;
}
.empty-cat-msg {
  font-size: 1rem;
  color: var(--ink-soft);
  margin-bottom: 3rem;
  max-width: 36rem;
}
.empty-cat-back {
  font-size: 0.62rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-soft);
  transition: color 0.2s, transform 0.4s var(--ease);
  display: inline-block;
}
.empty-cat-back:hover { color: var(--ink); transform: translateX(-6px); }

/* Gallery — landscape jello-zoom grid */
.gallery {
  padding-top: 5rem;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: clamp(0.5rem, 0.8vw, 0.85rem);
}
.gallery-item {
  aspect-ratio: 3/2;
  background: var(--line);
  overflow: hidden;
  position: relative;
  cursor: zoom-in;
  border-radius: 10px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  transform-origin: center center;
  will-change: transform;
  /* content-visibility: auto skips paint/composite for cells whose layout
     box is off-screen. The aspect-ratio above locks layout size so the grid
     stays stable. Biggest single perf win on long galleries (thesis: 73). */
  content-visibility: auto;
  contain-intrinsic-size: auto 220px;
  transition:
    transform 0.65s cubic-bezier(0.25, 1, 0.5, 1),
    box-shadow 0.5s ease;
}
.gallery-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  opacity: 0;
  transition: opacity 0.6s ease;
}
.gallery-item img.loaded { opacity: 1; }
/* Video gallery items get full parity with img: same fill, same fade-in.
   Used for embedded clips (autoplay, muted, looped) like the z07 construction
   walkthrough. data.js routes any .mp4/.webm/.mov path to Cloudinary's video
   CDN automatically. */
.gallery-item video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  opacity: 0;
  transition: opacity 0.6s ease;
}
.gallery-item video.loaded { opacity: 1; }

@media (hover: hover) {
  .gallery-item:hover:not(.zoomed):not(.pushed) {
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
  }
}

.gallery .gallery-item.zoomed {
  z-index: 20;
  transform: scale(3.5);
  border-radius: 14px;
  box-shadow:
    0 30px 70px rgba(0, 0, 0, 0.22),
    0 10px 24px rgba(0, 0, 0, 0.12);
  transition:
    transform 1.2s cubic-bezier(0.34, 1.4, 0.5, 1),
    box-shadow 0.85s ease,
    border-radius 0.7s ease;
}
.gallery .gallery-item.pushed {
  z-index: 1;
  transition: transform 1.1s cubic-bezier(0.34, 1.4, 0.5, 1);
}
.gallery.has-zoom .gallery-item:not(.zoomed) img,
.gallery.has-zoom .gallery-item:not(.zoomed) video {
  filter: saturate(0.78) brightness(0.92);
  transition: filter 0.5s ease;
}

/* Lightbox — fullscreen expanded tile.
   Selector intentionally matches .zoomed's 3-class specificity so .expanded
   wins the cascade if both classes ever briefly coexist (e.g. a hover timer
   firing immediately after a click). */
.gallery .gallery-item.expanded {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  aspect-ratio: auto;
  z-index: 200;
  background: rgba(10, 10, 10, 0.55);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  cursor: zoom-out;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4rem;
  border-radius: 0;
  transform: none;
  transition: none;
}
.gallery .gallery-item.expanded img,
.gallery .gallery-item.expanded video {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  filter: none;
  border-radius: 6px;
  box-shadow: 0 30px 90px rgba(0, 0, 0, 0.5);
}

/* Lightbox prev/next arrows — injected by scroll.js when a tile is expanded.
   Container is pointer-events:none so the backdrop still receives clicks
   (clicking dark area closes); the buttons themselves re-enable pointer
   events so they remain interactive. */
.lightbox-arrows {
  position: fixed;
  inset: 0;
  z-index: 250;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.4s var(--ease);
}
.lightbox-arrows.visible { opacity: 1; }

.lightbox-arrow {
  position: absolute;
  top: 50%;
  width: 56px;
  height: 56px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(10, 10, 10, 0.45);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border-radius: 50%;
  color: rgba(255, 255, 255, 0.9);
  cursor: none;
  pointer-events: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  transition:
    background 0.3s,
    border-color 0.3s,
    color 0.3s,
    transform 0.35s var(--ease);
}
.lightbox-arrow svg { width: 22px; height: 22px; pointer-events: none; }
.lightbox-arrow:hover {
  background: var(--yellow);
  color: var(--black);
  border-color: var(--yellow);
}
.lightbox-arrow--prev {
  left: 2rem;
  transform: translateY(-50%);
}
.lightbox-arrow--prev:hover { transform: translateY(-50%) translateX(-4px); }
.lightbox-arrow--next {
  right: 2rem;
  transform: translateY(-50%);
}
.lightbox-arrow--next:hover { transform: translateY(-50%) translateX(4px); }

/* Blur everything behind the lightbox */
body.lightbox-open .gallery > .gallery-item:not(.expanded) {
  filter: blur(14px) saturate(0.7);
  transition: filter 0.5s ease;
}
body.lightbox-open .nav,
body.lightbox-open .footer,
body.lightbox-open .detail-head,
body.lightbox-open .detail-back,
body.lightbox-open .dot-grid {
  filter: blur(10px);
  transition: filter 0.5s ease;
}


/* ── 09. Lab ──────────────────────────────────────────────────── */
.lab-body {
  background: var(--black);
  color: var(--white);
  min-height: 100vh;
  cursor: none;
  overflow: hidden;
}
.lab-body .nav { color: var(--white); mix-blend-mode: normal; }


/* ── Lab Index (lab.html + labs/alternates.html) ──────────────────
   Vertical stack of experiment cards. Each card is a horizontal
   banner with a number, name, tagline, meta, and a small SVG
   preview that hints at the experiment's signature visual. The
   page scrolls (overriding .lab-body's overflow:hidden).            */

.lab-index-body {
  overflow-x: hidden;
  overflow-y: auto;
  cursor: none;
}
@media (hover: none) {
  .lab-index-body { cursor: auto; }
}

.lab-index {
  position: relative;
  z-index: 2;
  padding: clamp(6rem, 14vh, 11rem) var(--pad-lg) 4rem;
  max-width: var(--max-content);
  margin: 0 auto;
}

.lab-index-head {
  margin-bottom: clamp(3rem, 8vh, 6rem);
  max-width: 38rem;
}
.lab-index-tag {
  font-family: var(--display);
  font-size: 0.7rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--yellow);
  margin-bottom: 1.2rem;
  opacity: 0;
  transform: translateY(8px);
  animation: lab-fade-up 0.7s var(--ease-out) 0.1s forwards;
}
.lab-index-title {
  font-family: var(--display);
  font-size: clamp(4rem, 13vw, 11rem);
  line-height: 0.88;
  letter-spacing: -0.012em;
  color: var(--white);
  margin-bottom: 1.4rem;
  opacity: 0;
  transform: translateY(16px);
  animation: lab-fade-up 1s var(--ease-out) 0.25s forwards;
}
.lab-index-sub {
  font-size: 0.95rem;
  line-height: 1.55;
  color: rgba(255, 255, 255, 0.55);
  min-height: 3em; /* reserve space so typewriter doesn't shift cards */
}

@keyframes lab-fade-up {
  to { opacity: 1; transform: translateY(0); }
}

.lab-index-list {
  list-style: none;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  margin: 0;
  padding: 0;
}
.lab-index-list li { list-style: none; }

.lab-index-card {
  display: grid;
  grid-template-columns: 4.5rem 1fr 160px 2rem;
  gap: 2rem;
  align-items: center;
  padding: 2.4rem 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  color: var(--white);
  text-decoration: none;
  position: relative;
  opacity: 0;
  transform: translateY(22px);
  transition:
    opacity 0.7s var(--ease-out),
    transform 0.7s var(--ease-out),
    background-color 0.35s var(--ease),
    padding-left 0.45s var(--ease);
}
.lab-index-card.in {
  opacity: 1;
  transform: translateY(0);
}

.lab-index-card .num {
  font-family: var(--display);
  font-size: 1rem;
  letter-spacing: 0.22em;
  color: rgba(255, 255, 255, 0.4);
  transition: color 0.35s var(--ease), transform 0.45s var(--ease);
}

.lab-index-card .body {
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  min-width: 0;
}
.lab-index-card .name {
  font-family: var(--display);
  font-size: clamp(2.2rem, 5.4vw, 3.8rem);
  letter-spacing: 0.015em;
  line-height: 0.92;
  color: var(--white);
  text-transform: uppercase;
  position: relative;
  display: inline-block;
  align-self: flex-start;
}
.lab-index-card .name::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -0.18em;
  height: 1.5px;
  width: 100%;
  background: var(--yellow);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.55s var(--ease);
}
.lab-index-card:hover .name::after {
  transform: scaleX(1);
}
.lab-index-card .tag {
  font-size: 0.85rem;
  color: rgba(255, 255, 255, 0.6);
  line-height: 1.4;
  margin-top: 0.4rem;
}
.lab-index-card .meta {
  font-family: var(--display);
  font-size: 0.6rem;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.35);
  margin-top: 0.5rem;
}

.lab-index-card .preview {
  width: 100%;
  max-width: 160px;
  aspect-ratio: 14 / 9;
  background: rgba(255, 255, 255, 0.025);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 3px;
  position: relative;
  overflow: hidden;
  transition:
    background-color 0.35s var(--ease),
    border-color 0.35s var(--ease),
    transform 0.4s var(--ease);
  will-change: transform;
}
.lab-index-card .preview svg {
  width: 100%;
  height: 100%;
  display: block;
}
.lab-index-card:hover .preview {
  background: rgba(245, 203, 92, 0.05);
  border-color: rgba(245, 203, 92, 0.55);
}

.lab-index-card .arrow {
  font-family: var(--display);
  font-size: 1.4rem;
  color: rgba(255, 255, 255, 0.35);
  transition: color 0.35s var(--ease), transform 0.4s var(--ease);
  justify-self: end;
}
.lab-index-card:hover {
  padding-left: 1.2rem;
}
.lab-index-card:hover .num {
  color: var(--yellow);
}
.lab-index-card:hover .arrow {
  color: var(--yellow);
  transform: translateX(6px);
}

/* Alternates is the "different" card — subtle yellow tint baseline */
.lab-index-card--alt {
  background: rgba(245, 203, 92, 0.035);
}
.lab-index-card--alt .num {
  color: var(--yellow);
  font-size: 1.4rem;
}

/* Placeholder for upcoming alternates — no hover effects, dim */
.lab-index-card--placeholder {
  opacity: 0.42;
  cursor: default;
  pointer-events: none;
}
.lab-index-card--placeholder.in {
  opacity: 0.42; /* lock past the in-animation */
}
.lab-index-card--placeholder .name::after { display: none; }
.lab-index-card--placeholder:hover { padding-left: 0; }

.lab-index-foot {
  margin-top: 4rem;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding-top: 1.5rem;
  border-top: 1px solid rgba(255, 255, 255, 0.06);
  font-family: var(--display);
  font-size: 0.6rem;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.4);
}
.lab-index-foot a { color: rgba(255, 255, 255, 0.55); text-decoration: none; transition: color 0.25s; }
.lab-index-foot a:hover { color: var(--yellow); }

@media (max-width: 760px) {
  .lab-index {
    padding: 5rem 1.3rem 3rem;
  }
  .lab-index-head { margin-bottom: 3rem; }
  .lab-index-sub { font-size: 0.85rem; min-height: 4em; }
  .lab-index-card {
    grid-template-columns: 3rem 1fr 1.5rem;
    gap: 1rem;
    padding: 1.8rem 0;
  }
  .lab-index-card .preview { display: none; }
  .lab-index-card .name { font-size: clamp(2rem, 9vw, 3rem); }
  .lab-index-card .arrow { font-size: 1.2rem; }
  .lab-index-card:hover { padding-left: 0.4rem; }
}


/* ── 10. About ────────────────────────────────────────────────── */
.about {
  margin-top: 8rem;
  padding: 4rem var(--pad-lg) 8rem;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6rem;
  max-width: var(--max-content);
  margin-left: auto;
  margin-right: auto;
}
.about-side {
  position: sticky;
  top: 8rem;
  align-self: start;
}
.about-name {
  font-family: var(--display);
  font-size: clamp(4rem, 9vw, 8rem);
  line-height: 0.85;
  letter-spacing: -0.005em;
}
.about-name .y { color: var(--yellow); }
.about-img {
  width: 70%;
  margin-top: 3rem;
  aspect-ratio: 3/4;
  background: var(--line);
  overflow: hidden;
}
.about-img img { width: 100%; height: 100%; object-fit: cover; }

.about-body { padding-top: 2rem; }
.about-tag {
  font-size: 0.6rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 2rem;
}

/* ─── Typewriter (about page) ─────────────────────────────────
   Lines stream in character-by-character. A yellow block cursor
   sits at the end of whichever .about-line is currently last —
   it visually "moves down" each time a new line begins. After
   the run completes, .done softens the cursor to gray and slows
   its blink, giving it that "chatbot is idle" feeling. */
.about-typer {
  font-size: 1.1rem;
  line-height: 1.85;
  color: var(--ink-soft);
  margin-bottom: 0;
  min-height: 18rem; /* prevent layout jump as text grows in */
}
.about-typer .about-line {
  margin: 0 0 1.5em 0;
  min-height: 1em;
  font-size: inherit;
  line-height: inherit;
  color: inherit;
}
.about-typer .about-line:last-child::after {
  content: "";
  display: inline-block;
  width: 0.45em;
  height: 1em;
  background: var(--yellow);
  margin-left: 0.18em;
  vertical-align: -0.12em;
  animation: about-blink 1.06s steps(2, end) infinite;
}
.about-typer.done .about-line:last-child::after {
  background: var(--ink-faint);
  animation-duration: 1.5s;
}
@keyframes about-blink {
  0%, 50%      { opacity: 1; }
  50.01%, 100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .about-typer .about-line:last-child::after { animation: none; opacity: 0.5; }
}
.about-actions {
  margin-top: 3.5rem;
  padding-top: 2.5rem;
  border-top: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 1rem;
  /* Hidden until the typewriter finishes — JS adds .ready to fade them in */
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 0.7s var(--ease-out), transform 0.7s var(--ease-out);
}
.about-actions.ready {
  opacity: 1;
  transform: translateY(0);
}
.about-actions a {
  font-size: 0.62rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-soft);
  transition: color 0.2s, padding 0.4s var(--ease);
}
.about-actions a:hover { color: var(--ink); padding-left: 1rem; }


/* ── 11. Footer ───────────────────────────────────────────────── */
.footer {
  border-top: 1px solid var(--line);
  padding: 4rem var(--pad-lg) 2rem;
  display: grid;
  grid-template-columns: 2fr 1fr 1fr 1fr;
  gap: 3rem;
  align-items: start;
  font-size: 0.6rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-soft);
  max-width: var(--max-content);
  margin: 0 auto;
}
.footer-block strong {
  display: block;
  color: var(--ink);
  font-family: var(--display);
  font-size: 0.85rem;
  letter-spacing: 0.1em;
  margin-bottom: 1.4rem;
  font-weight: 400;
}
.footer-block a,
.footer-block span { display: block; margin-bottom: 0.6rem; transition: color 0.2s; }
.footer-block a:hover { color: var(--ink); }
.footer-mark {
  font-family: var(--display);
  font-size: clamp(2.5rem, 7vw, 6rem);
  letter-spacing: 0.005em;
  line-height: 0.85;
  color: var(--ink);
}
/* Slide-in-from-left entrance, overrides the default .reveal fade-up.
   The mark is auto-tagged .reveal by scroll.js so this hooks in automatically. */
.footer-mark.reveal {
  opacity: 0;
  transform: translateX(-25vw);
  transition:
    opacity 0.6s var(--ease-out),
    transform 1.3s cubic-bezier(0.22, 1.4, 0.36, 1);
}
.footer-mark.reveal.in {
  opacity: 1;
  transform: translateX(0);
}
.footer-mark .y-dot {
  display: inline-block;
  width: 0.16em;
  height: 0.16em;
  background: var(--yellow);
  border-radius: 50%;
  margin-left: 0.05em;
  vertical-align: baseline;
}
.footer-base {
  grid-column: 1 / -1;
  margin-top: 4rem;
  padding-top: 2rem;
  border-top: 1px solid var(--line);
  display: flex;
  justify-content: space-between;
  color: var(--ink-faint);
}


/* ── 12. Utilities ────────────────────────────────────────────── */
.reveal {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.95s var(--ease-out), transform 0.95s var(--ease-out);
}
.reveal.in {
  opacity: 1;
  transform: translateY(0);
}

.loading {
  padding: 10rem var(--pad);
  text-align: center;
  color: var(--ink-faint);
  font-size: 0.6rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
}
/* Three-dot breath after the word — subtle "still working" cue while
   the sheet fetches. Each ::after dot animates with a stagger via a
   single keyframe + nth-child trick; here we use a pseudo-element +
   conic step to fake three pulses with one element. */
.loading::after {
  content: '';
  display: inline-block;
  width: 0.45em;
  height: 0.45em;
  margin-left: 0.6em;
  border-radius: 50%;
  background: var(--yellow);
  vertical-align: middle;
  animation: loading-pulse 1.1s var(--ease) infinite;
}
@keyframes loading-pulse {
  0%, 100% { opacity: 0.25; transform: scale(0.85); }
  50%      { opacity: 1;    transform: scale(1.05); }
}
@media (prefers-reduced-motion: reduce) {
  .loading::after { animation: none; opacity: 0.6; }
}

/* Brief flash when a mailto: click copies to clipboard (scroll.js handles
   the swap). The yellow tint + slight scale read as "something happened" */
a.copied {
  color: var(--yellow) !important;
  transition: color 0.2s ease;
}
.lab-body a.copied { color: var(--yellow) !important; }


/* ── 13. Motion + responsive ──────────────────────────────────── */
@media (max-width: 900px) {
  .index { padding: 6rem var(--pad) 3rem; }
  .index-row { grid-template-columns: 40px 1fr; gap: 1rem; padding: 1.4rem 0; }
  .index-row .loc,
  .index-row .year,
  .index-row .preview { display: none; }

  .cats { padding: 4rem var(--pad) 3rem; }
  .cat-link { grid-template-columns: 40px 1fr; padding: 1.6rem 0; }
  .cat-link .meta { display: none; }

  .cat-intro { grid-template-columns: 1fr; gap: 1rem; padding: 8rem var(--pad) 3rem; }
  .cat-intro-meta { text-align: left; }

  .proj-list {
    grid-template-columns: 1fr;
    padding: 3rem var(--pad) 5rem;
    gap: 3rem;
  }
  .proj-item,
  .proj-item:nth-child(even) {
    grid-column: 1;
    margin: 0;
  }

  .detail { padding: 0 var(--pad) 5rem; margin-top: 6rem; }
  .detail-head { grid-template-columns: 1fr; gap: 2rem; padding: 1rem 0 3rem; }
  .detail-desc { margin-left: 0; }

  .gallery {
    grid-template-columns: repeat(2, 1fr);
    gap: 0.5rem;
    padding-top: 3rem;
  }
  .gallery-item {
    cursor: pointer;
    border-radius: 8px;
  }
  .gallery-item.zoomed,
  .gallery-item.pushed {
    transform: none !important;
    z-index: auto;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  }
  .gallery.has-zoom .gallery-item:not(.zoomed) img,
  .gallery.has-zoom .gallery-item:not(.zoomed) video { filter: none; }
  .gallery-item.expanded { padding: 1rem; }
  .lightbox-arrow {
    width: 42px;
    height: 42px;
    cursor: pointer;
  }
  .lightbox-arrow svg { width: 18px; height: 18px; }
  .lightbox-arrow--prev { left: 0.75rem; }
  .lightbox-arrow--next { right: 0.75rem; }

  .about { grid-template-columns: 1fr; gap: 3rem; padding: 4rem var(--pad); margin-top: 6rem; }
  .about-side { position: static; }
  .about-img { width: 100%; }

  .footer { grid-template-columns: 1fr; gap: 2rem; padding: 3rem var(--pad) 2rem; }
  .footer-base { flex-direction: column; gap: 1rem; }

  .ios-icon { width: 62px; height: 62px; border-radius: 16px; }
  .ios-icon svg { width: 30px; height: 30px; }
  /* The "drag to release" hint is for cursor users — touch users just drag
     intuitively, and on narrow viewports the hint collides with the gravity
     slider. Hide it on mobile/touch. */
  .lab-hint { display: none; }
}

/* Mobile + touch — push scrollables onto their own composite layer, kill blend modes */
@media (max-width: 900px), (hover: none) {
  .dot-grid {
    background-image: radial-gradient(circle, rgba(10, 10, 10, 0.07) 1px, transparent 1.4px);
    background-size: 36px 36px;
  }
  .dot-grid--dark {
    background-image: radial-gradient(circle, rgba(255, 255, 255, 0.06) 1px, transparent 1.4px);
  }
  .proj-item, .gallery-item, .index-row {
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
    transform: translateZ(0);
  }
  .nav { mix-blend-mode: normal; color: var(--ink); }
  .lab-body .nav { color: var(--white); }
}

/* Respect users who want less motion */
@media (prefers-reduced-motion: reduce) {
  .reveal, .reveal.in { opacity: 1 !important; transform: none !important; transition: none !important; }
  .proj-item, .gallery-item, .index-row { transform: none !important; opacity: 1 !important; }
  .hero-name { animation: none !important; }
  .hero-name .y-dot { transition: none !important; transform: scale(1) !important; opacity: 1 !important; }
  .dot-grid { transform: none !important; }
}
