/* =========================================================================
   Walid Mahmud — portfolio
   Design notes: white paper, black ink, pastel pigment. Motion is slow and
   optical rather than bouncy; nothing moves faster than the eye expects.
   ========================================================================= */

/* ---------- Tokens ---------- */
:root {
  --bg:        #ffffff;
  --ink:       #0b0b0c;
  --ink-soft:  #1d1d1f;
  --muted:     #6e6e73;
  --muted-2:   #86868b;
  --line:      rgba(0, 0, 0, 0.09);
  --line-soft: rgba(0, 0, 0, 0.055);

  /* Pastel pigments */
  --p-blush:  #ffd6e2;
  --p-peach:  #ffdfc7;
  --p-butter: #fff2c9;
  --p-mint:   #c9f0df;
  --p-sky:    #d0e4ff;
  --p-lilac:  #e0d6ff;

  /* Glass */
  --glass-bg:     rgba(255, 255, 255, 0.42);
  --glass-bg-2:   rgba(255, 255, 255, 0.80);
  --glass-stroke: rgba(255, 255, 255, 0.92);
  --glass-shade:  rgba(16, 16, 20, 0.10);

  /* The etched face of tempered glass. Same trick as the paper grain, at a
     much tighter frequency and desaturated, so it reads as a frosted surface
     rather than as fibre. Held in a token because three rules need the exact
     same texture and a data URI is not something to paste around. */
  --frost: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='f'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.1' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23f)' opacity='0.2'/%3E%3C/svg%3E");

  /* Body text sitting on a tempered panel. --muted lands around 4.2:1 there,
     under the bar for text this size, and the streets below the hero pull the
     worst case lower still. A few steps darker clears it at the lede's mobile
     size without the paragraph starting to compete with the headline. */
  --on-glass: #4d4d53;

  --radius:    22px;
  --radius-lg: 30px;
  --radius-pill: 999px;

  --wrap: 1120px;
  --gutter: clamp(20px, 5vw, 48px);

  /* Apple-ish easing: decisive out, no overshoot */
  --ease:      cubic-bezier(0.22, 1, 0.36, 1);
  --ease-soft: cubic-bezier(0.4, 0, 0.2, 1);

  --font: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text",
          "Helvetica Neue", "Segoe UI", Inter, Roboto, system-ui, sans-serif;
}

/* ---------- Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 110px;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-size: 17px;
  line-height: 1.55;
  letter-spacing: -0.011em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* `clip` rather than `hidden`: hidden makes the body a scroll container,
     which breaks the sticky hero stage. Older browsers keep the fallback. */
  overflow-x: hidden;
  overflow-x: clip;
}

h1, h2, h3, p, dl, dd, ul { margin: 0; }
ul { padding: 0; list-style: none; }
img, svg { display: block; max-width: 100%; }

a {
  color: inherit;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}

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

.skip-link {
  position: fixed;
  top: 12px;
  left: 12px;
  z-index: 200;
  padding: 10px 16px;
  border-radius: var(--radius-pill);
  background: var(--ink);
  color: #fff;
  font-size: 14px;
  transform: translateY(-160%);
  transition: transform 0.3s var(--ease);
}
.skip-link:focus-visible { transform: translateY(0); }

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* =========================================================================
   Boot curtain
   The same idea as a device starting up: one mark, one thin bar, a flat
   ground, and nothing else on screen. The bar is driven by --boot, which JS
   writes as real loading progress rather than a canned animation.
   ========================================================================= */
.boot { display: none; }

.booting .boot {
  display: grid;
  position: fixed;
  inset: 0;
  z-index: 9999;
  place-items: center;
  background: var(--bg);
  /* The page underneath is mid-assembly; nothing there should be clickable. */
  pointer-events: auto;
}

.boot__inner {
  display: grid;
  justify-items: center;
  /* Mark and welcome share one cell so the second arrives exactly where the
     first left, instead of the block shifting as they swap. */
  grid-template-areas: "mark" "track";
  gap: 30px;
}

.boot__mark,
.boot__welcome { grid-area: mark; }

.boot__mark {
  font-size: 40px;
  font-weight: 600;
  letter-spacing: -0.04em;
  color: var(--ink);
  transition: opacity 0.38s var(--ease);
}

.boot__track {
  grid-area: track;
  width: 190px;
  height: 3px;
  border-radius: var(--radius-pill);
  background: var(--line);
  overflow: hidden;
  transition: opacity 0.38s var(--ease);
}

/* scaleX off a 0-1 value: the bar is composited, never re-laid-out. */
.boot__fill {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: inherit;
  background: var(--ink);
  transform: scaleX(var(--boot, 0));
  transform-origin: 0 50%;
  /* Short, linear: the JS already eases the number, and a second ease on top
     of that reads as lag rather than smoothness. */
  transition: transform 0.12s linear;
}

.boot__welcome {
  font-size: clamp(38px, 6vw, 68px);
  font-weight: 600;
  letter-spacing: -0.045em;
  color: var(--ink);
  opacity: 0;
  translate: 0 14px;
  transition: opacity 0.7s var(--ease), translate 0.7s var(--ease);
}

/* The mark and the bar step aside; the greeting takes their place. */
.boot[data-boot="welcome"] .boot__mark,
.boot[data-boot="welcome"] .boot__track,
.boot[data-boot="done"] .boot__mark,
.boot[data-boot="done"] .boot__track { opacity: 0; }

.boot[data-boot="welcome"] .boot__welcome,
.boot[data-boot="done"] .boot__welcome { opacity: 1; translate: 0 0; }

/* And the whole curtain lifts off the finished page. */
.boot[data-boot="done"] {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.9s var(--ease);
}

/* Entrance choreography is held while the curtain is up, or it would play out
   behind it and the page would be sitting there fully assembled the moment
   the white lifted. Pausing rather than delaying keeps the timing in the one
   place it is written — the animation shorthands themselves.

   Its own class, not `booting`: the hold is released as the curtain starts to
   fade, so the hero rises *through* the white instead of after it. */
.boot-hold #header *,
.boot-hold #main *,
.boot-hold .blob { animation-play-state: paused !important; }

/* =========================================================================
   Watercolor field
   One blur pass on the container (cheap) + multiply blending so the pigment
   reads as ink soaking into paper rather than glowing light.
   ========================================================================= */
.watercolor {
  position: fixed;
  inset: -20vh -10vw;
  z-index: -2;
  opacity: 0.9;
  pointer-events: none;
  /* Scroll-linked drift, written by JS */
  translate: 0 calc(var(--wc-y, 0) * 1px);
  will-change: translate;
}

/* The blur lives on each blob, not on the container, and the difference is
   the whole cost of this layer.

   With the filter on the parent, every frame a child moved the browser had to
   re-rasterize the entire 140%-of-viewport container through a 72px blur —
   continuously, forever, even while the hero canvas was covering it. On
   WebKit in particular that alone was a large slice of the frame. With the
   filter on the blob, each one is rasterized once and its drift is a
   transform on a composited layer: the compositor just slides a pre-blurred
   bitmap around, and the raster cost of this whole layer drops to nothing. */
.blob {
  position: absolute;
  display: block;
  border-radius: 50%;
  filter: blur(72px) saturate(118%);
  mix-blend-mode: multiply;
  opacity: 0.72;
  will-change: transform;
}

.blob--blush {
  width: 46vw; height: 46vw; min-width: 340px; min-height: 340px;
  top: -6vh; left: -4vw;
  background: radial-gradient(circle at 35% 35%, var(--p-blush), rgba(255, 214, 226, 0) 68%);
  animation: drift-a 34s var(--ease-soft) infinite;
}
.blob--sky {
  width: 52vw; height: 52vw; min-width: 380px; min-height: 380px;
  top: 2vh; right: -12vw;
  background: radial-gradient(circle at 60% 40%, var(--p-sky), rgba(208, 228, 255, 0) 68%);
  animation: drift-b 41s var(--ease-soft) infinite;
}
.blob--mint {
  width: 40vw; height: 40vw; min-width: 300px; min-height: 300px;
  top: 58vh; left: 8vw;
  background: radial-gradient(circle at 45% 55%, var(--p-mint), rgba(201, 240, 223, 0) 66%);
  animation: drift-c 37s var(--ease-soft) infinite;
}
.blob--lilac {
  width: 44vw; height: 44vw; min-width: 320px; min-height: 320px;
  top: 74vh; right: -6vw;
  background: radial-gradient(circle at 50% 50%, var(--p-lilac), rgba(224, 214, 255, 0) 68%);
  animation: drift-a 45s var(--ease-soft) infinite reverse;
}
.blob--peach {
  width: 36vw; height: 36vw; min-width: 280px; min-height: 280px;
  top: 128vh; left: 26vw;
  background: radial-gradient(circle at 50% 45%, var(--p-peach), rgba(255, 223, 199, 0) 66%);
  animation: drift-b 39s var(--ease-soft) infinite;
}
.blob--butter {
  width: 34vw; height: 34vw; min-width: 260px; min-height: 260px;
  top: 152vh; right: 14vw;
  background: radial-gradient(circle at 45% 50%, var(--p-butter), rgba(255, 242, 201, 0) 66%);
  animation: drift-c 43s var(--ease-soft) infinite reverse;
}

@keyframes drift-a {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  33%      { transform: translate3d(6vw, 4vh, 0) scale(1.12); }
  66%      { transform: translate3d(-3vw, 7vh, 0) scale(0.94); }
}
@keyframes drift-b {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  30%      { transform: translate3d(-7vw, 5vh, 0) scale(1.08); }
  70%      { transform: translate3d(4vw, -4vh, 0) scale(0.92); }
}
@keyframes drift-c {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  40%      { transform: translate3d(5vw, -6vh, 0) scale(1.14); }
  75%      { transform: translate3d(-6vw, 2vh, 0) scale(0.96); }
}

/* Paper grain — barely there, but it kills the flatness. */
.grain {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0.035;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* =========================================================================
   Liquid glass
   Layered: a blurred backdrop, a translucent body, a specular top edge,
   and a soft contact shadow. The ::after sheen is the "liquid" part.
   ========================================================================= */
.glass {
  position: relative;
  /* Two stacked layers make the surface read as curved rather than flat: a
     specular pool where the light hits, over a body that is bright at the rim,
     thin through the middle, and lifted again at the base. */
  background:
    radial-gradient(125% 85% at 14% -12%,
      rgba(255, 255, 255, 0.98) 0%,
      rgba(255, 255, 255, 0.45) 34%,
      rgba(255, 255, 255, 0) 64%),
    linear-gradient(180deg,
      var(--glass-bg-2) 0%,
      var(--glass-bg) 44%,
      rgba(255, 255, 255, 0.62) 100%);
  /* Gloss lives in what you can see *through* the glass. Heavy blur is what
     made this matte, so the blur comes down and the colour comes up. */
  -webkit-backdrop-filter: blur(13px) saturate(210%) brightness(1.07);
  backdrop-filter: blur(13px) saturate(210%) brightness(1.07);
  border: 1px solid var(--glass-stroke);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 1) inset,             /* crisp lit top edge */
    0 8px 16px -12px rgba(255, 255, 255, 0.95) inset, /* light pooling below it */
    0 -10px 20px -16px rgba(16, 16, 20, 0.22) inset,  /* refraction at the base */
    0 14px 38px -14px var(--glass-shade),
    0 3px 10px -5px rgba(16, 16, 20, 0.06);
  isolation: isolate;
}

/* Specular highlight sweeping the top edge */
.glass::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(
    135deg,
    rgba(255, 255, 255, 0.95) 0%,
    rgba(255, 255, 255, 0.25) 34%,
    rgba(255, 255, 255, 0) 55%,
    rgba(255, 255, 255, 0.55) 100%
  );
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
  z-index: 2;
}

/* Slow-travelling sheen: the liquid in "liquid glass" */
.glass::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(
    100deg,
    transparent 40%,
    rgba(255, 255, 255, 0.28) 46%,
    rgba(255, 255, 255, 0.85) 50%,
    rgba(255, 255, 255, 0.28) 54%,
    transparent 60%
  );
  background-size: 260% 100%;
  background-position: 140% 0;
  opacity: 0;
  transition: opacity 0.5s var(--ease);
  pointer-events: none;
  z-index: 1;
}
.glass:hover::after {
  opacity: 1;
  animation: sheen 1.5s var(--ease) forwards;
}
@keyframes sheen {
  from { background-position: 140% 0; }
  to   { background-position: -40% 0; }
}

/* =========================================================================
   Tempered glass
   A modifier on .glass, not a replacement for it. The polished variety above
   is a mirror with a body; this is the frosted sheet you get in a shower
   screen or a balcony rail. Light comes through it diffused rather than
   reflected, so three things move: the blur goes up a lot, the specular pool
   comes down, and the surface carries a fine etched grain of its own.

   The grain is the part that actually sells it. Without it a heavy blur just
   reads as a translucent rectangle; with it the panel has a surface, and the
   sunset behind it is clearly *behind* something.
   ========================================================================= */
.glass--tempered {
  background:
    var(--frost),
    /* A wider, softer pool than polished glass — frosted surfaces scatter the
       highlight instead of holding it in one place. */
    radial-gradient(150% 110% at 24% -24%,
      rgba(255, 255, 255, 0.88) 0%,
      rgba(255, 255, 255, 0.40) 42%,
      rgba(255, 255, 255, 0) 74%),
    /* Denser than it looks like it needs to be. Streets put genuinely dark
       pixels behind this panel — asphalt in shadow — and at the old alphas the
       lede fell to 3.8:1 over the worst of them. Frosted glass is fairly
       opaque anyway; this is the honest value for it. */
    linear-gradient(180deg,
      rgba(255, 255, 255, 0.88) 0%,
      rgba(255, 255, 255, 0.68) 46%,
      rgba(255, 255, 255, 0.8) 100%);
  background-size: 140px 140px, auto, auto;
  -webkit-backdrop-filter: blur(26px) saturate(150%) brightness(1.05);
  backdrop-filter: blur(26px) saturate(150%) brightness(1.05);
  border-color: rgba(255, 255, 255, 0.72);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.92) inset,   /* lit top edge, softened */
    0 -14px 26px -20px rgba(16, 16, 20, 0.26) inset,
    0 18px 44px -18px rgba(16, 16, 20, 0.15),
    0 2px 8px -4px rgba(16, 16, 20, 0.05);
}

/* The travelling sheen belongs to polished glass. On a frosted sheet it would
   be a wet-look streak across an etched surface — the one detail that would
   undo the rest. What is left is a slow, even brightening. */
.glass--tempered::after {
  background: linear-gradient(
    100deg,
    transparent 34%,
    rgba(255, 255, 255, 0.18) 48%,
    transparent 66%
  );
}
.glass--tempered:hover::after { animation: none; opacity: 1; }

/* =========================================================================
   Header
   ========================================================================= */
.header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  padding: 14px var(--gutter) 0;
  display: flex;
  justify-content: center;
}

.header__progress {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  overflow: hidden;
}
.header__progress span {
  display: block;
  height: 100%;
  width: 100%;
  transform: scaleX(var(--progress, 0));
  transform-origin: 0 50%;
  background: linear-gradient(90deg, var(--p-blush), var(--p-lilac), var(--p-sky), var(--p-mint));
  transition: transform 0.1s linear;
}

.nav {
  width: 100%;
  max-width: var(--wrap);
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 10px 12px 10px 18px;
  border-radius: var(--radius-pill);
}

.nav__mark {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  font-weight: 600;
  font-size: 15px;
  letter-spacing: -0.02em;
  white-space: nowrap;
}
.nav__name {
  overflow: hidden;
  text-overflow: ellipsis;
}
.nav__dot {
  flex: 0 0 auto;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: conic-gradient(from 210deg, var(--p-blush), var(--p-lilac), var(--p-sky), var(--p-mint), var(--p-blush));
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.7);
  animation: spin 9s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.nav__list {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-left: auto;
}
.nav__link {
  position: relative;
  display: inline-block;
  padding: 8px 14px;
  border-radius: var(--radius-pill);
  font-size: 14.5px;
  color: var(--muted);
  transition: color 0.3s var(--ease), background-color 0.3s var(--ease);
}
.nav__link::after {
  content: "";
  position: absolute;
  left: 14px;
  right: 14px;
  bottom: 5px;
  height: 1.5px;
  border-radius: 2px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform 0.4s var(--ease);
}
.nav__link:hover { color: var(--ink); }
.nav__link:hover::after { transform: scaleX(1); }
.nav__link.is-active {
  color: var(--ink);
  background: rgba(0, 0, 0, 0.045);
}

.nav__cta {
  flex: 0 0 auto;
  padding: 9px 18px;
  border-radius: var(--radius-pill);
  background: var(--ink);
  color: #fff;
  font-size: 14.5px;
  font-weight: 500;
  white-space: nowrap;
  transition: transform 0.35s var(--ease), opacity 0.3s var(--ease);
}
.nav__cta-short { display: none; }
.nav__cta:hover { transform: scale(1.04); opacity: 0.9; }
.nav__cta:active { transform: scale(0.98); }

/* --- Header intro: a water droplet falls, lands, and spreads into the bar.
       Runs on index only, once per browser session.

       The droplet and the bar are two elements doing one job. The drop falls
       and splashes; at the moment of impact the bar takes over, unclipped from
       a circle the same size as the splash, so the eye reads one object.
       Both are pinned to the bar's centre: the header is 14px of padding plus
       the bar, so the bar's midline is exactly calc(50% + 7px) down. --- */
.js .header[data-intro="pending"] .nav,
.js .header[data-intro="pending"] .header__progress { opacity: 0; }

.header__drop,
.header__ripple {
  position: absolute;
  top: calc(50% + 7px);
  left: 50%;
  translate: -50% -50%;
  border-radius: 50%;
  opacity: 0;              /* invisible unless the intro is actually playing */
  pointer-events: none;
  z-index: 3;
}

.header__drop {
  width: 34px;
  height: 34px;
  background:
    radial-gradient(circle at 34% 26%,
      rgba(255, 255, 255, 0.99) 0%,
      rgba(255, 255, 255, 0.55) 30%,
      rgba(255, 255, 255, 0.12) 62%,
      rgba(255, 255, 255, 0.4) 100%);
  -webkit-backdrop-filter: blur(6px) saturate(220%) brightness(1.1);
  backdrop-filter: blur(6px) saturate(220%) brightness(1.1);
  border: 1px solid rgba(255, 255, 255, 0.95);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 1) inset,
    0 -4px 8px -6px rgba(16, 16, 20, 0.35) inset,
    0 8px 18px -8px rgba(16, 16, 20, 0.28);
}

.header__ripple {
  width: 44px;
  height: 44px;
  border: 1.5px solid rgba(255, 255, 255, 0.9);
  box-shadow: 0 0 12px -2px rgba(16, 16, 20, 0.12);
}

.header[data-intro="play"] .header__drop {
  animation: drop-fall 0.6s cubic-bezier(0.45, 0, 0.75, 0.4) 0.1s both;
}
/* `forwards`, not `both`: a backwards fill would park the ring at its opening
   keyframe and leave it sitting on the landing spot all through the fall. */
.header[data-intro="play"] .header__ripple {
  animation: drop-splash 0.6s var(--ease) 0.46s forwards;
}

/* The bar is clipped to the splash and spreads outward from it. The second
   animation is a one-frame opacity flip so nothing shows before impact. */
.header[data-intro="play"] .nav {
  animation:
    nav-spread 0.95s var(--ease) 0.46s both,
    fade-in 0.01s linear 0.46s both;
}
.header[data-intro="play"] .header__progress {
  animation: fade-in 0.8s var(--ease) 1.2s both;
}
.header[data-intro="play"] .nav__mark {
  animation: nav-item-in 0.7s var(--ease) 0.7s both;
}
.header[data-intro="play"] .nav__list > li {
  animation: nav-item-in 0.7s var(--ease) both;
}
.header[data-intro="play"] .nav__list > li:nth-child(1) { animation-delay: 0.82s; }
.header[data-intro="play"] .nav__list > li:nth-child(2) { animation-delay: 0.89s; }
.header[data-intro="play"] .nav__list > li:nth-child(3) { animation-delay: 0.96s; }
.header[data-intro="play"] .nav__list > li:nth-child(4) { animation-delay: 1.03s; }
.header[data-intro="play"] .nav__cta {
  animation: nav-item-in 0.7s var(--ease) 1.1s both;
}

/* Returning within the same session: no animation, just there. */
.header[data-intro="done"] .nav,
.header[data-intro="done"] .header__progress { opacity: 1; }

/* Falling water stretches along its direction of travel and flattens on
   impact. The rebound is small — this is a droplet, not a ball.

   The fall is only 70px because that is all the room there is: the bar's
   midline sits ~42px below the top of the screen. Starting higher just means
   animating above the fold where nobody can see it. Stretched, the droplet's
   lower edge is already at the top edge on frame one, so the whole fall is
   on screen. */
@keyframes drop-fall {
  0%   { opacity: 0; translate: -50% calc(-50% - 70px); scale: 0.82 1.26; }
  10%  { opacity: 1; }
  58%  { translate: -50% -50%; scale: 0.86 1.2; animation-timing-function: cubic-bezier(0.3, 0, 0.2, 1); }
  70%  { translate: -50% -50%; scale: 1.5 0.5; opacity: 1; }
  82%  { translate: -50% -50%; scale: 1.1 0.8; opacity: 0.5; }
  100% { translate: -50% -50%; scale: 1.9 0.34; opacity: 0; }
}

/* The splash ring: thrown outward and gone. */
@keyframes drop-splash {
  0%   { opacity: 0.85; scale: 0.34; }
  55%  { opacity: 0.5; }
  100% { opacity: 0; scale: 2.6; }
}

/* Radius in px, not %, so the spread does not depend on how wide the bar is.
   640px is just past the corner of the widest the bar can ever be (--wrap is
   1120, so 560 to the end plus half its height). Overshooting to something
   like 1400 would look like a snap: the easing is front-loaded, so the entire
   visible range would be used up in the first fraction of the animation. */
@keyframes nav-spread {
  0%   { clip-path: circle(17px at 50% 50%); }
  100% { clip-path: circle(640px at 50% 50%); }
}

@keyframes nav-item-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }

/* Header condenses slightly once you start scrolling */
.header.is-scrolled .nav {
  box-shadow:
    0 1px 1px rgba(255, 255, 255, 0.9) inset,
    0 14px 40px -14px rgba(16, 16, 20, 0.16),
    0 3px 10px -6px rgba(16, 16, 20, 0.08);
}

/* =========================================================================
   Hero
   ========================================================================= */
/* The hero is a scroll wrapper around a sticky stage. Until the WebGL skyline
   actually starts, `.skyline-on` is absent and this behaves exactly like the
   old one-viewport hero — which is also the no-WebGL fallback. */
.hero {
  position: relative;
  /* No overflow here: it would break the sticky stage inside. */
}

.hero__stage {
  position: relative;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 140px var(--gutter) 80px;
  overflow: hidden;
}

/* The 3D canvas, behind everything. */
.hero__scene {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: none;
}
.hero__scene canvas { display: block; width: 100%; height: 100%; }

/* Headline, paragraph, buttons, scroll cue and panes ride together: they fade
   out and lift as one over the first quarter of the scroll. */
.hero__copy {
  display: contents;
}

.hero__whiteout {
  position: absolute;
  inset: 0;
  z-index: 6;
  background: #fff;
  opacity: var(--whiteout, 0);
  pointer-events: none;
}

/* --- Everything below applies only once the skyline is actually running --- */
.skyline-on .hero {
  height: calc(var(--hero-vh, 300) * 1vh);
}
.skyline-on .hero__stage {
  position: sticky;
  top: 0;
  height: 100svh;
  min-height: 0;
  /* The stage dissolves into the page over the last of the pin. The white-out
     hides the camera arriving inside the tower, but white is not what is
     underneath — the page has its own pastel field — so ending on it leaves a
     hard line across the screen at the stage's bottom edge. Fading the whole
     stage instead means the thing that scrolls away has already gone.

     Safe as a stacking context: it is only ever below 1 in the last 5% of the
     move, long after the hero's glass panels have faded out, so nothing with
     a backdrop-filter is caught inside a backdrop root. */
  opacity: var(--stage-fade, 1);
}
.skyline-on .hero__scene { display: block; }

/* The static SVG skyline is the fallback; the model replaces it. The hero's
   floating glass panes go too — over a white paper model they read as smudges
   on the lens rather than as depth, which the camera move now provides, and
   the stage is pinned here anyway so nothing inside it could parallax.
   Scoped to the hero: the pane fields in the sections below are a different
   thing and must survive. */
.skyline-on .hero .skyline,
.skyline-on .hero .panes { display: none; }

/* display:contents would drop the fade, so the group becomes a real box only
   when it has something to animate. It mirrors the stage's own layout so the
   copy lands in exactly the same place it did before, and so the absolutely
   positioned scroll cue still measures from the bottom of the viewport. */
.skyline-on .hero__copy {
  display: flex;
  flex-direction: column;
  justify-content: center;
  position: absolute;
  inset: 0;
  z-index: 2;
  padding: 140px var(--gutter) 80px;
  opacity: var(--copy-opacity, 1);
  translate: 0 var(--copy-lift, 0px);
  /* Deliberately no `will-change: opacity` here. It would promote this group
     to its own backdrop root, and every backdrop-filter inside it — the lede's
     tempered pane, both buttons, the portrait — would then sample an empty
     layer instead of the sky behind the stage. Frosted glass with nothing
     showing through it is just a white box. The fade is a handful of text
     nodes and repaints cheaply enough without the hint; the elements that
     actually need compositing get it from their own backdrop-filter. */
}

/* Shorter run on phones, and the reduced-motion path drops the pin entirely
   and keeps a single static frame. */
@media (max-width: 767px) {
  .skyline-on .hero { --hero-vh: 220; }
  .skyline-on .hero__copy { padding: 118px var(--gutter) 80px; }
}
@media (prefers-reduced-motion: reduce) {
  .skyline-on .hero { height: auto; }
  .skyline-on .hero__stage { position: relative; height: auto; min-height: 100svh; }
}

/* The next section arrives out of the middle of the frame rather than cutting
   in or sliding up: it fades while easing forward from slightly behind, so the
   handoff continues the camera move instead of interrupting it. Kept shallow —
   the wrap is tall, and a big scale on a tall box swings its ends around. */
[data-handoff] > .wrap {
  opacity: var(--handoff, 1);
  scale: calc(0.972 + 0.028 * var(--handoff, 1));
}

.hero__inner {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  position: relative;
  z-index: 2;
}

.hero__eyebrow {
  font-size: clamp(13px, 1.3vw, 15px);
  color: var(--muted);
  letter-spacing: 0.02em;
  margin-bottom: clamp(18px, 3vw, 28px);
}

.hero__title {
  font-size: clamp(60px, 13.5vw, 190px);
  line-height: 0.9;
  font-weight: 600;
  letter-spacing: -0.045em;
  margin-bottom: clamp(24px, 3.5vw, 40px);
}
.hero__title .line {
  display: block;
  overflow: hidden;
  padding-bottom: 0.04em;
}
.hero__title .line__in { display: block; }

/* Name and portrait share a row; the title keeps every pixel it needs and the
   portrait takes what is left. */
.hero__lead {
  display: flex;
  align-items: center;
  gap: clamp(24px, 4vw, 64px);
  /* The gap under the name moves here from the h1: as a flex item its margin
     counts toward the row's height, which would centre the portrait against
     the margin box instead of the letters. */
  margin-bottom: clamp(24px, 3.5vw, 40px);
}
.hero__lead .hero__title { flex: 1 1 auto; min-width: 0; margin-bottom: 0; }

/* =========================================================================
   Portrait — a circle of glass with a photograph behind it
   ========================================================================= */
.portrait {
  flex: 0 0 auto;
  margin: 0;
  width: clamp(132px, 17vw, 236px);
  aspect-ratio: 1;
  border-radius: 50%;
  overflow: hidden;
  display: grid;
  place-items: center;
  /* A pastel bloom sits under the glass so the circle still reads as an object
     on the days the photograph is missing. */
  background:
    radial-gradient(125% 85% at 14% -12%,
      rgba(255, 255, 255, 0.98) 0%,
      rgba(255, 255, 255, 0.4) 32%,
      rgba(255, 255, 255, 0) 62%),
    radial-gradient(90% 90% at 70% 78%, var(--p-lilac), transparent 62%),
    radial-gradient(85% 85% at 26% 30%, var(--p-sky), transparent 60%),
    linear-gradient(180deg, var(--glass-bg-2), rgba(255, 255, 255, 0.55));
}

.portrait__img {
  grid-area: 1 / 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
  /* Under the specular rim (z 2) but over the sheen's origin. */
  z-index: 0;
}

/* Shown only when there is no photograph to show. */
.portrait__fallback {
  grid-area: 1 / 1;
  z-index: 0;
  font-size: clamp(34px, 4.6vw, 62px);
  font-weight: 600;
  letter-spacing: -0.04em;
  color: rgba(11, 11, 12, 0.42);
  display: none;
}
.portrait.is-empty .portrait__img { display: none; }
.portrait.is-empty .portrait__fallback { display: block; }

/* The wet highlight that sells it as glass rather than a cropped circle. */
.portrait::after {
  background:
    radial-gradient(58% 42% at 30% 16%,
      rgba(255, 255, 255, 0.75) 0%,
      rgba(255, 255, 255, 0.18) 46%,
      transparent 72%),
    linear-gradient(100deg,
      transparent 40%,
      rgba(255, 255, 255, 0.28) 46%,
      rgba(255, 255, 255, 0.85) 50%,
      rgba(255, 255, 255, 0.28) 54%,
      transparent 60%);
  background-size: 100% 100%, 260% 100%;
  background-position: 0 0, 140% 0;
  background-repeat: no-repeat;
  opacity: 1;
  z-index: 2;
}
/* Its own sheen keyframe: the shared one moves a single background-position,
   which would drag the specular pool along with the travelling highlight. */
.portrait:hover::after { animation: portrait-sheen 1.5s var(--ease) forwards; }
@keyframes portrait-sheen {
  from { background-position: 0 0, 140% 0; }
  to   { background-position: 0 0, -40% 0; }
}

/* Entrance via translate/scale rather than the .stagger class, whose transform
   would end up overwriting the pointer tilt. */
.js .portrait {
  opacity: 0;
  animation: portrait-in 1.1s var(--ease) 0.34s both;
}
@keyframes portrait-in {
  from { opacity: 0; translate: 0 26px; scale: 0.92; }
  to   { opacity: 1; translate: 0 0; scale: 1; }
}

@media (max-width: 760px) {
  .hero__lead { flex-direction: column-reverse; align-items: flex-start; gap: 22px; }
  .portrait { width: clamp(104px, 30vw, 148px); }
}

/* The lede rides in its own pane of tempered glass. `max-width` is on the
   content box, so the measure has to grow by roughly the padding it just
   gained or the line length changes along with the look. */
.hero__lede {
  max-width: 52ch;
  padding: clamp(18px, 2.1vw, 26px) clamp(22px, 2.5vw, 32px);
  border-radius: clamp(24px, 2.8vw, 34px);
  font-size: clamp(17px, 1.8vw, 22px);
  line-height: 1.5;
  color: var(--on-glass);
  letter-spacing: -0.015em;
  margin-bottom: clamp(30px, 4vw, 44px);
}
/* A paragraph is not a control. The panel keeps its highlight, but it should
   not light up under the pointer the way the buttons beside it do — that is a
   reading surface and the flicker is a distraction while you are on it. */
.hero__lede:hover::after { opacity: 0; }

.hero__actions { display: flex; flex-wrap: wrap; gap: 12px; }

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 14px 28px;
  border-radius: var(--radius-pill);
  font-size: 16px;
  font-weight: 500;
  letter-spacing: -0.01em;
  /* Not background-color: the fill is a stack of gradients now, and that
     property has nothing left to animate. The lift is carried by the shadow
     and the rim instead. */
  transition: transform 0.4s var(--ease), color 0.3s var(--ease),
              box-shadow 0.4s var(--ease), border-color 0.3s var(--ease);
}
/* Smoked glass: black, but still glass. It keeps the polished treatment rather
   than the tempered one — this is the one control on the page that should look
   wet, and the travelling sheen needs a dark body to read against. The tint is
   translucent, so the sunset behind it still comes through the blur; a flat
   #000 fill would be a hole punched in the hero.

   Its neighbour is frosted and pale, so the pair reads as two different sheets
   of the same material rather than as two unrelated buttons. */
.btn--primary {
  color: #fff;
  font-weight: 600;
  background:
    /* Specular pool where the light lands, over a body that deepens toward
       the base — the same construction as the light glass, inverted. */
    radial-gradient(130% 100% at 22% -20%,
      rgba(255, 255, 255, 0.38) 0%,
      rgba(255, 255, 255, 0.1) 38%,
      rgba(255, 255, 255, 0) 66%),
    linear-gradient(180deg,
      rgba(46, 46, 52, 0.86) 0%,
      rgba(18, 18, 21, 0.9) 52%,
      rgba(8, 8, 10, 0.94) 100%);
  -webkit-backdrop-filter: blur(16px) saturate(150%) brightness(0.92);
  backdrop-filter: blur(16px) saturate(150%) brightness(0.92);
  border-color: rgba(255, 255, 255, 0.26);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.34) inset,   /* lit top edge */
    0 -12px 20px -16px rgba(255, 255, 255, 0.22) inset, /* light off the base */
    0 16px 34px -12px rgba(10, 10, 14, 0.5),
    0 3px 10px -5px rgba(10, 10, 14, 0.3);
}
/* The white sweep is built for a pale body; at full strength on black it
   flares. Same movement, less of it. */
.btn--primary::after {
  background: linear-gradient(
    100deg,
    transparent 40%,
    rgba(255, 255, 255, 0.14) 46%,
    rgba(255, 255, 255, 0.42) 50%,
    rgba(255, 255, 255, 0.14) 54%,
    transparent 60%
  );
  background-size: 260% 100%;
  background-position: 140% 0;
}
.btn--primary:hover {
  transform: translateY(-2px) scale(1.02);
  border-color: rgba(255, 255, 255, 0.4);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.4) inset,
    0 -12px 20px -16px rgba(255, 255, 255, 0.26) inset,
    0 24px 44px -14px rgba(10, 10, 14, 0.55),
    0 4px 12px -6px rgba(10, 10, 14, 0.34);
}
.btn--primary:active { transform: translateY(0) scale(0.985); }

.btn--ghost {
  color: var(--ink);
  border-color: rgba(255, 255, 255, 0.6);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.85) inset,
    0 12px 30px -16px rgba(16, 16, 20, 0.14);
}
.btn--ghost:hover {
  transform: translateY(-2px);
  border-color: rgba(255, 255, 255, 0.9);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.95) inset,
    0 18px 36px -16px rgba(16, 16, 20, 0.2);
}

.hero__scroll {
  position: absolute;
  left: 50%;
  bottom: 34px;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  z-index: 2;
}
.hero__scroll-line {
  width: 1px;
  height: 44px;
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0.25), transparent);
  position: relative;
  overflow: hidden;
}
.hero__scroll-line::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--ink);
  animation: scroll-cue 2.4s var(--ease-soft) infinite;
}
@keyframes scroll-cue {
  0%   { transform: translateY(-100%); opacity: 0; }
  40%  { opacity: 1; }
  100% { transform: translateY(100%); opacity: 0; }
}
.hero__scroll-label {
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted-2);
}

/* ---------- Skyline ----------
   Sits behind the glass panes and in front of the pigment. Anchored to the
   bottom of the hero and allowed to run off both edges, so it reads as a city
   continuing past the frame rather than a graphic centred on the page. */
.skyline {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 0;
  height: clamp(150px, 26vh, 280px);
  pointer-events: none;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.skyline svg { width: 100%; height: 100%; display: block; }

/* Floating glass panes (parallax) */
.panes { position: absolute; inset: 0; z-index: 1; pointer-events: none; }
.pane {
  position: absolute;
  display: block;
  border-radius: var(--radius-lg);
  opacity: 0.85;
  will-change: translate;
}
.pane--1 { width: 190px; height: 240px; top: 16%;  right: 7%;  transform: rotate(-8deg); }
.pane--2 { width: 130px; height: 130px; top: 62%;  right: 22%; border-radius: 50%; }
.pane--3 { width: 150px; height: 96px;  top: 74%;  left: 4%;   transform: rotate(6deg); }

/* ---------- Section pane fields ----------
   The same glass, carried past the fold so the page keeps moving in layers
   instead of going flat the moment the hero ends. They sit behind the copy
   and drift at their own rate; the content is raised above them below. */
.panes--section { z-index: 0; }
.section > .wrap { position: relative; z-index: 1; }

/* The handoff fade only knows about .wrap, so the panes have to be told. They
   sit at the edges of the frame, so they get a deeper push than the copy —
   otherwise they are the one thing that still reads as arriving from the side. */
[data-handoff] > .panes {
  opacity: var(--handoff, 1);
  scale: calc(0.9 + 0.1 * var(--handoff, 1));
}

.pane--a { width: 200px; height: 148px; top: 6%;  right: 4%; transform: rotate(-7deg); }
.pane--b { width: 116px; height: 116px; top: 58%; left: 2%;  border-radius: 50%; }
.pane--c { width: 150px; height: 196px; top: 22%; left: 3%;  transform: rotate(5deg); }
.pane--d { width: 132px; height: 132px; top: 70%; right: 5%; border-radius: 50%; }
.pane--e { width: 176px; height: 112px; top: 12%; left: 4%;  transform: rotate(-5deg); }

/* Below this the copy runs edge to edge and the panes would just be clutter
   sitting under the text. */
@media (max-width: 1180px) {
  .panes--section { display: none; }
}

/* =========================================================================
   Sections
   ========================================================================= */
.section {
  position: relative;
  padding: clamp(96px, 14vh, 168px) 0;
}

.kicker {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted-2);
  margin-bottom: 22px;
}

.statement {
  font-size: clamp(30px, 5.2vw, 62px);
  line-height: 1.08;
  font-weight: 600;
  letter-spacing: -0.035em;
  max-width: 19ch;
  margin-bottom: clamp(40px, 6vw, 72px);
  text-wrap: balance;
}
.statement--center {
  max-width: 15ch;
  margin-inline: auto;
  text-align: center;
  margin-bottom: 20px;
}

.heading {
  font-size: clamp(28px, 4vw, 46px);
  line-height: 1.1;
  font-weight: 600;
  letter-spacing: -0.032em;
  margin-bottom: 14px;
}
.heading__sub {
  color: var(--muted);
  font-size: 17px;
  max-width: 52ch;
  margin-bottom: clamp(36px, 5vw, 56px);
}
.heading__sub a {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: rgba(0, 0, 0, 0.25);
  transition: text-decoration-color 0.3s var(--ease);
}
.heading__sub a:hover { text-decoration-color: var(--ink); }

/* ---------- About ---------- */
.about__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
  gap: clamp(32px, 5vw, 72px);
  align-items: start;
}
.about__prose p {
  font-size: clamp(16px, 1.5vw, 19px);
  line-height: 1.62;
  color: var(--muted);
  max-width: 62ch;
}
.about__prose p + p { margin-top: 1.15em; }

.card {
  border-radius: var(--radius-lg);
  padding: 32px 30px;
  position: sticky;
  top: 116px;
}
.card__title {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--muted-2);
  margin-bottom: 22px;
}
.facts { display: grid; gap: 2px; }
.fact {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 18px;
  padding: 13px 0;
  border-top: 1px solid var(--line-soft);
}
.fact:first-child { border-top: 0; padding-top: 0; }
.fact dt { color: var(--muted); font-size: 15px; }
.fact dd { font-weight: 500; font-size: 15px; text-align: right; }

/* ---------- Focus tiles ---------- */
.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 18px;
}
.tile {
  border-radius: var(--radius-lg);
  padding: 30px 28px 32px;
  overflow: hidden;
  transition: transform 0.5s var(--ease), box-shadow 0.5s var(--ease);
}
.tile:hover {
  --lift: -4px;
  box-shadow:
    0 1px 1px rgba(255, 255, 255, 0.9) inset,
    0 22px 46px -18px rgba(16, 16, 20, 0.2);
}
.tile__glyph {
  display: block;
  width: 46px;
  height: 46px;
  border-radius: 15px;
  margin-bottom: 22px;
  background:
    radial-gradient(circle at 32% 30%, rgba(255, 255, 255, 0.95), transparent 60%),
    var(--accent, var(--p-sky));
  box-shadow:
    0 1px 2px rgba(255, 255, 255, 0.9) inset,
    0 8px 18px -8px rgba(0, 0, 0, 0.22);
}
.tile h3 {
  font-size: 19px;
  font-weight: 600;
  letter-spacing: -0.02em;
  margin-bottom: 9px;
}
.tile p { color: var(--muted); font-size: 15.5px; line-height: 1.55; }

/* The wide tile spans the whole grid so five cards never leave an orphan on
   the second row. Side-by-side layout keeps it from reading as a stretched
   version of the others. */
.tile--wide {
  grid-column: 1 / -1;
  display: flex;
  align-items: flex-start;
  gap: 26px;
  padding: 30px 32px 32px;
}
.tile--wide .tile__glyph { flex: 0 0 auto; margin-bottom: 0; }
.tile--wide .tile__body { min-width: 0; }
.tile--wide p { max-width: 74ch; }

@media (max-width: 560px) {
  .tile--wide { flex-direction: column; gap: 0; }
  .tile--wide .tile__glyph { margin-bottom: 22px; }
}

/* ---------- Projects ---------- */
.projects {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 20px;
}
.project__card {
  display: flex;
  flex-direction: column;
  height: 100%;
  border-radius: var(--radius-lg);
  overflow: hidden;
  transition: transform 0.5s var(--ease), box-shadow 0.5s var(--ease);
}
.project__card:hover {
  --lift: -6px;
  box-shadow:
    0 1px 1px rgba(255, 255, 255, 0.9) inset,
    0 30px 60px -22px rgba(16, 16, 20, 0.26);
}
.project__art {
  height: 168px;
  position: relative;
  background: linear-gradient(135deg, var(--a), var(--b));
  overflow: hidden;
}
.project__art::after {
  content: "";
  position: absolute;
  inset: -40%;
  background:
    radial-gradient(circle at 28% 32%, rgba(255, 255, 255, 0.85), transparent 46%),
    radial-gradient(circle at 74% 68%, rgba(255, 255, 255, 0.55), transparent 44%);
  animation: drift-c 26s var(--ease-soft) infinite;
}

/* Logo panel: the KGB mark is white artwork on transparency, so it needs an
   ink backdrop to exist at all. A faint pastel glow keeps it in the family. */
.project__art--logo {
  display: grid;
  place-items: center;
  padding: 26px 30px;
  /* Same two knobs as the gradient variant above, so a logo card is set the
     same way as any other — `style="--a: #1b2430; --b: #0a0e14"`. The defaults
     are the near-black this was built with, so a card that sets nothing looks
     exactly as it did. Keep both dark for white-on-transparent artwork; the
     pastel glows above are mixed for an ink base and a pale one would leave
     the mark with nothing to sit against. */
  background:
    radial-gradient(120% 90% at 22% 18%, rgba(224, 214, 255, 0.30), transparent 60%),
    radial-gradient(110% 90% at 82% 88%, rgba(255, 214, 226, 0.26), transparent 58%),
    linear-gradient(150deg, var(--a, #17171b), var(--b, #0b0b0c) 62%);
}
.project__art--logo img {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 230px;
  height: auto;
  max-height: 100%;
  object-fit: contain;
  transition: transform 0.6s var(--ease);
}
/* The white sheen would wash the logo out — keep it to a whisper here. */
.project__art--logo::after { opacity: 0.16; }

.project__card:hover .project__art--logo img { transform: scale(1.04); }
.project__body {
  padding: 26px 26px 28px;
  display: flex;
  flex-direction: column;
  flex: 1;
}
.project__meta {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--muted-2);
  margin-bottom: 10px;
}
.project__title {
  font-size: 22px;
  font-weight: 600;
  letter-spacing: -0.025em;
  margin-bottom: 10px;
}
.project__desc {
  color: var(--muted);
  font-size: 15.5px;
  line-height: 1.55;
  margin-bottom: 18px;
}
.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
  margin-bottom: 22px;
}
.tags li {
  padding: 5px 11px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.5);
  font-size: 12.5px;
  color: var(--muted);
}
.project__go {
  margin-top: auto;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 15px;
  font-weight: 500;
}
.arrow { transition: transform 0.4s var(--ease); }
.project__card:hover .arrow,
.channel:hover .arrow { transform: translateX(5px); }

/* ---------- Contact ---------- */
.contact { padding-bottom: clamp(110px, 16vh, 190px); }
.contact .kicker { text-align: center; }
.contact__lede {
  text-align: center;
  color: var(--muted);
  font-size: 18px;
  max-width: 42ch;
  margin: 0 auto clamp(44px, 6vw, 64px);
}
.channels {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
  gap: 16px;
  max-width: 960px;
  margin-inline: auto;
}
.channel {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 22px 24px;
  border-radius: var(--radius);
  transition: transform 0.5s var(--ease), box-shadow 0.5s var(--ease);
}
.channel:hover {
  --lift: -4px;
  box-shadow:
    0 1px 1px rgba(255, 255, 255, 0.9) inset,
    0 24px 48px -20px rgba(16, 16, 20, 0.24);
}
.channel__icon {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border-radius: 14px;
  color: var(--ink-soft);
  background:
    radial-gradient(circle at 32% 28%, rgba(255, 255, 255, 0.95), transparent 62%),
    var(--accent, var(--p-sky));
  box-shadow:
    0 1px 2px rgba(255, 255, 255, 0.9) inset,
    0 8px 18px -9px rgba(0, 0, 0, 0.24);
}
.channel__icon svg { width: 21px; height: 21px; }
.channel__text { display: flex; flex-direction: column; min-width: 0; }
.channel__text strong { font-size: 15.5px; font-weight: 600; letter-spacing: -0.015em; }
.channel__text span {
  font-size: 13.5px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.channel .arrow { margin-left: auto; color: var(--muted-2); }

/* ---------- Footer ---------- */
.footer {
  border-top: 1px solid var(--line-soft);
  padding: 34px 0;
  background: rgba(255, 255, 255, 0.6);
  -webkit-backdrop-filter: blur(18px);
  backdrop-filter: blur(18px);
}
.footer__inner {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  font-size: 13.5px;
  color: var(--muted-2);
}

/* =========================================================================
   Motion: entrance + reveal
   ========================================================================= */
.js .stagger {
  opacity: 0;
  transform: translateY(22px);
  animation: rise 1s var(--ease) both;
}
.js .hero__eyebrow.stagger  { animation-delay: 0.10s; }
.js .line:nth-child(1) .stagger { animation-delay: 0.18s; }
.js .line:nth-child(2) .stagger { animation-delay: 0.26s; }
.js .hero__lede.stagger     { animation-delay: 0.40s; }
.js .hero__actions.stagger  { animation-delay: 0.50s; }
.js .hero__scroll.stagger   { animation-delay: 0.85s; }

.js .hero__title .stagger {
  transform: translateY(105%);
  animation-name: rise-mask;
}

@keyframes rise {
  to { opacity: 1; transform: none; }
}
@keyframes rise-mask {
  from { opacity: 0; transform: translateY(105%); }
  to   { opacity: 1; transform: none; }
}

/* Reveals arrive out of the middle of themselves rather than sliding up into
   place: they start slightly small and settle, which reads as approaching the
   viewer head-on. It is the same language as the hero — a camera coming
   forward — and nothing on the page enters from an edge.

   Using `scale` rather than `transform` keeps it independent of the pointer
   tilt (`transform`) and the parallax offset (`translate`), so all three can
   sit on one element without any of them clobbering the others. */
.js .reveal {
  opacity: 0;
  scale: 0.94;
  transition:
    opacity 0.9s var(--ease),
    scale 0.9s var(--ease);
  transition-delay: var(--reveal-delay, 0s);
}
.js .reveal.is-in { opacity: 1; scale: 1; }

/* Scroll-linked parallax offsets (written by JS) */
[data-parallax] { translate: 0 calc(var(--py, 0) * 1px); }

/* Sections dim as they slide off the top of the viewport and come back as you
   scroll up. JS writes --fade every frame, so there is deliberately no
   transition here — the value is already tied to scroll position. */
[data-fade] { opacity: var(--fade, 1); }

/* Pointer tilt on glass. --lift replaces a hover translateY so the lift and
   the tilt compose in one transform instead of clobbering one another. */
[data-tilt] {
  transform:
    perspective(1000px)
    rotateX(var(--rx, 0deg))
    rotateY(var(--ry, 0deg))
    translateY(var(--lift, 0px));
  /* Resting state eases back slowly once the pointer leaves... */
  transition: transform 0.6s var(--ease), box-shadow 0.5s var(--ease);
}
/* ...but tracks the pointer tightly while it is over the card. */
[data-tilt].is-tilting {
  transition: transform 0.14s var(--ease-soft), box-shadow 0.5s var(--ease);
}

/* =========================================================================
   Responsive
   ========================================================================= */
@media (max-width: 900px) {
  .about__grid { grid-template-columns: 1fr; }
  .card { position: static; }
  .nav__list { display: none; }
  .nav { justify-content: space-between; }
  .pane--1 { width: 130px; height: 165px; top: 12%; right: 5%; }
  .pane--2 { width: 90px; height: 90px; top: 68%; right: 12%; }
  .pane--3 { display: none; }
}

@media (max-width: 560px) {
  html { scroll-padding-top: 92px; }
  body { font-size: 16px; }
  .hero { padding-top: 118px; }
  .nav { padding: 9px 10px 9px 16px; }
  .nav__cta { padding: 8px 15px; font-size: 14px; }
  .btn { width: 100%; }
  .hero__actions { flex-direction: column; align-items: stretch; }
  .blob { filter: blur(56px) saturate(115%); }
  .channel { padding: 18px 18px; }
}

/* Very narrow phones: the pill has to stay a pill, so the CTA gets shorter. */
@media (max-width: 400px) {
  :root { --gutter: 16px; }
  .nav { gap: 10px; padding: 8px 8px 8px 14px; }
  .nav__mark { font-size: 14px; }
  .nav__cta { padding: 8px 13px; font-size: 13.5px; }
  .nav__cta-long { display: none; }
  .nav__cta-short { display: inline; }
}

/* =========================================================================
   Reduced compositor load
   Set on <html> by the page itself, not by a media query. Two class families
   from two independent judges: `perf-low` from main.js's frame-pacing
   watchdog, which sees the whole frame; `gpu-<tier>` from the hero, which
   times its own GPU work (`gpu-min` is its last rung and gets the same
   treatment as `gpu-low`). They are kept separate so neither can overwrite
   the other's verdict on a reload.

   What is expensive here is almost entirely WebKit's backdrop-filter, whose
   cost rises with the blur radius and with how many of them overlap a region
   that is repainting — and during the hero, that region is a WebGL canvas
   redrawing every frame. So: radii come down, the decorative pane fields go
   (eight blurred panes drifting over the page, purely ornamental), and the
   watercolour drops its blend mode. The design survives all of it; what goes
   is the last few percent of gloss, on the machines that were paying the
   most for it.
   ========================================================================= */
.perf-low .glass,
.gpu-low .glass,
.gpu-min .glass,
.gpu-mobile .glass {
  -webkit-backdrop-filter: blur(8px) saturate(180%) brightness(1.05);
  backdrop-filter: blur(8px) saturate(180%) brightness(1.05);
}
.perf-low .glass--tempered,
.gpu-low .glass--tempered,
.gpu-min .glass--tempered,
.gpu-mobile .glass--tempered {
  -webkit-backdrop-filter: blur(14px) saturate(140%) brightness(1.04);
  backdrop-filter: blur(14px) saturate(140%) brightness(1.04);
}
.perf-low .btn--primary,
.gpu-low .btn--primary,
.gpu-min .btn--primary,
.gpu-mobile .btn--primary {
  -webkit-backdrop-filter: blur(10px) saturate(140%) brightness(0.92);
  backdrop-filter: blur(10px) saturate(140%) brightness(0.92);
}
.perf-low .footer,
.gpu-low .footer,
.gpu-min .footer,
.gpu-mobile .footer {
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}

/* The pane fields are the single largest pile of backdrop blur on the page
   and they are decoration. Below 1180px they are already gone. */
.perf-low .panes--section,
.gpu-low .panes--section,
.gpu-min .panes--section { display: none; }

/* Multiply is a per-frame compositor cost on every blob; plain alpha reads
   nearly the same at these opacities and costs nothing. */
.perf-low .blob,
.gpu-low .blob,
.gpu-min .blob,
.gpu-mobile .blob { mix-blend-mode: normal; opacity: 0.6; }

/* The hover sheen re-rasterises a large gradient for its whole duration. */
.perf-low .glass:hover::after,
.gpu-low .glass:hover::after,
.gpu-min .glass:hover::after { animation: none; }

/* =========================================================================
   Reduced motion — everything still arrives, nothing travels.
   ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

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

  .stagger { opacity: 1 !important; transform: none !important; }
  .reveal  { opacity: 1 !important; translate: none !important; scale: none !important; }
  [data-handoff] > .wrap,
  [data-handoff] > .panes { scale: none !important; }

  .js .header[data-intro="pending"] .nav,
  .js .header[data-intro="pending"] .header__progress { opacity: 1 !important; }

  /* No droplet, and nothing left clipped behind it. */
  .header__drop,
  .header__ripple { display: none !important; }
  .header .nav { clip-path: none !important; }
  .js .portrait { opacity: 1 !important; }

  .blob { animation: none !important; }
  .watercolor,
  [data-parallax] { translate: none !important; }

  /* The curtain still appears and still says its piece — it just does not
     travel to get there, and JS shortens every hold to match. */
  .boot__welcome { translate: none !important; }
  [data-fade] { opacity: 1 !important; }
  [data-tilt] { transform: none !important; }
}
