/* =====================================================================
   Maneet Kohli — site.css (v2, September 2026)
   Desktop rules first, top to bottom. Every other size (tablets, phones,
   narrow windows, portrait, landscape phones) is handled in the
   RESPONSIVE block at the very end, which only overrides.
   ===================================================================== */

/* ---------- Tokens ---------- */
:root {
  /* Pitch black: matches the shadow value in the hero photo (0,0,0). */
  --black: #000;
  --white: #fff;

  /* Name ink. Blended with `difference`: on black the letters show as this
     colour; over the white wave they invert to (255 - ink), so near-white
     ink reads near-black through the wave. */
  --name-ink: rgb(238 238 238);
  --name-tracking: -0.025em;

  --edge: clamp(24px, 3vw, 48px);   /* padding from the four sides */

  --figure-h: 80vh;              /* hero photo height, bottom-anchored */
  --figure-aspect: 1.1833;       /* hero-figure.png width / height */
  --figure-w: calc(var(--figure-h) * var(--figure-aspect));
  /* Figure mask, top to bottom: head fully opaque, torso dropped to
     --torso-opacity so the wave shows through the suit, then a dissolve
     at the very bottom to hide the photo's hard cut. Percentages are of
     the photo's height: chin ≈ 55%, shoulders begin ≈ 62%. */
  --head-solid-until: 52%;
  --torso-from: 64%;
  --torso-opacity: 0.97;
  --figure-fade-start: 100%;

  /* Where the head sits inside the photo at eye level, as fractions of
     the photo width measured from its horizontal centre. Left edge of the
     turban is 0.154 W left of centre, right edge is 0.268 W right of it.
     The name is sized so MANEET (5.586 em wide) fills the space between
     the viewport edge and the turban, minus a gap. Both words share it. */
  --head-left: 0.154;
  --head-right: 0.268;
  --name-gap: clamp(16px, 2vw, 32px);
  --name-em-width: 5.586;        /* MANEET in Archivo 900 / 125 */
  --name-em-last: 3.981;         /* KOHLI, same face */
  /* The smaller of the two gutters sets the size: the right one is
     narrower (the head sits right of the photo's centre) but KOHLI is the
     shorter word, so on mid-width screens either can be the limit. */
  --name-size: clamp(
    36px,
    min(
      calc(
        (50vw - var(--head-left) * var(--figure-w) - var(--edge) - var(--name-gap)) /
          var(--name-em-width)
      ),
      calc(
        (50vw - var(--head-right) * var(--figure-w) - var(--edge) - var(--name-gap)) /
          var(--name-em-last)
      )
    ),
    220px
  );
  /* Horizontal nudge of the figure. 0 on desktop (the name gutters are
     built around the photo's own centre); the stacked hero shifts it so
     the head, not the photo, is centred. */
  --figure-shift: 0px;

  --dock-h: 60px;
  --dock-top: 20px;
  --dock-tile: 44px;
  --dock-sig-w: 112px;           /* signature slot, no tile behind it */

  /* Small type: meta line, major line, cycling words, scroll prompt. */
  --meta-size: 13px;
  --meta-dim: rgb(255 255 255 / 0.5);

  --blur-h: 24vh;                /* progressive blur band height */

  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --font-display: "Archivo", "Inter", system-ui, sans-serif;
  --font-body: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
}

/* ---------- Reset ---------- */
*,
*::before,
*::after {
  box-sizing: border-box;
}
html,
body {
  margin: 0;
  padding: 0;
}
html {
  background: var(--black);
  color: var(--white);
  font-family: var(--font-body);
  -webkit-text-size-adjust: 100%;   /* iOS: no auto-inflation in landscape */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}
body {
  min-height: 100vh;
  min-height: 100svh;
  overflow-x: hidden;
}
img {
  display: block;
  max-width: 100%;
}
a {
  color: inherit;
  text-decoration: none;
}
ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* =====================================================================
   HERO
   z-map (inside .hero, its own stacking context):
     1  .hero__wave    shader canvas, transparent over black
     2  .hero__name    two words, mix-blend-mode: difference against the wave
     3  .hero__figure  the photo — frontmost, never blended
   page-level:
     50 .edge-blur     fixed progressive blur at the bottom edge
     56 .hero-status   cycling line + scroll prompt, above the blur
     58 .dots          step dots
     60 .dock          fixed top nav
   ===================================================================== */
.hero {
  position: relative;
  isolation: isolate;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  background: var(--black);
}

/* ---------- Wave (Paper Shaders mounts a <canvas> inside) ---------- */
.hero__wave {
  position: absolute;
  inset: 0;
  z-index: 1;
  opacity: 0;
  animation: wave-in 1.6s var(--ease-out) 0.25s forwards;
}
.hero__wave canvas {
  width: 100%;
  height: 100%;
}
/* Static fallback if WebGL / the module fails to load. */
.hero__wave.is-fallback {
  background:
    radial-gradient(120% 60% at 20% 110%, rgb(255 255 255 / 0.55), transparent 60%),
    radial-gradient(120% 60% at 85% 105%, rgb(255 255 255 / 0.4), transparent 60%);
}

/* ---------- Name ---------- */
.hero__name {
  position: absolute;
  inset: 0;
  z-index: 2;
  margin: 0;
  pointer-events: none;
  font-family: var(--font-display);
  font-weight: 900;
  font-stretch: 125%;
  font-size: var(--name-size);
  line-height: 0.8;
  letter-spacing: var(--name-tracking);
  text-transform: uppercase;
  color: var(--name-ink);
  mix-blend-mode: difference;
}
.hero__name-word {
  position: absolute;
  display: block;
  white-space: nowrap;
  /* Vertically centred on the hero: line box is 0.8em, so pull up half. */
  top: 50%;
  margin-top: -0.4em;
  opacity: 0;
  transform: translateY(0.12em);
  animation: name-in 1.1s var(--ease-out) forwards;
}
/* Each word owns the gutter between the viewport edge and the turban and
   is centred inside it, so the composition stays symmetric around the
   figure even though the head is not centred in the photo. */
.hero__name-word--first {
  left: var(--edge);
  right: calc(50% + var(--head-left) * var(--figure-w) + var(--name-gap));
  text-align: center;
  animation-delay: 0.1s;
}
.hero__name-word--last {
  left: calc(50% + var(--head-right) * var(--figure-w) + var(--name-gap));
  right: var(--edge);
  text-align: center;
  animation-delay: 0.25s;
}
/* The inner block is exactly as wide as the glyphs, so the small lines
   above / below can align to the word's own left and right edges. */
.hero__name-inner {
  position: relative;
  display: inline-block;
}
.hero__meta {
  position: absolute;
  display: flex;
  align-items: center;
  gap: 8px;
  white-space: nowrap;
  font-family: var(--font-body);
  font-weight: 400;
  font-stretch: 100%;
  font-size: var(--meta-size);
  line-height: 1;
  letter-spacing: 0.01em;
  text-transform: none;
  color: var(--meta-dim);
}
.hero__meta--left {
  left: 0;
  bottom: 100%;
  margin-bottom: 22px;
}
.hero__meta--right {
  right: 0;
  top: 100%;
  margin-top: 22px;
  justify-content: flex-end;
}

/* ---------- Day / night icon (midu's availability status) ---------- */
.hero__meta-icon {
  position: relative;
  width: 14px;
  height: 14px;
  flex: none;
}
.hero__meta-icon:empty {
  display: none;
}
.hero__meta-icon svg {
  display: block;
  width: 14px;
  height: 14px;
  color: #fff;
}
.hero__meta-icon--sun svg {
  animation: sun-spin 14s linear infinite;
}
@keyframes sun-spin {
  to {
    transform: rotate(360deg);
  }
}
/* Sleep: three z's that pop in one after another, rise and fade. */
.hero__meta-icon--sleep .zz {
  position: absolute;
  left: 0;
  bottom: 0;
  font-family: var(--font-body);
  font-weight: 500;
  color: #fff;
  line-height: 1;
  opacity: 0;
  animation: zz 2.8s ease-in-out infinite;
}
.hero__meta-icon--sleep .zz:nth-child(1) { font-size: 7px; left: 0; bottom: 0; animation-delay: 0s; }
.hero__meta-icon--sleep .zz:nth-child(2) { font-size: 9px; left: 5px; bottom: 4px; animation-delay: 0.45s; }
.hero__meta-icon--sleep .zz:nth-child(3) { font-size: 11px; left: 10px; bottom: 8px; animation-delay: 0.9s; }
@keyframes zz {
  0% { opacity: 0; transform: translate(0, 3px); }
  20% { opacity: 1; transform: translate(0, 0); }
  55% { opacity: 1; transform: translate(1px, -2px); }
  75%, 100% { opacity: 0; transform: translate(2px, -5px); }
}

/* =====================================================================
   SHIMMER — midu's idle text effect. The element keeps its dim colour;
   a transparent copy on top is clipped to the glyphs and carries a bright
   band that sweeps across every few seconds, then rests.
   ===================================================================== */
.shimmer {
  position: relative;
  display: inline-block;
  color: var(--meta-dim);
}
.shimmer::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  color: transparent;
  pointer-events: none;
  -webkit-background-clip: text;
  background-clip: text;
  background-repeat: no-repeat;
  background-image: linear-gradient(90deg, transparent 30%, #fff 50%, transparent 70%);
  background-size: 300% 100%;
  background-position: 100% 0;
  animation: shimmer-ltr 5.5s ease-in-out infinite;
}
.shimmer--rtl::after {
  background-position: 0 0;
  animation-name: shimmer-rtl;
}
.shimmer--ttb::after {
  background-image: linear-gradient(180deg, transparent 30%, #fff 50%, transparent 70%);
  background-size: 100% 300%;
  background-position: 0 100%;
  animation-name: shimmer-ttb;
}
/* The band crosses during the first 40% of the cycle, then holds off-glyph. */
@keyframes shimmer-ltr {
  0% { background-position: 100% 0; }
  40%, 100% { background-position: 0% 0; }
}
@keyframes shimmer-rtl {
  0% { background-position: 0% 0; }
  40%, 100% { background-position: 100% 0; }
}
@keyframes shimmer-ttb {
  0% { background-position: 0 100%; }
  40%, 100% { background-position: 0 0%; }
}

/* =====================================================================
   HERO STATUS — cycling line + scroll prompt, centred on the torso.
   Fixed chrome outside the track, above the edge blur (z 56) so the
   prompt stays crisp while the figure behind it blurs. It leaves with the
   hero: html[data-page] is set by the pager the moment a move starts.
   ===================================================================== */
.hero-status {
  position: fixed;
  inset: 0;
  z-index: 56;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  gap: 22px;
  padding-bottom: 11vh;
  pointer-events: none;
  opacity: 0;
  animation: figure-in 1.2s var(--ease-out) 0.9s forwards;
}
.cycle {
  position: relative;
  height: 1.3em;
  min-width: 30ch;               /* "BUSINESS MINDED" with 0.26em tracking */
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: #fff;
  text-align: center;
  overflow: hidden;
  /* soft edges so words seem to surface out of nothing */
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 25%, #000 75%, transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 25%, #000 75%, transparent);
}
.cycle__word {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  line-height: 1.3em;
  white-space: nowrap;
  opacity: 0;
  transform: translateY(110%);
  filter: blur(6px);
  transition:
    transform 0.75s var(--ease-out),
    opacity 0.55s var(--ease-out),
    filter 0.55s var(--ease-out);
}
.cycle__word.is-active {
  opacity: 1;
  transform: translateY(0);
  filter: blur(0);
}
.cycle__word.is-leaving {
  opacity: 0;
  transform: translateY(-110%);
  filter: blur(6px);
}
.scroll-prompt {
  pointer-events: auto;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: var(--meta-size);
  font-weight: 500;
  letter-spacing: 0.02em;
  color: rgb(255 255 255 / 0.82);
  text-shadow: 0 1px 12px rgb(0 0 0 / 0.5);
}
.scroll-prompt .shimmer {
  color: rgb(255 255 255 / 0.82);
}
html[data-page]:not([data-page="hero"]) .hero-status .scroll-prompt {
  pointer-events: none;
}
/* The same line on the project pages, as a button that advances the
   pager. Same box as .hero-status so it lands in the same spot. */
.projects-status {
  position: fixed;
  inset: 0;
  z-index: 56;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding-bottom: 11vh;
  pointer-events: none;
}
.projects-status .scroll-prompt {
  appearance: none;
  border: 0;
  padding: 0;
  background: none;
  font-family: inherit;
  cursor: pointer;
  pointer-events: none;
}
.projects-status__inner {
  opacity: 0;
  transform: translateY(16vh);
  transition:
    transform 0.8s var(--page-ease),
    opacity 0.5s var(--page-ease);
}
html[data-page="projects"]:not(.is-last-step) .projects-status__inner {
  opacity: 1;
  transform: none;
}
html[data-page="projects"]:not(.is-last-step) .projects-status .scroll-prompt {
  pointer-events: auto;
}
.scroll-prompt__arrow {
  width: 8px;
  height: 10px;
  color: rgb(255 255 255 / 0.9);
  animation: arrow-bob 2.2s ease-in-out infinite;
}
@keyframes arrow-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(3px); }
}

/* =====================================================================
   CURSOR — circular ring that trails the pointer and swells over links.
   Only on devices with a real pointer; the OS cursor is hidden there.
   ===================================================================== */
.cursor {
  display: none;
}
@media (hover: hover) and (pointer: fine) {
  html.has-cursor,
  html.has-cursor * {
    cursor: none !important;
  }
  .cursor {
    --cur: 12px;
    display: block;
    position: fixed;
    left: 0;
    top: 0;
    z-index: 1000;
    width: var(--cur);
    height: var(--cur);
    margin: calc(var(--cur) / -2) 0 0 calc(var(--cur) / -2);
    border-radius: 50%;
    background: #fff;
    /* White dot with `difference` = the inverse of whatever is under it. */
    mix-blend-mode: difference;
    pointer-events: none;
    opacity: 0;
    transform: translate3d(var(--cx, -100px), var(--cy, -100px), 0) scale(var(--cs, 1));
    transition:
      opacity 0.3s var(--ease-out),
      width 0.45s var(--ease-out),
      height 0.45s var(--ease-out),
      margin 0.45s var(--ease-out),
      background-color 0.45s var(--ease-out);
  }
  .cursor.is-visible {
    opacity: 1;
  }
  /* Over a project card (utomic.framer.website pattern): the dot grows
     into a frosted disc, a dashed ring spins around it, an arrow sits in
     the middle. */
  .cursor__ring,
  .cursor__arrow {
    position: absolute;
    left: 50%;
    top: 50%;
    opacity: 0;
    transition: opacity 0.3s var(--ease-out);
  }
  .cursor__ring {
    width: 100%;
    height: 100%;
    translate: -50% -50%;
    fill: none;
    stroke: #fff;
    stroke-width: 3.2;
    stroke-dasharray: 2.6 2.55;
    animation: cursor-spin 9s linear infinite;
  }
  .cursor__arrow {
    width: 32px;
    height: 32px;
    translate: -50% -50%;
    fill: none;
    stroke: #fff;
    stroke-width: 2.4;
    stroke-linecap: round;
    stroke-linejoin: round;
    transform: scale(0.6);
    transition:
      opacity 0.3s var(--ease-out),
      transform 0.45s var(--ease-out);
  }
  .cursor__label {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    padding: 0 16px;
    text-align: center;
    font-family: var(--font-body);
    font-size: 11px;
    font-weight: 500;
    line-height: 1.25;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: #fff;
    opacity: 0;
    transform: scale(0.6);
    transition:
      opacity 0.3s var(--ease-out),
      transform 0.45s var(--ease-out);
  }
  .cursor.is-card.is-label .cursor__label {
    opacity: 1;
    transform: scale(1);
  }
  .cursor.is-card.is-label .cursor__arrow {
    opacity: 0;
  }
  .cursor.is-card {
    --cur: 112px;
    mix-blend-mode: normal;
    background: rgb(24 24 28 / 0.32);
    -webkit-backdrop-filter: blur(10px) saturate(120%);
    backdrop-filter: blur(10px) saturate(120%);
  }
  .cursor.is-card .cursor__ring,
  .cursor.is-card .cursor__arrow {
    opacity: 1;
  }
  .cursor.is-card .cursor__arrow {
    transform: scale(1);
  }
  @keyframes cursor-spin {
    to {
      rotate: 360deg;
    }
  }
}


/* ---------- Figure ---------- */
.hero__figure-wrap {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}
.hero__figure {
  position: absolute;
  pointer-events: auto;
  left: 50%;
  bottom: 0;
  height: var(--figure-h);
  width: auto;
  max-width: none;
  transform: translateX(calc(-50% - var(--figure-shift)));
  user-select: none;
  -webkit-user-drag: none;
  /* Head solid, torso translucent so the wave moves through the suit,
     bottom dissolved so the photo's hard cut never shows. */
  -webkit-mask-image: linear-gradient(
    to bottom,
    #000 var(--head-solid-until),
    rgb(0 0 0 / var(--torso-opacity)) var(--torso-from),
    rgb(0 0 0 / var(--torso-opacity)) var(--figure-fade-start),
    transparent 100%
  );
  mask-image: linear-gradient(
    to bottom,
    #000 var(--head-solid-until),
    rgb(0 0 0 / var(--torso-opacity)) var(--torso-from),
    rgb(0 0 0 / var(--torso-opacity)) var(--figure-fade-start),
    transparent 100%
  );
  opacity: 0;
  animation: figure-in 1.4s var(--ease-out) 0.2s forwards;
}
/* Variant for comparison: name in front of the figure. */
.hero--name-front .hero__name {
  z-index: 4;
}

/* ---------- Load animations ---------- */
@keyframes name-in {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
@keyframes figure-in {
  to {
    opacity: 1;
  }
}
@keyframes wave-in {
  to {
    opacity: 1;
  }
}

/* =====================================================================
   EDGE BLUR — progressive blur pinned to the viewport's bottom edge.
   Eight stacked backdrop-filter layers. Each doubles the blur radius of
   the one before and is masked to a 37.5%-tall band that steps down the
   strip, so the blur ramps smoothly from nothing to ~17.5px at the edge.
   ===================================================================== */
.edge-blur {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--blur-h);
  z-index: 50;
  pointer-events: none;
}
.edge-blur__layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.edge-blur__layer:nth-child(1) {
  -webkit-backdrop-filter: blur(0.136719px);
  backdrop-filter: blur(0.136719px);
  -webkit-mask-image: linear-gradient(rgb(0 0 0 / 0) 0%, #000 12.5%, #000 25%, rgb(0 0 0 / 0) 37.5%);
  mask-image: linear-gradient(rgb(0 0 0 / 0) 0%, #000 12.5%, #000 25%, rgb(0 0 0 / 0) 37.5%);
}
.edge-blur__layer:nth-child(2) {
  -webkit-backdrop-filter: blur(0.273438px);
  backdrop-filter: blur(0.273438px);
  -webkit-mask-image: linear-gradient(rgb(0 0 0 / 0) 12.5%, #000 25%, #000 37.5%, rgb(0 0 0 / 0) 50%);
  mask-image: linear-gradient(rgb(0 0 0 / 0) 12.5%, #000 25%, #000 37.5%, rgb(0 0 0 / 0) 50%);
}
.edge-blur__layer:nth-child(3) {
  -webkit-backdrop-filter: blur(0.546875px);
  backdrop-filter: blur(0.546875px);
  -webkit-mask-image: linear-gradient(rgb(0 0 0 / 0) 25%, #000 37.5%, #000 50%, rgb(0 0 0 / 0) 62.5%);
  mask-image: linear-gradient(rgb(0 0 0 / 0) 25%, #000 37.5%, #000 50%, rgb(0 0 0 / 0) 62.5%);
}
.edge-blur__layer:nth-child(4) {
  -webkit-backdrop-filter: blur(1.09375px);
  backdrop-filter: blur(1.09375px);
  -webkit-mask-image: linear-gradient(rgb(0 0 0 / 0) 37.5%, #000 50%, #000 62.5%, rgb(0 0 0 / 0) 75%);
  mask-image: linear-gradient(rgb(0 0 0 / 0) 37.5%, #000 50%, #000 62.5%, rgb(0 0 0 / 0) 75%);
}
.edge-blur__layer:nth-child(5) {
  -webkit-backdrop-filter: blur(2.1875px);
  backdrop-filter: blur(2.1875px);
  -webkit-mask-image: linear-gradient(rgb(0 0 0 / 0) 50%, #000 62.5%, #000 75%, rgb(0 0 0 / 0) 87.5%);
  mask-image: linear-gradient(rgb(0 0 0 / 0) 50%, #000 62.5%, #000 75%, rgb(0 0 0 / 0) 87.5%);
}
.edge-blur__layer:nth-child(6) {
  -webkit-backdrop-filter: blur(4.375px);
  backdrop-filter: blur(4.375px);
  -webkit-mask-image: linear-gradient(rgb(0 0 0 / 0) 62.5%, #000 75%, #000 87.5%, rgb(0 0 0 / 0) 100%);
  mask-image: linear-gradient(rgb(0 0 0 / 0) 62.5%, #000 75%, #000 87.5%, rgb(0 0 0 / 0) 100%);
}
.edge-blur__layer:nth-child(7) {
  -webkit-backdrop-filter: blur(8.75px);
  backdrop-filter: blur(8.75px);
  -webkit-mask-image: linear-gradient(rgb(0 0 0 / 0) 75%, #000 87.5%, #000 100%, rgb(0 0 0 / 0) 112.5%);
  mask-image: linear-gradient(rgb(0 0 0 / 0) 75%, #000 87.5%, #000 100%, rgb(0 0 0 / 0) 112.5%);
}
.edge-blur__layer:nth-child(8) {
  -webkit-backdrop-filter: blur(17.5px);
  backdrop-filter: blur(17.5px);
  -webkit-mask-image: linear-gradient(rgb(0 0 0 / 0) 87.5%, #000 100%, #000 112.5%, rgb(0 0 0 / 0) 125%);
  mask-image: linear-gradient(rgb(0 0 0 / 0) 87.5%, #000 100%, #000 112.5%, rgb(0 0 0 / 0) 125%);
}

/* =====================================================================
   DOCK — frosted bottom nav (Magnetto pattern).
   [signature tile] [links with sliding underline] [Contact +]
   ===================================================================== */
.dock {
  position: fixed;
  left: 0;
  right: 0;
  top: var(--dock-top);
  z-index: 60;
  width: max-content;
  margin-inline: auto;
  display: flex;
  align-items: center;
  gap: 36px;
  height: var(--dock-h);
  padding: 8px 8px 8px 12px;
  border-radius: 999px;
  background: rgb(22 22 22 / 0.62);
  border: 1px solid rgb(255 255 255 / 0.08);
  box-shadow:
    0 20px 60px rgb(0 0 0 / 0.45),
    inset 0 1px 0 rgb(255 255 255 / 0.06);
  -webkit-backdrop-filter: blur(22px) saturate(140%);
  backdrop-filter: blur(22px) saturate(140%);
  opacity: 0;
  animation: dock-in 1s var(--ease-out) 0.7s forwards;
}
@keyframes dock-in {
  from {
    opacity: 0;
    transform: translateY(-14px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Signature tile */
.dock__sig {
  position: relative;
  flex: none;
  width: var(--dock-sig-w);
  height: var(--dock-tile);
  border-radius: 12px;
  overflow: hidden;
  background: transparent;
  display: grid;
  place-items: center;
  transition: transform 0.5s var(--ease-out);
}
.dock__sig:hover {
  transform: scale(1.04);
}
.dock__sig-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.dock__sig-fallback {
  display: none;
  font-family: var(--font-display);
  font-weight: 900;
  font-stretch: 125%;
  font-size: 18px;
  letter-spacing: -0.03em;
  color: var(--white);
}
.dock__sig--fallback .dock__sig-img {
  display: none;
}
.dock__sig--fallback .dock__sig-fallback {
  display: block;
}

/* Links. .dock__nav stacks two panels in one grid cell: the main row and
   the "About me" row. Only one is on; the other sits blurred underneath,
   so the bar never grows or drops anything, it just swaps in place. */
.dock__nav {
  display: grid;
  /* One track clamped to the cell, so a wider hidden panel overflows it
     evenly instead of stretching the track and shoving the live row. */
  grid-template-columns: minmax(0, 1fr);
  justify-items: center;
  width: var(--nav-w, auto);
  transition: width 0.55s var(--ease-out);
}
.dock__panel[data-panel="projects"] {
  gap: 22px;
}
.dock__links {
  display: flex;
  align-items: center;
  gap: 28px;
}
.dock__panel {
  grid-area: 1 / 1;
  justify-self: center;
  flex: none;
  white-space: nowrap;
  transition:
    opacity 0.35s var(--ease-out),
    transform 0.5s var(--ease-out),
    filter 0.35s var(--ease-out);
}
.dock__panel:not(.is-on) {
  opacity: 0;
  pointer-events: none;
  transform: translateY(7px);
  filter: blur(5px);
}
.dock__panel.is-on {
  transition-delay: 0.1s;
}
.dock__crumb {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin: 0;
  padding: 6px 18px 6px 0;
  border: 0;
  border-right: 1px solid rgb(255 255 255 / 0.16);
  border-radius: 0;
  background: none;
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgb(255 255 255 / 0.48);
  cursor: pointer;
  transition: color 0.4s var(--ease-out);
}
.dock__crumb svg {
  width: 9px;
  height: 9px;
  transition: transform 0.4s var(--ease-out);
}
.dock__crumb:hover,
.dock__crumb:focus-visible {
  color: rgb(255 255 255 / 0.8);
  outline: none;
}
.dock__crumb:hover svg {
  transform: translateX(-2px);
}
.dock__link {
  position: relative;
  display: inline-block;
  margin: 0;
  padding: 6px 0;
  border: 0;
  background: none;
  font-family: var(--font-body);
  cursor: pointer;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgb(255 255 255 / 0.86);
  transition: color 0.4s var(--ease-out);
}
.dock__link:hover,
.dock__link:focus-visible {
  color: var(--white);
  outline: none;
}
/* Two hairlines: the first slides out to the right, the second follows
   in from the left. */
.dock__link::before,
.dock__link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: currentColor;
  transition: transform 0.55s var(--ease-out);
}
.dock__link::before {
  transform: scaleX(1);
  transform-origin: left;
}
.dock__link::after {
  transform: scaleX(0);
  transform-origin: left;
  transition-delay: 0.12s;
}
.dock__link:hover::before,
.dock__link:focus-visible::before {
  transform: scaleX(0);
  transform-origin: right;
}
.dock__link:hover::after,
.dock__link:focus-visible::after {
  transform: scaleX(1);
}

/* CTA */
.dock__cta {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 14px;
  height: var(--dock-tile);
  padding: 0 20px 0 22px;
  border-radius: 999px;
  background: var(--white);
  color: var(--black);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  transition:
    transform 0.5s var(--ease-out),
    background-color 0.4s var(--ease-out);
}
.dock__cta:hover {
  transform: scale(1.03);
  background: #ececec;
}
.dock__cta-plus {
  font-size: 16px;
  line-height: 1;
  transition: transform 0.5s var(--ease-out);
}
.dock__cta:hover .dock__cta-plus {
  transform: rotate(90deg);
}

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  .hero__wave,
  .hero__name-word,
  .hero__figure,
  .dock {
    animation: none;
    opacity: 1;
    transform: none;
  }
  .hero__figure {
    transform: translateX(calc(-50% - var(--figure-shift)));
  }
  .dock__link::before,
  .dock__link::after,
  .dock__cta,
  .dock__cta-plus,
  .dock__sig,
  .dock__nav,
  .dock__panel,
  .dock__crumb {
    transition: none;
  }
  .hero-status {
    animation: none;
    opacity: 1;
  }
  .shimmer::after,
  .hero__meta-icon--sun svg,
  .hero__meta-icon--sleep .zz,
  .scroll-prompt__arrow {
    animation: none;
  }
  .cycle__word {
    transition: none;
  }
  .cursor,
  .cursor__ring,
  .cursor__arrow,
  .cursor__label,
  .card__icon,
  .card__pen,
  .card__plane,
  .projects__title {
    transition: none;
  }
  .card__write-mask,
  .card__route-mask {
    transition: none;
  }
  .cursor__ring {
    animation: none;
  }
}

/* =====================================================================
   PAGES — the site is a stack of full-viewport pages. Native scrolling
   is off; js/site.js moves .pages one page per gesture (wheel, swipe,
   keys, dots). Fixed chrome (dock, edge blur, dots, cursor) lives outside
   the track so the transform never captures it.
   z-map (page-level): 50 edge blur, 56 hero status, 58 dots, 60 dock.
   ===================================================================== */
html,
body {
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
}
.pages {
  position: relative;
  width: 100%;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
}
/* Pages are stacked, later pages on top, and each one owns its own
   enter/leave transform (see PAGE STATES). The track itself never moves. */
.page {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  isolation: isolate;
  will-change: transform;
  transition: transform var(--page-ms) var(--page-ease);
}
/* Stacked pages still hit-test where they're invisible (a closed mask
   doesn't stop the pointer), so only the active page takes it. */
.page:not(.is-active) {
  pointer-events: none;
}
.page:nth-child(1) { z-index: 1; }
.page:nth-child(2) { z-index: 2; }
.page:nth-child(3) { z-index: 3; }
.page:nth-child(4) { z-index: 4; }
.page:nth-child(5) { z-index: 5; }
.pages.is-instant .page,
.pages.is-instant .page::after {
  transition: none;
}

/* =====================================================================
   PROJECT PAGES
   z-map (inside .project):
     1  .project__bg   live backgrounds; the incoming one is lifted to 2
                       while it wipes in over the outgoing one
     3  .flip          the slab (two .card faces)
   The card palette is fixed (dark glass) across all projects; per-step
   colour lives only in the backgrounds and html[data-theme] for the dots.
   ===================================================================== */
:root {
  --font-serif: "Instrument Serif", "Times New Roman", serif;
  --card-w: clamp(440px, 34vw, 600px);
}
.page--projects {
  /* Three rows: the title centred between the dock and the slab, the slab,
     and an equal-weight row below. Padding-top starts row one at the dock's
     bottom edge. Backgrounds are absolute and stay out of the grid. */
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) auto minmax(0, 1fr);
  justify-items: center;
  align-items: center;
  padding-top: calc(var(--dock-top) + var(--dock-h));
  background: var(--black);
  --card-bg: rgb(8 8 10 / 0.5);
  --card-line: rgb(255 255 255 / 0.14);
  --card-ink: #fff;
  --preview-tint: rgb(255 255 255 / 0.05);
  --tag-bg: #fff;
  --tag-ink: #000;
}
/* The card keeps this one dark glass palette on every project; only the
   backgrounds and the dots change per step. */

/* ---------- Title ----------
   "Projects and Experiences" on every step, in the bio kicker's voice
   (Inter, tracked, shimmer) but bigger and bolder, inside a frosted pill
   like the dock so it reads over sky, nebula, paper and white alike. */
.projects__title {
  grid-row: 1;
  position: relative;
  z-index: 4;
  margin: 0;
  padding: 13px 26px;
  border-radius: 999px;
  background: rgb(22 22 22 / 0.55);
  border: 1px solid rgb(255 255 255 / 0.1);
  box-shadow: 0 16px 48px rgb(0 0 0 / 0.28), inset 0 1px 0 rgb(255 255 255 / 0.06);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  backdrop-filter: blur(18px) saturate(140%);
  text-align: center;
  white-space: nowrap;
  pointer-events: none;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: clamp(14px, 1.15vw, 18px);
  letter-spacing: 0.26em;
  text-transform: uppercase;
  transition:
    opacity 0.8s var(--page-ease),
    transform var(--page-ms) var(--page-ease);
}
.projects__title .shimmer {
  color: rgb(255 255 255 / 0.92);
}
.page--projects:not(.is-active) .projects__title {
  opacity: 0;
  transform: translateY(6vh);
}

/* ---------- Backgrounds ----------
   A step is a page turn. The slab starts flipping first; 0.3s later the
   incoming background wipes over the outgoing one on the same curve, in
   the direction the slab is turning (forward: up from the bottom edge,
   back: down from the top). The wipe is a soft-edged mask three viewports
   tall whose position slides one viewport. js/site.js marks the incoming
   layer .is-on and the outgoing one .is-off; everything else is hidden.
   [data-dir] on the page picks the direction and is set before the
   classes change, so the incoming layer starts closed on the right side. */
.project__bg {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  visibility: hidden;
  -webkit-mask-image: linear-gradient(to bottom, transparent 46%, #000 54%);
  mask-image: linear-gradient(to bottom, transparent 46%, #000 54%);
  -webkit-mask-size: 100% 300%;
  mask-size: 100% 300%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: 0 0;
  mask-position: 0 0;
  transition:
    -webkit-mask-position 0.85s var(--page-ease) 0.3s,
    mask-position 0.85s var(--page-ease) 0.3s;
}
.page--projects[data-dir="back"] .project__bg {
  -webkit-mask-image: linear-gradient(to top, transparent 46%, #000 54%);
  mask-image: linear-gradient(to top, transparent 46%, #000 54%);
  -webkit-mask-position: 0 100%;
  mask-position: 0 100%;
}
.project__bg.is-on {
  visibility: visible;
  z-index: 2;
  -webkit-mask-position: 0 100%;
  mask-position: 0 100%;
}
.page--projects[data-dir="back"] .project__bg.is-on {
  -webkit-mask-position: 0 0;
  mask-position: 0 0;
}
.project__bg.is-off {
  visibility: visible;
  z-index: 1;
  -webkit-mask-image: none;
  mask-image: none;
}
/* Landing on the page (or the first paint): no wipe. */
.page--projects.is-instant .project__bg {
  transition: none;
}
.project__bg > canvas,
.project__bg > svg,
.project__bg > div,
.galaxy__nebula canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* Sky: gradient ground, clouds painted on the canvas, hills over both. */
.project__bg--sky {
  background: linear-gradient(180deg, #4a97e4 0%, #79b6ee 48%, #cfe4f9 100%);
}
.project__bg--sky > .sky__hills {
  top: auto;
  bottom: 0;
  height: 36vh;
  min-height: 220px;
  filter: drop-shadow(0 -10px 30px rgb(20 70 20 / 0.22));
}

/* Galaxy: near-black ground, nebula shader, stars on top. */
.project__bg--galaxy {
  background: #03050d;
}
.galaxy__nebula {
  opacity: 0.55;
}
.galaxy__stars {
  mix-blend-mode: screen;
}

/* Paper: beige craft stock with grain, flight paths on the svg above it. */
.project__bg--paper {
  background: #efe5cf;
}
.paper__grain {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='320' height='320'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.42  0 0 0 0 0.30  0 0 0 0 0.20  0 0 0 0.42 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
  background-size: 320px 320px;
  opacity: 0.55;
  mix-blend-mode: multiply;
}
.paper__grain::after {
  /* soft vignette so the sheet reads as paper, not a flat fill */
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(120% 90% at 50% 45%, transparent 55%, rgb(96 60 30 / 0.22) 100%);
}
.paper__sky {
  overflow: visible;
}
.flight {
  fill: none;
  stroke: #7a1f2b;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-dasharray: 0.5 7.5;
  opacity: 0.62;
}
.flight--static {
  opacity: 0.3;
}
.flight-mask__path {
  fill: none;
  stroke: #fff;
  stroke-width: 8;
}
.plane {
  fill: #7a1f2b;
}

/* Resume Agent: white sheet, three columns of grey text typing themselves
   (the resume, the agent's trace, the cover letter). Low contrast, a hair
   of blur and a white glow behind the slab keep the card in focus. */
.project__bg--resume {
  background: #f7f7f7;
}
.resume {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  column-gap: clamp(32px, 4vw, 72px);
  padding: 8vh clamp(32px, 5vw, 96px) 6vh;
  color: rgb(0 0 0 / 0.34);
  /* Enough blur that the lines read as text but no word is legible.
     Bigger or bolder lines get extra blur below so they stay unreadable. */
  filter: blur(3.2px);
  user-select: none;
}
.resume__page {
  min-width: 0;
  overflow: hidden;
  font: 400 clamp(11px, 0.78vw, 13px) / 1.62 var(--font-body);
  letter-spacing: 0.005em;
  transition: opacity 0.6s var(--ease-out);
}
.resume__page.is-clearing {
  opacity: 0;
}
.resume__page--trace {
  padding-top: 4vh;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: clamp(10px, 0.7vw, 12px);
  letter-spacing: 0;
}
.resume__page--letter {
  padding-top: 6vh;
}
.resume__line {
  min-height: 1.62em;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.resume__line--gap {
  min-height: 0.9em;
}
.resume__line--name {
  margin-bottom: 0.1em;
  font-size: 1.7em;
  font-weight: 700;
  letter-spacing: 0.08em;
  line-height: 1.2;
  text-transform: uppercase;
  color: rgb(0 0 0 / 0.42);
  filter: blur(3px);
}
.resume__line--meta {
  color: rgb(0 0 0 / 0.36);
}
.resume__line--h {
  margin-top: 1.1em;
  padding-bottom: 2px;
  border-bottom: 1px solid rgb(0 0 0 / 0.14);
  font-size: 0.86em;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgb(0 0 0 / 0.44);
  filter: blur(1px);
}
.resume__line--role {
  margin-top: 0.55em;
  font-weight: 600;
  color: rgb(0 0 0 / 0.4);
  filter: blur(1px);
}
.resume__line--li {
  padding-left: 1.1em;
  text-indent: -1.1em;
}
.resume__line--cmd {
  color: rgb(0 0 0 / 0.5);
}
.resume__line--ok {
  color: rgb(0 0 0 / 0.5);
}
.resume__line--p {
  margin-bottom: 0.8em;
}
.resume__caret {
  display: inline-block;
  width: 0.5em;
  height: 1em;
  margin-left: 2px;
  vertical-align: -0.12em;
  background: currentColor;
  animation: caret-blink 1s steps(2, start) infinite;
}
@keyframes caret-blink {
  to {
    visibility: hidden;
  }
}
.resume__glow {
  background: radial-gradient(52% 64% at 50% 52%, #fff 0%, rgb(255 255 255 / 0.92) 34%, rgb(255 255 255 / 0) 72%);
  pointer-events: none;
}

/* ---------- Card ---------- */
.card {
  position: relative;
  z-index: 3;
  width: var(--card-w);
  padding: 12px;
  border-radius: 18px;
  background: var(--card-bg);
  border: 1px solid var(--card-line);
  box-shadow: var(--card-shadow, 0 30px 80px rgb(0 0 0 / 0.32));
  color: var(--card-ink);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 2px 4px 0 6px;
}
.card__name {
  margin: 0;
  font: 500 15px/1.2 var(--font-body);
  letter-spacing: -0.01em;
}
.card__preview {
  aspect-ratio: 16 / 10;
  border-radius: 12px;
  border: 1px solid var(--card-line);
  background: var(--preview-tint);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.04);
}
/* Banner preview (Authentic Intelligence): the channel art, cropped to
   the card's 16:10, eased in a slow zoom while the card is hovered. */
.card__preview--banner {
  position: relative;
  overflow: hidden;
  background: #4a97e4;
}
.card__banner {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1);
  transform-origin: 50% 55%;
  transition: transform 1.1s var(--ease-out);
  will-change: transform;
}
/* .is-live: on touch screens there is no hover, so js/site.js marks the
   front face once it lands and each card's one motion plays then. */
.card:hover .card__banner,
.card.is-live .card__banner {
  transform: scale(1.07);
}

/* Mark previews (Resume Agent, Regal): a composed still at rest, one
   motion on hover, reversed on leave. Rest and hover transforms live here
   in CSS, in SVG user units, so both states ride one transition. */
.card__preview--resume,
.card__preview--regal {
  position: relative;
  display: grid;
  place-items: center;
  overflow: hidden;
}
.card__icon {
  position: relative;
  z-index: 1;
  width: 78%;
  height: auto;
  overflow: visible;
}

/* Resume Agent: an editorial mark. A white sheet with the wordmark set
   thin and letter-spaced at rest, always legible. On hover a grid of
   cells behind it breathes slowly to a faint grey and back; the middle
   row, which holds the word, is excluded from the grid entirely so the
   text never changes. */
.card__preview--resume {
  background: #fff;
  border-color: rgb(0 0 0 / 0.08);
  color: #000;
}
.card__grid {
  position: absolute;
  z-index: 1;
  inset: 0;
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  grid-template-rows: repeat(5, 1fr);
}
.card__cell {
  background: #d8d8d8;
  opacity: 0;
  border: 1px solid rgb(0 0 0 / 0);
  animation: card-cell-flicker 5.5s ease-in-out infinite;
  animation-play-state: paused;
  transition: border-color 0.5s var(--ease-out);
}
.card:hover .card__cell,
.card.is-live .card__cell {
  animation-play-state: running;
  border-color: rgb(0 0 0 / 0.06);
}
@keyframes card-cell-flicker {
  0%, 100% { opacity: 0; }
  50% { opacity: 0.3; }
}
.card__cell:nth-child(5n+1) { animation-duration: 4.5s; }
.card__cell:nth-child(5n+2) { animation-duration: 5.5s; }
.card__cell:nth-child(5n+3) { animation-duration: 6.5s; }
.card__cell:nth-child(5n+4) { animation-duration: 7.5s; }
.card__cell:nth-child(5n+5) { animation-duration: 3.8s; }
.card__cell:nth-child(7n+1) { animation-delay: 0s; }
.card__cell:nth-child(7n+2) { animation-delay: 0.4s; }
.card__cell:nth-child(7n+3) { animation-delay: 0.9s; }
.card__cell:nth-child(7n+4) { animation-delay: 1.4s; }
.card__cell:nth-child(7n+5) { animation-delay: 1.9s; }
.card__cell:nth-child(7n+6) { animation-delay: 2.4s; }
.card__cell:nth-child(7n) { animation-delay: 2.9s; }
/* Middle row (cells 17-24 of 40): sits directly behind the wordmark, so
   it's excluded from the grid, hover or not, to keep the text fixed. */
.card__cell:nth-child(n+17):nth-child(-n+24),
.card:hover .card__cell:nth-child(n+17):nth-child(-n+24),
.card.is-live .card__cell:nth-child(n+17):nth-child(-n+24) {
  animation: none;
  border-color: transparent;
}
.card__word {
  position: relative;
  z-index: 2;
  font: 300 clamp(10px, 0.95vw, 13px)/1 var(--font-body);
  letter-spacing: 0.4em;
  text-transform: uppercase;
  color: #000;
  mix-blend-mode: normal;
  pointer-events: none;
}

/* Regal: a dispatch map on craft paper, inverted to a night negative —
   deep maroon ground (the same warm family as the plane), cream ink.
   Maroon pins joined by faint dotted routes, the plane parked on the
   left pin. On hover the plane flies the top route (offset-path, nose
   along the tangent) to the far pin while the solid route draws in
   behind it; leaving flies it back. */
.card__preview--regal {
  background: #431118;
  border-color: rgb(239 229 207 / 0.22);
  color: #efe5cf;
}
.card__preview--regal::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='320' height='320'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.94  0 0 0 0 0.90  0 0 0 0 0.81  0 0 0 0.5 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
  background-size: 320px 320px;
  opacity: 0.22;
  mix-blend-mode: screen;
  pointer-events: none;
}
.card__preview--regal::after {
  content: "";
  position: absolute;
  inset: 9px;
  border: 1.5px dashed rgb(239 229 207 / 0.3);
  border-radius: 8px;
  pointer-events: none;
}
.card__icon--map {
  width: 86%;
}
.card__route--faint {
  opacity: 0.38;
}
.card__route-mask {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  transition: stroke-dashoffset 1.4s var(--ease-out);
}
.card:hover .card__route-mask,
.card.is-live .card__route-mask {
  stroke-dashoffset: 0;
}
.card__pin {
  fill: currentColor;
}
.card__pin-ring {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  opacity: 0.55;
}
/* The plane geometry is centred on its own origin with the nose at about
   -53deg, so auto-rotate plus 53deg points it down the route. */
.card__plane {
  offset-path: path("M38 66 C 60 36, 90 28, 112 34 S 168 74, 186 58");
  offset-distance: 0%;
  offset-rotate: auto 53deg;
  /* Lifted a few units off the line so it parks beside the pin and flies
     just above the route. */
  transform: translateY(-7px) scale(0.36);
  transition: offset-distance 1.4s var(--ease-out);
}
.card:hover .card__plane,
.card.is-live .card__plane {
  offset-distance: 100%;
}

/* The card is an <a>; only faces with a real href get a link cursor. */
.card {
  display: flex;
}
.card:not([href]) {
  cursor: default;
}
.card__tags {
  display: flex;
  flex-wrap: nowrap;
  gap: 0.5em;
  justify-content: flex-end;
  padding: 0 4px 2px;
  /* The row never wraps: type, padding and gap all ride this size, which
     tracks the card width so four tags fit at the narrowest card. */
  font-size: clamp(9px, 0.72vw, 12px);
}
.tag {
  flex: none;
  white-space: nowrap;
  font: 500 1em/1 var(--font-body);
  letter-spacing: 0.01em;
  padding: 0.66em 0.8em;
  border-radius: 0.5em;
  background: var(--tag-bg);
  color: var(--tag-ink);
}

/* ---------- Dots ---------- */
.dots {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 26px;
  z-index: 58;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 10px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s var(--ease-out);
  --dots-ink: #fff;
}
html[data-theme="mono"] .dots {
  --dots-ink: #111;   /* white would vanish on the Resume Agent sheet */
}
.dots__dot[hidden] {
  display: none;
}
.dots.is-visible {
  opacity: 1;
  pointer-events: auto;
}
.dots__dot {
  position: relative;
  width: 7px;
  height: 7px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: var(--dots-ink);
  opacity: 0.35;
  cursor: pointer;
  transition:
    width 0.45s var(--ease-out),
    opacity 0.45s var(--ease-out),
    background-color 0.45s var(--ease-out);
}
/* A 7px dot is no target for a thumb: an invisible 32px hit area around it. */
.dots__dot::before {
  content: "";
  position: absolute;
  inset: -12px;
}
.dots__dot:hover {
  opacity: 0.7;
}
.dots__dot.is-active {
  width: 24px;
  opacity: 1;
}
.dots__dot:focus-visible {
  outline: 1px solid var(--dots-ink);
  outline-offset: 4px;
}

@media (prefers-reduced-motion: reduce) {
  .page,
  .page--projects::after {
    transition: none;
  }
  .dots__dot {
    transition: none;
  }
  .card__banner,
  .card:hover .card__banner {
    transition: none;
    transform: none;
  }
}

/* =====================================================================
   PAGE STATES — js/site.js marks every page .is-active, .is-above (already
   passed) or .is-below (still to come). Everything below is choreography
   keyed off those three classes; the track itself moves in .pages.
   ===================================================================== */
:root {
  /* One curve for the track and every page choreography. A quicker
     ramp than the classic (0.76, 0, 0.24, 1) so a gesture reads as
     motion within ~150ms instead of a quarter second of nothing. Keep
     PAGE_EASE in js/site.js identical. */
  --page-ease: cubic-bezier(0.45, 0, 0.2, 1);
  --page-ms: 1100ms;
}

/* ---------- Page moves ----------
   Three kinds of move, one per boundary:
     hero → bio        slide: both pages travel one viewport, like a track.
     bio → projects    iris: bio holds still; projects sits over it and is
                       revealed through a soft-edged circle growing from
                       the centre (a radial mask whose size transitions).
     projects → about  cover: projects holds still and dims; about rises
                       from the bottom edge over it, with a shadow along
                       its top. Reversed on the way back. */
.page.is-above {
  transform: translateY(-100%);
}
.page.is-below {
  transform: translateY(100%);
}
/* iris: bio holds under projects */
.page--bio.is-above {
  transform: none;
}
.page--projects {
  --iris-feather: 140px;
  -webkit-mask-image: radial-gradient(circle closest-side, #000 calc(100% - var(--iris-feather)), transparent 100%);
  mask-image: radial-gradient(circle closest-side, #000 calc(100% - var(--iris-feather)), transparent 100%);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: 50% 50%;
  mask-position: 50% 50%;
  -webkit-mask-size: 220vmax 220vmax;
  mask-size: 220vmax 220vmax;
  transition:
    transform var(--page-ms) var(--page-ease),
    -webkit-mask-size var(--page-ms) var(--page-ease),
    mask-size var(--page-ms) var(--page-ease);
}
.page--projects.is-below {
  transform: none;
  -webkit-mask-size: 0px 0px;
  mask-size: 0px 0px;
}
/* cover: projects holds under about and dims; the slab stays put */
.page--projects.is-above {
  transform: none;
}
.page--projects::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 10;
  background: #000;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--page-ms) var(--page-ease);
}
.page--projects.is-above::after {
  opacity: 0.6;
}
.page--projects.is-above .flip,
.page--projects.is-above .projects__title {
  opacity: 1;
  transform: none;
}
.page--about {
  box-shadow: 0 -40px 120px rgb(0 0 0 / 0.55);
}
/* slide: about → contact is the default move again, the same slide the
   hero makes into bio, so the site closes the way it opened. */

/* ---------- Hero exit (Tanweer pattern) ----------
   Name flies up ~1.5× page speed and blurs out, the figure lags behind
   the page and dissolves, the wave dims, the small lines leave first.
   Leaving upward only: the hero is never "below" anything. */
.hero__name {
  transition:
    transform var(--page-ms) var(--page-ease),
    opacity 0.9s var(--page-ease),
    filter 0.9s var(--page-ease);
}
.page--hero.is-above .hero__name {
  transform: translateY(-58vh) scale(0.94);
  opacity: 0;
  filter: blur(14px);
}
.hero__figure-wrap {
  transform-origin: 50% 100%;
  transition:
    transform var(--page-ms) var(--page-ease),
    opacity 0.85s var(--page-ease) 0.05s,
    filter 0.85s var(--page-ease);
}
.page--hero.is-above .hero__figure-wrap {
  transform: translateY(26vh) scale(0.96);
  opacity: 0;
  filter: blur(6px);
}
.hero__wave canvas {
  transition: opacity 0.9s var(--page-ease);
}
.page--hero.is-above .hero__wave canvas {
  opacity: 0;
}
.hero-status__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 22px;
  transition:
    transform 0.8s var(--page-ease),
    opacity 0.5s var(--page-ease);
}
html[data-page]:not([data-page="hero"]) .hero-status__inner {
  transform: translateY(-16vh);
  opacity: 0;
}

/* =====================================================================
   BIO — one bold statement between two white waves.
   z-map: 1 .bio__bg (two rotated wave shaders), 2 .bio__text
   ===================================================================== */
.page--bio {
  display: grid;
  place-items: center;
  background: var(--black);
}
.bio__bg {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
}
/* Each wave is the hero's shader rotated a quarter turn. The host is a
   100vh × 100vw box turned on its side so it covers the viewport, then
   masked so only the band along its own edge shows. */
.bio__wave {
  position: absolute;
  inset: 0;
  overflow: hidden;
  transition:
    transform 1.3s var(--page-ease),
    opacity 1s var(--page-ease);
}
.bio__wave--left {
  -webkit-mask-image: linear-gradient(90deg, #000 0%, #000 22%, transparent 62%);
  mask-image: linear-gradient(90deg, #000 0%, #000 22%, transparent 62%);
}
.bio__wave--right {
  -webkit-mask-image: linear-gradient(270deg, #000 0%, #000 22%, transparent 62%);
  mask-image: linear-gradient(270deg, #000 0%, #000 22%, transparent 62%);
}
/* Top and bottom: the same wave, the host unrotated (bottom) or turned
   half way (top) so its band lands on that screen edge. */
.bio__wave--top {
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 10%, transparent 44%);
  mask-image: linear-gradient(180deg, #000 0%, #000 10%, transparent 44%);
}
.bio__wave--bottom {
  -webkit-mask-image: linear-gradient(0deg, #000 0%, #000 10%, transparent 44%);
  mask-image: linear-gradient(0deg, #000 0%, #000 10%, transparent 44%);
}
.bio__wave-host {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 100vh;
  height: 100vw;
  translate: -50% -50%;
}
.bio__wave--left .bio__wave-host {
  rotate: 90deg;
}
.bio__wave--right .bio__wave-host {
  rotate: -90deg;
}
.bio__wave--top .bio__wave-host,
.bio__wave--bottom .bio__wave-host {
  width: 100vw;
  height: 100vh;
}
.bio__wave--top .bio__wave-host {
  rotate: 180deg;
}
.bio__wave-host canvas {
  width: 100%;
  height: 100%;
}
.page--bio:not(.is-active) .bio__wave--left {
  transform: translateX(-18vw);
  opacity: 0;
}
.page--bio:not(.is-active) .bio__wave--right {
  transform: translateX(18vw);
  opacity: 0;
}
.page--bio:not(.is-active) .bio__wave--top {
  transform: translateY(-18vh);
  opacity: 0;
}
.page--bio:not(.is-active) .bio__wave--bottom {
  transform: translateY(18vh);
  opacity: 0;
}

.bio__text {
  position: relative;
  z-index: 2;
  width: min(92vw, 1500px);
  padding-top: calc(var(--dock-top) + var(--dock-h));
  padding-bottom: 4vh;
}
/* A soft shade under the column so the copy stays legible however far
   the waves lean in. Sits below the text inside .bio__text's stacking
   context, above the waves. */
.bio__text::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: -6% -10%;
  background: radial-gradient(ellipse 50% 60% at 50% 55%, rgb(0 0 0 / 0.78) 0%, rgb(0 0 0 / 0.5) 45%, rgb(0 0 0 / 0) 100%);
  pointer-events: none;
}
/* Five paragraphs, one size, set thin but big enough to carry the page.
   Each paragraph is a shimmer in its own right: the glyphs are cut from
   a bright band that sweeps across every few seconds (the kicker's old
   idle effect), staggered down the page so the sweep flows through the
   copy. Background-clip on the paragraph itself, not a data-text copy,
   so the serif accents wrap with the rest. */
.bio__statement {
  margin: 0 auto;
  max-width: 58ch;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: clamp(17px, 1.65vw, 27px);
  line-height: 1.42;
  letter-spacing: -0.006em;
  text-align: left;
}
.bio__line,
.contact__line {
  display: block;
  margin: 0 0 1.05em;
  color: transparent;
  -webkit-background-clip: text;
  background-clip: text;
  background-repeat: no-repeat;
  background-color: rgb(255 255 255 / 0.74);
  /* Two layers under the glyphs: the reading light (box-sized, placed by
     --lx / --ly from js, faded by --lo on .bio__statement) and the
     shimmer band. The shimmer keyframes move background-position; the
     light is box-sized so that leaves it where it is. */
  background-image:
    radial-gradient(230px circle at var(--lx, -999px) var(--ly, -999px), rgb(255 255 255 / var(--lo, 0)) 0%, rgb(255 255 255 / 0) 70%),
    linear-gradient(90deg, transparent 26%, #fff 50%, transparent 74%);
  background-size: auto, 300% 100%;
  background-position: 100% 0;
  animation: shimmer-ltr 4s ease-in-out infinite;
  transition:
    transform var(--page-ms) var(--page-ease),
    opacity 0.8s var(--page-ease),
    filter 0.8s var(--page-ease);
}
.bio__line:last-child {
  margin-bottom: 0;
}
.bio__line:nth-child(2) { animation-delay: 0.25s; }
.bio__line:nth-child(3) { animation-delay: 0.5s; }
.bio__line:nth-child(4) { animation-delay: 0.75s; }
/* Enter: odd lines from the left, even lines from the right, staggered. */
.page--bio:not(.is-active) .bio__line {
  opacity: 0;
  filter: blur(10px);
}
.page--bio:not(.is-active) .bio__line:nth-child(odd) {
  transform: translateX(-38vw);
}
.page--bio:not(.is-active) .bio__line:nth-child(even) {
  transform: translateX(38vw);
}
.page--bio.is-active .bio__line:nth-child(1) { transition-delay: 0.05s; }
.page--bio.is-active .bio__line:nth-child(2) { transition-delay: 0.11s; }
.page--bio.is-active .bio__line:nth-child(3) { transition-delay: 0.17s; }
.page--bio.is-active .bio__line:nth-child(4) { transition-delay: 0.23s; }
.page--bio.is-active .bio__line:nth-child(5) { transition-delay: 0.29s; }
.bio__accent {
  font-family: var(--font-serif);
  font-style: italic;
  font-weight: 400;
  font-size: 1.06em;
  letter-spacing: -0.01em;
  text-transform: none;
}

/* =====================================================================
   ABOUT — photo helix left, copy right, three steps on one page.
   z-map: 1 .helix, 2 .about__copy
   ===================================================================== */
:root {
  --helix-photo-h: clamp(120px, 15vw, 210px);
}
.page--about {
  background: var(--black);
}
.helix {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 52vw;
  z-index: 1;
  perspective: 1400px;
  perspective-origin: 50% 50%;
  /* fade the strand out at the top and bottom of the viewport */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 18%, #000 82%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 18%, #000 82%, transparent 100%);
  transition:
    opacity 0.9s var(--page-ease),
    transform var(--page-ms) var(--page-ease);
}
.page--about:not(.is-active) .helix {
  opacity: 0;
  transform: translateY(10vh);
}
.helix__photo {
  position: absolute;
  left: 50%;
  top: 50%;
  margin: 0;
  height: var(--helix-photo-h);
  aspect-ratio: 4 / 5;
  border-radius: 10px;
  overflow: hidden;
  transform-style: preserve-3d;
  backface-visibility: visible;
  box-shadow: 0 24px 60px rgb(0 0 0 / 0.55);
  will-change: transform;
}
.helix__photo--land {
  height: calc(var(--helix-photo-h) * 0.82);
  aspect-ratio: 5 / 4;
}
.helix__photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* depth shade: js sets --shade 0 (front) .. 1 (back) */
.helix__photo::after {
  content: "";
  position: absolute;
  inset: 0;
  background: #000;
  opacity: calc(var(--shade, 0) * 0.72);
  pointer-events: none;
}
/* the rungs of the ladder between paired photos */
.helix__rung {
  position: absolute;
  left: 50%;
  top: 50%;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgb(255 255 255 / 0.55) 30%, rgb(255 255 255 / 0.55) 70%, transparent);
  opacity: 0.35;
  transform-style: preserve-3d;
  pointer-events: none;
}

.about__copy {
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  width: 48vw;
  z-index: 2;
  display: grid;
  align-items: center;
  padding-right: clamp(32px, 6vw, 120px);
  padding-left: 2vw;
  transition:
    opacity 0.9s var(--page-ease),
    transform var(--page-ms) var(--page-ease);
}
.page--about:not(.is-active) .about__copy {
  opacity: 0;
  transform: translateY(14vh);
}
.about__block {
  grid-area: 1 / 1;
  max-width: 560px;
  transition:
    transform 1s var(--page-ease),
    opacity 0.7s var(--page-ease),
    filter 0.7s var(--page-ease);
}
.about__block.is-prev {
  transform: translateY(-9vh);
  opacity: 0;
  filter: blur(8px);
  pointer-events: none;
}
.about__block.is-next {
  transform: translateY(9vh);
  opacity: 0;
  filter: blur(8px);
  pointer-events: none;
}
.about__index {
  margin: 0 0 18px;
  font-size: var(--meta-size);
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgb(255 255 255 / 0.9);
}
.about__index span {
  margin-left: 10px;
  font-weight: 400;
  letter-spacing: 0.02em;
  text-transform: none;
  color: var(--meta-dim);
}
.about__title {
  margin: 0 0 22px;
  font-family: var(--font-display);
  font-weight: 900;
  font-stretch: 100%;
  font-size: clamp(30px, 3.5vw, 60px);
  line-height: 1;
  letter-spacing: -0.03em;
  color: #f4f4f4;
}
.about__block p:not(.about__index) {
  margin: 0 0 14px;
  font-size: clamp(14px, 1.05vw, 17px);
  line-height: 1.6;
  color: rgb(255 255 255 / 0.66);
}

/* =====================================================================
   CONTACT — the bio's wave once more, along the bottom edge as a
   horizon. "Let's talk." is set like the hero name (Archivo 900 / 125,
   difference blend) so the wave inverts through the letters as it
   leans in. Below it: one paragraph, the email (click to copy), links.
   z-map: .contact__bg paints first, .contact after it; neither carries a
   z-index because a stacking context between the title and the wave
   would isolate the blend (see .hero__name).
   ===================================================================== */
.page--contact {
  display: grid;
  place-items: center;
  background: var(--black);
}
.contact__bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
}
.contact__wave {
  position: absolute;
  inset: 0;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(0deg, #000 0%, #000 16%, transparent 60%);
  mask-image: linear-gradient(0deg, #000 0%, #000 16%, transparent 60%);
  transition:
    transform 1.3s var(--page-ease),
    opacity 1s var(--page-ease);
}
.contact__wave-host {
  position: absolute;
  inset: 0;
}
.contact__wave-host canvas {
  width: 100%;
  height: 100%;
}
.page--contact:not(.is-active) .contact__wave {
  transform: translateY(24vh);
  opacity: 0;
}

.contact {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  width: min(92vw, 1240px);
  padding-top: calc(var(--dock-top) + var(--dock-h) + 2vh);
  padding-bottom: 8vh;
}
.contact__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  width: 100%;
  margin-bottom: clamp(18px, 3.2vh, 40px);
}
.contact__index {
  margin: 0;
  font-size: var(--meta-size);
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgb(255 255 255 / 0.9);
}
.contact__meta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-size: var(--meta-size);
  font-weight: 500;
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.contact__title {
  margin: 0 0 clamp(18px, 3vh, 34px) -0.04em;
  font-family: var(--font-display);
  font-weight: 900;
  font-stretch: 125%;
  font-size: clamp(56px, 9.4vw, 184px);
  line-height: 0.86;
  letter-spacing: var(--name-tracking);
  text-transform: uppercase;
  color: var(--name-ink);
  mix-blend-mode: difference;
  pointer-events: none;
}
.contact__statement {
  max-width: 52ch;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: clamp(16px, 1.35vw, 22px);
  line-height: 1.45;
  letter-spacing: -0.006em;
}
.contact__line {
  margin-bottom: 0;
}
.contact__accent {
  font-family: var(--font-serif);
  font-style: italic;
  font-weight: 400;
  font-size: 1.06em;
  letter-spacing: -0.01em;
}
/* The email is a button that copies itself; the hint under it flips
   from "Click to copy" to "Copied" for a beat (js toggles .is-copied). */
.contact__email {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  margin: clamp(26px, 5vh, 56px) 0 0;
  padding: 0;
  border: 0;
  background: none;
  font-family: var(--font-body);
  font-weight: 300;
  font-size: clamp(24px, 3.1vw, 54px);
  line-height: 1.05;
  letter-spacing: -0.02em;
  text-align: left;
  cursor: pointer;
  color: var(--white);
}
.contact__email:focus-visible {
  outline: none;
}
.contact__email .shimmer {
  color: rgb(255 255 255 / 0.92);
  transition: color 0.4s var(--ease-out);
}
.contact__email:hover .shimmer,
.contact__email:focus-visible .shimmer {
  color: var(--white);
}
.contact__hint {
  position: relative;
  display: block;
  height: 1.3em;
  overflow: hidden;
  font-size: var(--meta-size);
  font-weight: 500;
  line-height: 1.3em;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--meta-dim);
}
.contact__hint-word {
  display: block;
  transition:
    transform 0.55s var(--ease-out),
    opacity 0.4s var(--ease-out);
}
.contact__hint-word:nth-child(2) {
  position: absolute;
  left: 0;
  top: 0;
  color: var(--white);
  transform: translateY(110%);
  opacity: 0;
}
.contact__email.is-copied .contact__hint-word:nth-child(1) {
  transform: translateY(-110%);
  opacity: 0;
}
.contact__email.is-copied .contact__hint-word:nth-child(2) {
  transform: none;
  opacity: 1;
}
.contact__links {
  display: flex;
  gap: 32px;
  margin: clamp(22px, 4vh, 44px) 0 0;
  padding: 0;
  list-style: none;
}
/* The dock's link (two sliding hairlines), a size up. */
.contact__link {
  font-size: 13px;
}
/* Authentic Intelligence: the label sits in the link row's voice with
   the dock CTA's "+"; hovering the node (or focusing / tapping the
   label, which toggles .is-open) fans four frosted icon tiles out to
   the right, one after another. Leaving folds them back at once. */
.contact__ai {
  display: flex;
  align-items: center;
  gap: 18px;
  width: max-content;
  margin-top: 22px;
}
.contact__ai-label {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin: 0;
  padding: 6px 0;
  border: 0;
  background: none;
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgb(255 255 255 / 0.86);
  cursor: pointer;
  transition: color 0.4s var(--ease-out);
}
.contact__ai-label:focus-visible {
  outline: none;
}
.contact__ai:hover .contact__ai-label,
.contact__ai:focus-within .contact__ai-label,
.contact__ai.is-open .contact__ai-label {
  color: var(--white);
}
.contact__ai-plus {
  font-size: 16px;
  line-height: 1;
  color: var(--meta-dim);
  transition:
    transform 0.5s var(--ease-out),
    color 0.4s var(--ease-out);
}
.contact__ai:hover .contact__ai-plus,
.contact__ai:focus-within .contact__ai-plus,
.contact__ai.is-open .contact__ai-plus {
  transform: rotate(90deg);
  color: var(--white);
}
.contact__ai-icons {
  display: flex;
  gap: 10px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.contact__ai-icon {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 1px solid rgb(255 255 255 / 0.14);
  background: rgb(22 22 22 / 0.62);
  -webkit-backdrop-filter: blur(22px) saturate(140%);
  backdrop-filter: blur(22px) saturate(140%);
  color: var(--white);
  opacity: 0;
  transform: translateX(-12px) scale(0.8);
  pointer-events: none;
  transition:
    opacity 0.3s var(--ease-out),
    transform 0.5s var(--ease-out),
    background-color 0.35s var(--ease-out),
    border-color 0.35s var(--ease-out),
    color 0.35s var(--ease-out);
}
.contact__ai-icon svg {
  width: 16px;
  height: 16px;
  display: block;
}
.contact__ai:hover .contact__ai-icon,
.contact__ai:focus-within .contact__ai-icon,
.contact__ai.is-open .contact__ai-icon {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
.contact__ai:hover li:nth-child(2) .contact__ai-icon,
.contact__ai:focus-within li:nth-child(2) .contact__ai-icon,
.contact__ai.is-open li:nth-child(2) .contact__ai-icon { transition-delay: 0.06s; }
.contact__ai:hover li:nth-child(3) .contact__ai-icon,
.contact__ai:focus-within li:nth-child(3) .contact__ai-icon,
.contact__ai.is-open li:nth-child(3) .contact__ai-icon { transition-delay: 0.12s; }
.contact__ai-icon:hover,
.contact__ai-icon:focus-visible {
  background: var(--white);
  border-color: var(--white);
  color: var(--black);
  outline: none;
}
/* No href yet: the tile shows but reads as inert. */
.contact__ai-icon:not([href]) {
  cursor: default;
}

/* Enter: the wave rises into place, the copy lifts in from below in
   reading order, each piece a beat after the last. */
.contact__top,
.contact__title,
.contact__statement,
.contact__email,
.contact__links,
.contact__ai {
  transition:
    transform var(--page-ms) var(--page-ease),
    opacity 0.8s var(--page-ease),
    filter 0.8s var(--page-ease);
}
.page--contact:not(.is-active) .contact__top,
.page--contact:not(.is-active) .contact__title,
.page--contact:not(.is-active) .contact__statement,
.page--contact:not(.is-active) .contact__email,
.page--contact:not(.is-active) .contact__links,
.page--contact:not(.is-active) .contact__ai {
  opacity: 0;
  transform: translateY(7vh);
  filter: blur(10px);
}
.page--contact.is-active .contact__top { transition-delay: 0.05s; }
.page--contact.is-active .contact__title { transition-delay: 0.11s; }
.page--contact.is-active .contact__statement { transition-delay: 0.17s; }
.page--contact.is-active .contact__email { transition-delay: 0.23s; }
.page--contact.is-active .contact__links { transition-delay: 0.29s; }
.page--contact.is-active .contact__ai { transition-delay: 0.35s; }

/* =====================================================================
   FLIP — the project slab. Two card faces back to back, 14px apart, and
   two hairline edges so it reads as a solid piece of glass mid-turn.
   .flip__inner rotates about X in 180° steps (js sets --flip-angle).
   ===================================================================== */
:root {
  --slab-depth: 14px;
}
.flip {
  grid-row: 2;
  position: relative;
  z-index: 3;
  width: var(--card-w);
  perspective: 2200px;
  transition:
    opacity 0.9s var(--page-ease),
    transform var(--page-ms) var(--page-ease);
}
.page--projects:not(.is-active) .flip {
  opacity: 0;
  transform: translateY(10vh) scale(0.96);
}
.flip__inner {
  position: relative;
  width: 100%;
  transform-style: preserve-3d;
  transform: rotateX(var(--flip-angle, 0deg));
  transition: transform var(--page-ms) var(--page-ease);
}
.flip__face {
  position: absolute;
  inset: 0;
  height: auto;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
/* Face A is in flow and sets the slab's height; face B sits behind it. */
.flip__face--a {
  position: relative;
  transform: translateZ(calc(var(--slab-depth) / 2));
}
.flip__face--b {
  transform: rotateX(180deg) translateZ(calc(var(--slab-depth) / 2));
}
/* Faces are the card; the card no longer defines its own width. */
.flip__face.card {
  width: 100%;
}
.flip__edge {
  position: absolute;
  left: 0;
  right: 0;
  height: var(--slab-depth);
  background: var(--card-line);
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.flip__edge--top {
  top: 0;
  transform-origin: 50% 0;
  transform: rotateX(-90deg) translateY(calc(var(--slab-depth) * -1));
}
.flip__edge--bottom {
  bottom: 0;
  transform-origin: 50% 100%;
  transform: rotateX(90deg) translateY(var(--slab-depth));
}


@media (prefers-reduced-motion: reduce) {
  .hero__name,
  .hero__figure-wrap,
  .hero-status__inner,
  .projects-status__inner,
  .bio__wave,
  .bio__line,
  .helix,
  .about__copy,
  .about__block,
  .contact__wave,
  .contact__top,
  .contact__title,
  .contact__statement,
  .contact__email,
  .contact__links,
  .contact__ai,
  .contact__ai-plus,
  .contact__ai-icon,
  .contact__hint-word,
  .flip,
  .flip__inner,
  .flip__edge,
  .project__bg,
  .resume__page {
    transition: none;
  }
  .resume__caret {
    animation: none;
  }
}

/* =====================================================================
   RESPONSIVE — every size other than a wide desktop. Only overrides;
   the desktop rules above stay the source of truth.

   stacked   (max-aspect-ratio: 11/10)  portrait of any size: the hero puts
                                        the name above a smaller figure.
   narrow    (max-width: 899px)         tablets in portrait, phones both
                                        ways: dock sub-rows drop below the
                                        bar, About lays the helix behind the
                                        copy, text pages scroll inside
                                        themselves if they must.
   phone     (max-width: 599px)         compact dock across the width,
                                        tighter type, notch / home bar safe
                                        areas, fewer shaders and blur layers.
   short     (max-height: 560px)        landscape phones.
   Touch (hover: none) is handled in js: prompts say "Swipe", card motions
   play on landing (.is-live, rules sit next to the :hover ones).
   ===================================================================== */

/* ---------- stacked hero (portrait) ----------
   Name block under the dock: MANEET flush left with its line above it,
   KOHLI flush right with the clock under it, both the full column wide.
   The figure keeps the space that is left, never wider than 140vw, and is
   shifted so the head (not the photo) sits on the centre line. The status
   line still sits on the torso at the bottom. */
@media (max-aspect-ratio: 11/10) {
  :root {
    --hero-name-top: calc(var(--dock-top) + var(--dock-h) + 44px);
    --name-size: clamp(
      36px,
      min(calc((100vw - 2 * var(--edge)) / var(--name-em-width)), 24vh),
      140px
    );
    /* 1.68em: two 0.8em line boxes plus the gap between the words.
       56px: the clock line under KOHLI plus breathing room. */
    --figure-h: min(
      calc(100svh - var(--hero-name-top) - 1.68 * var(--name-size) - 56px),
      calc(140vw / var(--figure-aspect))
    );
    --figure-shift: calc((var(--head-right) - var(--head-left)) / 2 * var(--figure-w));
  }
  .hero__name {
    inset: auto;
    top: var(--hero-name-top);
    left: var(--edge);
    right: var(--edge);
  }
  .hero__name-word {
    position: relative;
    top: auto;
    left: auto;
    right: auto;
    margin-top: 0;
  }
  .hero__name-word--first {
    text-align: left;
  }
  .hero__name-word--last {
    text-align: right;
    margin-top: 0.08em;
  }
  .hero-status {
    gap: 16px;
    padding-bottom: calc(7vh + env(safe-area-inset-bottom, 0px));
  }
}

/* ---------- narrow ---------- */
@media (max-width: 899px) {
  :root {
    --dock-h: 54px;
    --dock-tile: 40px;
    --dock-sig-w: 84px;
    --meta-size: 12px;
    --card-w: min(92vw, 460px);
    --helix-photo-h: clamp(104px, 30vw, 200px);
  }

  /* Dock: the bar keeps its main row; About me / Projects open a list
     under it instead of swapping the row (their labels do not fit). The
     js-measured --nav-w only matters for the in-bar swap, so it is ignored. */
  .dock {
    gap: 20px;
    padding: 7px 7px 7px 10px;
  }
  .dock__links {
    gap: 20px;
  }
  .dock__nav {
    width: auto;
  }
  .dock__panel[data-panel="main"]:not(.is-on) {
    opacity: 1;
    transform: none;
    filter: none;
    pointer-events: auto;
  }
  .dock__panel:not([data-panel="main"]) {
    position: absolute;
    left: 0;
    right: 0;
    top: calc(100% + 8px);
    justify-self: stretch;   /* the grid's center would shrink it to its content */
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 6px;
    border-radius: 22px;
    background: rgb(22 22 22 / 0.8);
    border: 1px solid rgb(255 255 255 / 0.08);
    box-shadow:
      0 20px 60px rgb(0 0 0 / 0.45),
      inset 0 1px 0 rgb(255 255 255 / 0.06);
    -webkit-backdrop-filter: blur(22px) saturate(140%);
    backdrop-filter: blur(22px) saturate(140%);
  }
  .dock__panel:not([data-panel="main"]):not(.is-on) {
    transform: translateY(-6px);
  }
  .dock__panel:not([data-panel="main"]) li {
    display: block;
  }
  .dock__crumb {
    width: 100%;
    justify-content: flex-start;
    margin-bottom: 4px;
    padding: 10px 14px 12px;
    border-right: 0;
    border-bottom: 1px solid rgb(255 255 255 / 0.1);
  }
  .dock__panel:not([data-panel="main"]) .dock__link {
    display: block;
    width: 100%;
    padding: 12px 14px;
    border-radius: 15px;
    text-align: left;
    transition:
      color 0.4s var(--ease-out),
      background-color 0.3s var(--ease-out);
  }
  .dock__panel:not([data-panel="main"]) .dock__link::before,
  .dock__panel:not([data-panel="main"]) .dock__link::after {
    display: none;
  }
  .dock__panel:not([data-panel="main"]) .dock__link:hover,
  .dock__panel:not([data-panel="main"]) .dock__link:focus-visible {
    background: rgb(255 255 255 / 0.08);
  }

  /* Hero status: the cycling line has no room for a fixed min width. */
  .cycle {
    min-width: 0;
  }

  /* Bio: the column becomes the page's own scroller. Auto margins keep
     the statement centred while it fits and let it start at the top and
     scroll when it does not (short phones). */
  .bio__text {
    position: absolute;
    inset: 0;
    width: auto;
    display: flex;
    flex-direction: column;
    padding: calc(var(--dock-top) + var(--dock-h) + 3vh) var(--edge) 7vh;
    overflow-x: hidden;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }
  /* The shade stays inside the scroller's box: anything hanging past its
     bottom edge would become scroll range. */
  .bio__text::before {
    inset: 0 -10%;
  }
  .bio__statement {
    width: 100%;
    margin: auto;
    font-size: clamp(15px, min(3.4vw, 5vh), 24px);
  }

  /* Projects: the card by viewport width, tags allowed a second row. */
  .projects__title {
    font-size: 12px;
    letter-spacing: 0.22em;
    padding: 11px 20px;
  }
  .card__tags {
    flex-wrap: wrap;
    justify-content: flex-start;
    font-size: 10px;
  }
  .resume {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .resume__page--letter {
    display: none;
  }

  /* About: the helix spans the page behind the copy, dimmed and pushed
     back, with a dark pool under the text so it reads.
     z-map here: 1 .helix, 1 .page--about::after (the pool), 2 .about__copy */
  .helix {
    width: 100%;
    --helix-r: 0.36;
    opacity: 0.58;
    -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 14%, #000 86%, transparent 100%);
    mask-image: linear-gradient(to bottom, transparent 0%, #000 14%, #000 86%, transparent 100%);
  }
  .helix__photo::after {
    opacity: calc(0.22 + var(--shade, 0) * 0.66);
  }
  .page--about::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    background: radial-gradient(ellipse 64% 56% at 50% 52%, rgb(0 0 0 / 0.84) 0%, rgb(0 0 0 / 0.58) 48%, rgb(0 0 0 / 0) 100%);
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.9s var(--page-ease);
  }
  .page--about.is-active::after {
    opacity: 1;
  }
  .about__copy {
    left: 0;
    width: 100%;
    justify-items: center;
    padding: calc(var(--dock-top) + var(--dock-h) + 3vh) var(--edge) calc(60px + env(safe-area-inset-bottom, 0px));
    overflow-x: hidden;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }
  .about__block {
    width: 100%;
    max-width: 540px;
    text-shadow: 0 1px 18px rgb(0 0 0 / 0.6);
  }
  .about__block p:not(.about__index) {
    color: rgb(255 255 255 / 0.78);
  }

  /* Contact: same scroller treatment; the title is one line up to the
     phone breakpoint, sized to the column. */
  .contact {
    position: absolute;
    inset: 0;
    width: auto;
    padding: calc(var(--dock-top) + var(--dock-h) + 3vh) var(--edge) calc(6vh + env(safe-area-inset-bottom, 0px));
    overflow-x: hidden;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }
  .contact__top {
    margin-top: auto;
  }
  .contact__ai {
    margin-bottom: auto;
  }
  .contact__title {
    font-size: min(clamp(56px, 9.4vw, 184px), calc((100vw - 2 * var(--edge)) / 7.7));
  }
}

/* ---------- phone ---------- */
@media (max-width: 599px) {
  :root {
    --edge: 18px;
    --dock-top: calc(10px + env(safe-area-inset-top, 0px));
    --dock-h: 50px;
    --dock-tile: 38px;
    --dock-sig-w: 64px;
    --blur-h: 18vh;
  }

  /* Dock across the width: signature, About me / Projects, Contact.
     Home goes; the signature already goes home. */
  .dock {
    left: 10px;
    right: 10px;
    width: auto;
    justify-content: space-between;
    gap: 10px;
    padding: 6px 6px 6px 8px;
  }
  .dock__panel[data-panel="main"] li:first-child {
    display: none;
  }
  .dock__links {
    gap: 16px;
  }
  .dock__link {
    padding: 8px 0;
    font-size: 11px;
    letter-spacing: 0.08em;
  }
  .dock__panel:not([data-panel="main"]) .dock__link {
    padding: 12px 14px;
  }
  .dock__cta {
    gap: 10px;
    padding: 0 14px 0 16px;
    font-size: 11px;
  }

  /* Chrome: dots and prompts clear the home indicator; the three
     faintest blur layers (under 0.6px) are not worth a phone's GPU. */
  .dots {
    bottom: calc(22px + env(safe-area-inset-bottom, 0px));
  }
  .projects-status {
    padding-bottom: calc(9vh + env(safe-area-inset-bottom, 0px));
  }
  .edge-blur__layer:nth-child(-n+3) {
    display: none;
  }
  .cycle {
    font-size: 12px;
    letter-spacing: 0.22em;
  }

  /* Bio: two waves (top and bottom) instead of four. The side hosts are
     display: none, so js/site.js never mounts them. */
  .bio__wave--left,
  .bio__wave--right {
    display: none;
  }

  /* Projects */
  .projects__title {
    font-size: 11px;
    letter-spacing: 0.2em;
    padding: 10px 16px;
  }
  .card {
    padding: 10px;
    border-radius: 16px;
    gap: 8px;
  }
  .card__name {
    font-size: 14px;
  }
  .card__tags {
    font-size: 9.5px;
  }
  .project__bg--sky > .sky__hills {
    height: 30vh;
    min-height: 180px;
  }

  /* About */
  .about__index {
    margin-bottom: 12px;
  }
  .about__title {
    margin-bottom: 14px;
    font-size: clamp(28px, 8.6vw, 40px);
  }
  .about__block p:not(.about__index) {
    margin-bottom: 12px;
    font-size: 14.5px;
    line-height: 1.55;
  }

  /* Contact: the title breaks into LET'S / TALK. at a size the column can
     carry; everything under it tightens up. */
  .contact__top {
    margin-bottom: 14px;
  }
  .contact__title {
    margin-bottom: 14px;
    font-size: clamp(48px, 17.5vw, 96px);
  }
  .contact__statement {
    font-size: 15px;
    line-height: 1.5;
  }
  .contact__email {
    margin-top: 22px;
    font-size: clamp(20px, 6.4vw, 30px);
  }
  .contact__links {
    margin-top: 18px;
    gap: 22px;
  }
  .contact__ai {
    width: 100%;
    flex-wrap: wrap;
    gap: 10px 16px;
    margin-top: 16px;
  }
  .contact__ai-label {
    font-size: 12px;
  }
}

/* ---------- short (landscape phones) ----------
   The card is sized from the height (16:10 preview plus ~110px of head,
   tags and padding) and the page keeps 40px clear above the dots. Under
   480px the title pill and the swipe prompt go; the dots still show where
   you are. */
@media (max-height: 560px) {
  :root {
    --card-w: min(92vw, 440px, calc((100svh - 230px) * 1.6));
  }
  .page--projects {
    padding-bottom: 40px;
  }
  .hero-status {
    gap: 12px;
    padding-bottom: 6vh;
  }
}
@media (max-height: 480px) {
  :root {
    --card-w: min(92vw, 560px);
  }
  .projects__title,
  .projects-status {
    display: none;
  }
  /* The card turns sideways: preview on the left sets the height, the
     name and tags share the right half. */
  .card {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
    column-gap: 12px;
  }
  .card__preview {
    grid-column: 1;
    grid-row: 1 / 3;
  }
  .card__head {
    grid-column: 2;
    grid-row: 1;
    padding-top: 4px;
  }
  .card__tags {
    grid-column: 2;
    grid-row: 2;
    align-self: end;
  }
}

/* Wide windows can be short too (landscape tablets, split-screen, zoom).
   Keep the desktop columns, but give overflowing copy a reachable scroll
   range instead of letting the page's overflow:hidden cut it off. */
@media (min-width: 900px) and (max-height: 650px) {
  .bio__text,
  .contact {
    position: absolute;
    inset: 0;
    width: auto;
    padding: calc(var(--dock-top) + var(--dock-h) + 3vh) 4vw 7vh;
  }
  .bio__text {
    display: flex;
    flex-direction: column;
  }
  .bio__text::before { inset: 0 -10%; }
  .bio__statement { margin-block: auto; flex-shrink: 0; }
  .bio__text,
  .about__copy,
  .contact {
    overflow-x: hidden;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  .about__copy {
    padding-top: calc(var(--dock-top) + var(--dock-h) + 3vh);
    padding-bottom: 48px;
    align-items: safe center;
  }
  .contact__top { margin-top: auto; }
  .contact__ai { margin-bottom: auto; }
  .contact > * { flex-shrink: 0; }
}

/* Keep all four dock targets separate on small phones. The main row is
   intrinsically sized, so shrinking its grid track alone overlaps them. */
@media (max-width: 359px) {
  :root { --dock-sig-w: 36px; }
  .dock { gap: 6px; }
  .dock__links { gap: 10px; }
  .dock__cta { gap: 6px; padding-inline: 10px; }
}

/* Mobile / mask fallback. Transform transitions retain a visible page
   turn without rasterizing a viewport-sized animated mask every frame. */
.use-slide-transitions .page--projects {
  -webkit-mask-image: none;
  mask-image: none;
  transition: transform var(--page-ms) var(--page-ease);
}
.use-slide-transitions .page--projects.is-below {
  transform: translateY(100%);
}
.use-slide-transitions .page--projects .project__bg {
  -webkit-mask-image: none;
  mask-image: none;
  transform: translateY(100%);
  transition: transform 0.85s var(--page-ease) 0.3s;
}
.use-slide-transitions .page--projects[data-dir="back"] .project__bg {
  transform: translateY(-100%);
}
.use-slide-transitions .page--projects .project__bg.is-on,
.use-slide-transitions .page--projects .project__bg.is-off {
  transform: translateY(0);
}
.use-slide-transitions .page--projects.is-instant .project__bg,
.use-slide-transitions .pages.is-instant .page--projects {
  transition: none;
}

/* This must follow the choreography and responsive rules. Earlier
   reduced-motion declarations were overridden by later transitions. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }
}
