/* ---------------------------------------------------------------
   Sylia Adèle Lakrib — /test2
   Blue-sky canvas, light streaks, glossy script "hello",
   oversized grotesque headline, mono UI.
   --------------------------------------------------------------- */

:root {
  /* --- the journey --------------------------------------------------
     --t is the scroll position: 0 at the top of the page, 1 at the
     bottom, written by script.js and by nothing else. The palette it
     drives lives in sky.css, on .sky, which owns the sky's own colours;
     these three are only the body backdrop behind that fixed layer. */
  --t: 0;

  --sky-1: #b9d8f2;
  --sky-2: #cfe4f7;
  --sky-3: #a9cdec;

  /* Four stops, because a sphere reads from its limb rather than its
     middle: a white-hot core, a body, then a warmer and slightly deeper
     edge right at the rim. --sun-rays is a triplet so the halo and the
     rays can pick their own alphas from it. */
  /* Cream, not orange. In the reference the disc is almost white-buttery
     and only turns golden in the last sliver — the sky carries the warmth,
     the sun carries the light. Going orange here made it darker than its
     own sky, which is the one thing a star cannot be. */
  --sun-hot: #fdf9de;
  --sun-core: #f9efb8;
  --sun-edge: #f5de96;
  --sun-limb: #eec777;
  --sun-rays: 250, 214, 140;
  /* How far the centre lift pushes toward white. Needed here because the
     two cream stops are only a few values apart, so mixing between them
     alone moved nothing visible. */
  --sun-lift: 0.55;

  --warm: 255, 246, 224;

  /* Off-black, and slightly warm: pure #000 on a warm sky reads as a
     screen colour, where a warm near-black reads as ink on paper. Held as
     a triplet so the three levels share one value — the two faded ones
     were literal rgba(0,0,0,…), so they would have stayed cold while the
     solid text warmed up. */
  --ink-rgb: 34, 28, 24;
  --ink: rgb(var(--ink-rgb));
  --ink-2: rgba(var(--ink-rgb), .62);
  --ink-3: rgba(var(--ink-rgb), .34);

  /* Ink for the type that sits ON the sun. The disc is light in BOTH
     themes, so it needs one dark colour of its own rather than following
     the theme — off-white on a pale sun is what became unreadable. A dark
     blue rather than the off-black: it belongs to the sky the page has
     just travelled through. Only the footer's lower block uses it; the
     headline above the sun keeps the theme's own ink. */
  --ink-sun-rgb: 28, 44, 68;
  --line: rgba(0, 0, 0, .13);
  --panel: rgba(255, 255, 255, .28);
  --pill: rgba(255, 255, 255, .15);      /* nav hover fill */
  --pill-line: rgba(0, 0, 0, .18);
  --pill-shadow: rgba(18, 64, 158, .22); /* saturated blue cast under the pill */

  --sans: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: "JetBrains Mono", ui-monospace, Consolas, monospace;
  --script: "Pacifico", cursive;

  --pad: clamp(1.15rem, 3.4vw, 3.25rem);
  --ease: cubic-bezier(.22, 1, .36, 1);

  /* --- column grid -------------------------------------------------
     The four vertical rules and two horizontal ones, as % of the
     viewport. script.js / work.js read these back rather than carrying
     their own copy, so the rules and the text aligned to them can
     never drift apart. Sections span --c1 → --c4; --gutter is what
     every text block adds to clear its own rule. */
  /* Outer edge is a flat 60px rather than a % of the viewport. The two
     inner rules keep the 30.5 : 31.5 : 31 rhythm, now measured across the
     span between those edges — and since the fr ratios in .hero-cols,
     .cols and .top are those same numbers, the columns follow on their
     own. --edge is the only value to touch. */
  --edge: 20px;
  --c1: var(--edge);
  --c2: calc(var(--edge) + (100% - 2 * var(--edge)) * 30.5 / 93);
  --c3: calc(var(--edge) + (100% - 2 * var(--edge)) * 62 / 93);
  --c4: calc(100% - var(--edge));
  --r1: 33%;
  --r2: 68%;
  --gutter: 20px;

}

:root[data-theme="dark"] {
  --sky-1: #0e2136;
  --sky-2: #14304d;
  --sky-3: #0a1929;

  /* a rising sun, not a setting one. Light in both themes, which is why
     the type that sits on it has its own ink — see --ink-sun-rgb. */
  --sun-hot: #fff8fa;
  --sun-core: #ffe2ea;
  --sun-edge: #e5aac2;
  --sun-limb: #bd82a4;
  --sun-rays: 244, 196, 218;
  --sun-lift: 0.4;

  --warm: 120, 165, 225;

  /* the same idea inverted: warm off-white rather than pure #fff */
  --ink-rgb: 244, 240, 232;
  --ink: rgb(var(--ink-rgb));
  --ink-2: rgba(var(--ink-rgb), .66);
  --ink-3: rgba(var(--ink-rgb), .36);
  --line: rgba(255, 255, 255, .14);
  --panel: rgba(255, 255, 255, .06);
  --pill: rgba(255, 255, 255, .15);
  --pill-line: rgba(255, 255, 255, .26);
  --pill-shadow: rgba(10, 44, 120, .38);
}

*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--sky-1);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

a { color: inherit; text-decoration: none; }
h1, h2, h3, p, ul { margin: 0; }
ul { list-style: none; padding: 0; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }

.mono {
  font-family: var(--mono);
  font-size: .78rem;
  letter-spacing: .02em;
  line-height: 1.5;
}

.dim { color: var(--ink-3); }
.br { opacity: .45; }

::selection { background: #c0fe04; color: #000; }

/* Sky background lives in sky.css / sky.js */

/* --- top bar ---------------------------------------------------- */

/* --- burger (hidden above the mobile breakpoint) ------------------ */

.burger {
  display: none;
  position: relative;
  width: 34px;
  height: 22px;
  padding: 0;
  flex: 0 0 auto;
}
.burger span {
  position: absolute;
  left: 6px;
  right: 6px;
  height: 1.5px;
  background: currentColor;
  transition: transform .4s var(--ease), opacity .25s var(--ease);
}
.burger span:nth-child(1) { top: 7px; }
.burger span:nth-child(2) { bottom: 7px; }

.menu-open .burger span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.menu-open .burger span:nth-child(2) { transform: translateY(-6.5px) rotate(-45deg); }

/* Two tracks split on rule 3 (62 = --c3 - --c1, 31 = --c4 - --c3), so the
   nav starts on the same verticalcolumn 3 does — i.e. WORK sits directly
   above the bio paragraph. gap must be 0 or the split misses the rule. */
.top {
  position: relative;
  z-index: 5;
  display: grid;
  grid-template-columns: minmax(0, 62fr) minmax(0, 31fr);
  align-items: center;
  gap: 0;
  padding: 1.15rem calc(100% - var(--c4)) 1.15rem var(--c1);
}

.wordmark {
  padding-left: var(--gutter);
  font-family: var(--mono);
  font-size: .84rem;
  font-weight: 500;
  letter-spacing: .04em;
}

/* space-between rather than a fixed gap: the band is fixed by the rules,
   so distributing the slack is what keeps the five labels evenly spaced
   at any width. The gap is only a floor. */
.top-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* no floor: the band between rules 3 and 4 is tight, and any minimum
     here would push the last label past the rule instead of just closing
     the gaps up */
  gap: 0;
  padding-left: var(--gutter);
  padding-right: var(--gutter);
  font-family: var(--mono);
  font-size: .78rem;
  letter-spacing: .04em;
}
.top-nav > * { white-space: nowrap; }
/* Sound on: a small equaliser next to the label so the state reads at a
   glance, not just from the word inside the brackets. */
#sound.is-on::after {
  content: "";
  display: inline-block;
  width: 3px;
  height: 7px;
  margin-left: .45rem;
  vertical-align: baseline;
  background: currentColor;
  box-shadow: 5px 0 0 currentColor, 10px 0 0 currentColor;
  animation: eq 1.1s ease-in-out infinite alternate;
}

@keyframes eq {
  from { height: 4px; box-shadow: 5px 0 0 currentColor, 10px 0 0 currentColor; opacity: .55; }
  to   { height: 9px; box-shadow: 5px 0 0 currentColor, 10px 0 0 currentColor; opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  #sound.is-on::after { animation: none; }
}

/* Hover draws a thin oval around the label. It lives on a pseudo-element
   with negative insets, so nothing in the bar shifts when it appears. */
.top-nav a,
.top-nav button {
  position: relative;
  /* own stacking context, so the filled pill can sit behind the label */
  isolation: isolate;
  transition: opacity .25s, transform .4s var(--ease);
}

/* the label lifts a couple of px, carrying the oval with it */
.top-nav a:hover,
.top-nav button:hover,
.top-nav a:focus-visible,
.top-nav button:focus-visible {
  transform: translateY(-2px);
}

.top-nav a::before,
.top-nav button::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: -.55em -1.05em;
  border: 1px solid transparent;
  border-radius: 999px;
  background: transparent;
  opacity: 0;
  transform: scale(.86);
  box-shadow: 0 0 0 rgba(20, 45, 80, 0);
  transition:
    opacity .3s var(--ease),
    transform .45s var(--ease),
    background .3s var(--ease),
    box-shadow .4s var(--ease);
  pointer-events: none;
}

.top-nav a:hover::before,
.top-nav button:hover::before,
.top-nav a:focus-visible::before,
.top-nav button:focus-visible::before {
  opacity: 1;
  transform: scale(1);
  border-color: var(--pill-line);
  /* light frosted fill, so the pill reads brighter than the sky */
  background: var(--pill);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  /* soft cast underneath, so the lift reads as height */
  box-shadow: 0 5px 14px -4px var(--pill-shadow);
}

/* --- hero ------------------------------------------------------- */

.hero {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  min-height: calc(100svh - 4.5rem);
  /* content box runs rule 1 → rule 4 */
  padding: clamp(1.5rem, 4vh, 3rem) calc(100% - var(--c4)) clamp(4rem, 9vh, 6rem) var(--c1);
}

/* Column widths are the gaps between the rules — 30.5 : 31.5 : 31 —
   so with no gap the internal edges land exactly on --c2 and --c3, and
   each child's own padding is what holds it off its rule. */
.hero-cols {
  position: relative;
  z-index: 3;
  display: grid;
  /* minmax(0, …): without it an fr track has an auto minimum, so a text
     block wider than its column would push the track and drag the rules
     with it. This keeps the columns fixed and lets the text overhang. */
  grid-template-columns: minmax(0, 30.5fr) minmax(0, 31.5fr) minmax(0, 31fr);
  gap: 0;
  align-items: start;
}

/* every column clears the rule on its left, and stops short of the next */
.hero-cols > * { padding-left: var(--gutter); padding-right: var(--gutter); }

.discipline {
  font-size: clamp(1.4rem, 2.5vw, 2.15rem);
  font-weight: 700;
  line-height: 1.08;
  letter-spacing: -.025em;
}

.statement { color: var(--ink); }

/* The bio's line count is deliberate: its 156 characters wrap to four
   lines at 40 per line and to three at 58.
   `width` rather than `max-width`, because at narrower viewports column 3
   is the tighter of the two and a cap would just let the column win — the
   paragraph is allowed to overhang instead, since the columns must not move.
   The unit is not plain `ch`: that is the width of the "0" glyph and
   ignores letter-spacing, so a 40ch box only holds ~38 characters. Adding
   the .02em that .mono tracks makes it one real character wide.
   box-sizing is border-box project-wide, so the two gutters have to be
   added back or they eat into the character count — and the text would
   stop well short of the next rule. */
.bio { color: var(--ink); width: calc(40 * (1ch + .02em) + 2 * var(--gutter)); }

/* 58 characters per line fits three lines, and still lands inside
   column 3 at this viewport */
@media (min-width: 1700px) {
  .bio { width: calc(58 * (1ch + .02em) + 2 * var(--gutter)); }
}

/* narrower than this the overhang would cross the last rule, so the
   paragraph goes back to simply filling its column */
@media (max-width: 1023px) {
  .bio { width: auto; }
}

.mega {
  position: relative;
  z-index: 3;
  padding-left: var(--gutter);
  font-size: min(75px, 11vw);
  font-weight: 800;
  line-height: .93;
  letter-spacing: -.035em;
  text-transform: uppercase;
}

/* --- glossy script "hello" -------------------------------------- */

/* Authored at a fixed 960x420 so the SVG filter and the clip-path share
   one coordinate space; --k (set in script.js from the word's measured
   width) scales it to span ~50% of the viewport. Centring is done with
   margins so `transform` stays free for the scale. */
.hello {
  position: absolute;
  left: 50%;
  top: calc(39% + 50px);
  width: 960px;
  height: 420px;
  /* Centred with `translate` rather than negative margins: margins are
     tied to the fixed 960x420 box, this stays true whatever the size,
     and it composes cleanly with the scale below. */
  translate: -50% -50%;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  transform: scale(var(--k, 1));
  transform-origin: 50% 50%;
  perspective: 1600px;
}

.hello.in { animation: helloIn 1.8s var(--ease) forwards; }

/* JS writes the cursor parallax + perspective shift here */
.hello-tilt {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  will-change: transform;
}

/* Idle float, independent of the cursor.
   Opacity lives here rather than on .hello, whose own opacity is driven
   by the entrance animation. */
.hello-drift {
  position: absolute;
  inset: 0;
  animation: helloDrift 19s ease-in-out infinite;
  transition: opacity .6s var(--ease);
}

/* Softer against a night sky, so the stars stay readable through it */
:root[data-theme="dark"] .hello-drift { opacity: .45; }

/* Real refraction of the sky behind, clipped to the letterforms.
   Low blur on purpose — clear glass distorts, frosted glass fogs. */
.hello-refract {
  position: absolute;
  inset: 0;
  clip-path: url(#helloClipCore);
  -webkit-backdrop-filter: blur(3px) saturate(1.4) brightness(1.03);
  backdrop-filter: blur(3px) saturate(1.4) brightness(1.03);
}

/* PERFORMANCE — the word costs nothing once it is off screen.

   This pass is the most expensive thing on the page, and none of that is
   the filter: it is the pairing of backdrop-filter with clip-path:url().
   An SVG clip is not a compositor clip, so the blur cannot be handed to
   the GPU and runs on the main thread. Worse, it sits inside .hello-drift,
   whose 19s animation never ends — the sampled region therefore moves on
   every frame, so the backdrop was re-blurred and re-clipped sixty times
   a second for as long as the tab was open, cursor or no cursor, scroll
   or no scroll.

   Nothing here changes what the word looks like. The hero is a single
   screen at the top of the page; for the whole of the rest of the scroll
   the glass is out of view and its cost was being paid for nothing.
   script.js puts .hello--rest on once the hero leaves the viewport and
   takes it off when it comes back.

   The drift is paused rather than removed: animation-play-state resumes
   from where it stopped, so scrolling back up finds the word mid-float
   exactly where it would have been, with no jump. */
.hello--rest .hello-refract { display: none; }
.hello--rest .hello-drift { animation-play-state: paused; }
.hello--rest .hello-sheen i { animation-play-state: paused; }

/* And the glass itself. On desktop the live 24-primitive chain stays
   mounted — the bake is not faithful enough to stand in for it, see
   hello-raster.js — so it is the most expensive thing on the page, and
   .hello-drift kept it re-rasterising on the CPU while the visitor was
   pages below reading the work list.

   Taking it out of the render tree while the word is off screen changes
   nothing anyone can see: .hello--rest is only set once no part of the
   hero is in view. The hero still pays full price while you are looking
   at it, which is the honest place for that cost to sit. */
.hello--rest .hello-svg { display: none; }

/* The slow colour drift across the letters.

   This used to be two SMIL <animate> tags sliding #hGrad — the gradient
   that FILLS the text, i.e. the filter's own input. Moving it meant the
   browser re-ran all 24 primitives every frame, on the CPU, forever.

   Same five stops and the same direction, but painted on top of the
   finished artwork instead of feeding it. Nothing below is invalidated,
   so the whole effect costs one composited transform per frame.

   The travel lives on the inner <i> and the clip on the wrapper: that
   way the clip-path is applied to the layer once, and only a transform
   changes. Animating the gradient's own background-position instead
   would repaint — and re-clip — the element on every frame. */
.hello-sheen {
  position: absolute;
  inset: 0;
  clip-path: url(#helloClipCore);
  overflow: hidden;
  /* Modulates the light already in the glass rather than covering it,
     so the specular highlights and the contour survive underneath. */
  mix-blend-mode: soft-light;
  /* Calibrated against the artwork, not picked by eye: at .7 the blend
     turned the clear glass into blue plastic and buried the specular
     highlights. The body of the glass is only 20% alpha to begin with
     (feComponentTransfer, slope .2), so the gradient it used to slide
     was always a faint modulation — this matches that register. It is
     meant to be almost invisible in a still and legible only as slow
     movement. */
  opacity: .18;
  pointer-events: none;
}

.hello-sheen i {
  position: absolute;
  /* wider than the box on both sides, so the travel never walks an edge
     of the gradient into view */
  inset: 0 -45%;
  display: block;
  background: linear-gradient(
    105deg,
    #9fc9f2 0%,
    #5f9ade 22%,
    #8dbdee 44%,
    #4f88d6 66%,
    #87b8ec 88%,
    #9fc9f2 100%
  );
  /* 16s, matching the cycle the SMIL pair used to run */
  animation: hello-sheen 16s ease-in-out infinite alternate;
  will-change: transform;
}

@keyframes hello-sheen {
  from { transform: translate3d(-14%, 0, 0); }
  to   { transform: translate3d(14%, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .hello-sheen i { animation: none; }
}

/* The hue shift lives inside the #hWarp filter, not here: a CSS
   hue-rotate cannot be confined to the cursor. See index.html. */
.hello-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* Le rendu WebGL du verre. Il se superpose exactement à la boîte du
   mot — pas au viewport : c'était le premier des deux leviers de
   performance, un prototype plein écran mesurant 22 fps là où la boîte
   du mot ne pèse qu'une fraction des pixels du héros.

   Le shader fait discard hors des lettres, donc le ciel CSS et les
   nuages continuent de passer au travers partout ailleurs. */
.hello-gl {
  position: absolute;
  /* PAD_X / PAD_Y de hello-glass.js, en négatif. Le canvas déborde la
     boîte du mot parce que le SVG qu'il remplace portait overflow:visible :
     à ras, les lettres et le halo se faisaient couper. Ces quatre valeurs
     et les deux du module sont un seul jeu — changer l'un sans l'autre
     décale le mot dans sa boîte. */
  left: -25%;
  top: -45%;
  width: 150%;
  height: 190%;
  pointer-events: none;
}

/* Quand le verre GPU a pris la main, tout le chemin SVG s'efface :
   l'artwork filtré, la réfraction en backdrop-filter et le sheen. C'est
   exactement ce que remplace le canvas, et les trois réunis étaient le
   coût dominant de la page. La classe n'est posée qu'après un montage
   réussi, donc un navigateur sans WebGL2 garde le rendu SVG intact. */
.hello--gl .hello-svg,
.hello--gl .hello-raster,
.hello--gl .hello-refract,
.hello--gl .hello-sheen { display: none; }

/* the baked bitmap that replaces it on phones */
.hello-raster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.hello-type {
  font-family: var(--script);
  font-size: 300px;
}

@keyframes helloIn {
  0%   { opacity: 0; scale: .88; }
  60%  { opacity: 1; }
  100% { opacity: 1; scale: 1; }
}

/* A few degrees of drift at rest — never more */
@keyframes helloDrift {
  0%, 100% { transform: translate3d(0, 0, 0) rotate(-2.6deg); }
  50%      { transform: translate3d(0, -24px, 0) rotate(-0.6deg); }
}

/* The sparkle trail under the cursor is deliberately styled from JS with
   inline styles and animated through the Web Animations API — see
   sparkTrail() in script.js. Nothing for it here on purpose: sky.css
   already owns .star / .stars, and a stylesheet-driven version kept being
   silently overridden. */

/* --- status bar ------------------------------------------------- */

.status {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  padding: .85rem calc(100% - var(--c4) + var(--gutter)) .85rem calc(var(--c1) + var(--gutter));
  font-size: .72rem;
  letter-spacing: .05em;
  pointer-events: none;
}

/* The middle item is centred by giving the two ends equal weight, not by
   space-between: that equalises the GAPS, so the moment the two ends differ
   in width — the clock against a 16px globe — the middle sits off centre by
   half the difference. Also degrades correctly to two items, which is what
   the work pages have once #coords is hidden. */
.status > :first-child,
.status > :last-child { flex: 1 1 0; }
.status > :last-child { text-align: right; justify-content: flex-end; }
.globe { display: flex; opacity: .7; }

/* --- content blocks --------------------------------------------- */

main { position: relative; z-index: 2; }

/* No max-width: these have to share the hero's edges, and the hero is
   flush to the rules, which are viewport-relative. Capping the width
   here is what used to pull every section inboard of the headline on
   wide screens. */
.block {
  padding: clamp(3.5rem, 9vw, 7rem) calc(100% - var(--c4)) clamp(3.5rem, 9vw, 7rem) var(--c1);
}

.block-head {
  display: flex;
  justify-content: space-between;
  padding: 0 var(--gutter) .8rem;
  border-bottom: 1px solid var(--line);
  letter-spacing: .12em;
}

/* work list */
.row { border-bottom: 1px solid var(--line); }

.row-btn {
  display: grid;
  grid-template-columns: 3.5rem minmax(0, 1fr) 8rem 2rem;
  align-items: center;
  gap: 1rem;
  width: 100%;
  padding: 1.4rem var(--gutter);
  text-align: left;
  transition: background .3s var(--ease), padding-left .3s var(--ease);
}
/* hover still nudges the row right, now measured from the gutter */
.row-btn:hover { background: var(--panel); padding-left: calc(var(--gutter) + .6rem); }

.row-i, .row-meta { color: var(--ink-2); letter-spacing: .08em; }
.row-meta { text-align: right; }

.work-all {
  display: inline-block;
  margin-top: 2rem;
  margin-left: var(--gutter);
  padding-bottom: 3px;
  border-bottom: 1px solid var(--line);
  letter-spacing: .1em;
  font-size: .76rem;
  transition: border-color .25s, transform .35s var(--ease);
}
.work-all:hover { border-color: var(--ink); transform: translateX(3px); }

.row-title {
  font-size: clamp(1.5rem, 3.4vw, 2.6rem);
  font-weight: 700;
  letter-spacing: -.03em;
  line-height: 1.05;
}

.row-mark {
  font-size: 1.15rem;
  color: var(--ink-2);
  justify-self: end;
}

.chips { display: flex; flex-wrap: wrap; gap: .5rem; }
.chips li {
  padding: .3rem .7rem;
  border: 1px solid var(--line);
  border-radius: 100px;
  font-size: .68rem;
  letter-spacing: .08em;
  color: var(--ink-2);
}

/* expertise — same three columns as the hero, on the same rules */
.cols {
  display: grid;
  grid-template-columns: minmax(0, 30.5fr) minmax(0, 31.5fr) minmax(0, 31fr);
  gap: 0;
  padding-top: 2.5rem;
}
.cols > * { padding-left: var(--gutter); padding-right: var(--gutter); }
.col h3 { font-size: 1.5rem; font-weight: 700; letter-spacing: -.02em; margin-bottom: 1rem; }
.col ul li {
  padding: .55rem 0;
  border-top: 1px solid var(--line);
  font-size: .95rem;
  color: var(--ink-2);
  transition: color .25s, padding-left .25s var(--ease);
}
.col ul li:hover { color: var(--ink); padding-left: .4rem; }

/* about */
.about { max-width: 64ch; padding-top: 2.5rem; padding-left: var(--gutter); }
.about .big {
  font-size: clamp(1.4rem, 2.9vw, 2.1rem);
  font-weight: 700;
  letter-spacing: -.025em;
  line-height: 1.18;
  margin-bottom: 1.3rem;
}
.about p:not(.big) { color: var(--ink-2); }
.chips.wide { margin-top: 2rem; }

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

.foot {
  /* named because the sun cancels it out to reach the true bottom edge —
     absolute offsets resolve against the padding box, not the border box */
  --foot-pb: 5.5rem;

  position: relative;
  z-index: 2;
  padding: clamp(4rem, 10vw, 8rem) calc(100% - var(--c4)) var(--foot-pb) var(--c1);
  border-top: 1px solid var(--line);
}
.mega.small { font-size: clamp(1.9rem, 6vw, 5.4rem); }

/* --- contact finale --------------------------------------------- */
/* Only the home page gets the full-height treatment; work pages keep
   the plain .foot with just a colophon. */

.foot--big {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 100svh;
  overflow: hidden;
  border-top: 0;
}

/* The sky opens up behind the section as it comes into view */

/* Centred for the finale, so the headline sits square on the sun. Revert
   to text-align: left here (and drop the justify-content below) to put the
   block back on the page's left edge. */
.foot-inner {
  position: relative;
  z-index: 1;
  padding: 0 var(--gutter);
  text-align: center;
}
.foot-links { justify-content: center; }

/* The three blocks that land on the disc. Redeclaring the ink scale HERE
   rather than swapping --ink-sun-rgb higher up is deliberate: --ink and
   friends are declared on :root, so they are already resolved by the time
   they inherit — overriding the triplet alone would change nothing. The
   faded levels are also raised a little, because a dark blue at 34% on a
   pale disc was legible in theory only. */
.foot-mail,
.foot-links,
.foot--big .colophon {
  --ink: rgb(var(--ink-sun-rgb));
  --ink-2: rgba(var(--ink-sun-rgb), .74);
  --ink-3: rgba(var(--ink-sun-rgb), .52);

  color: var(--ink);
}

/* --- the sun ----------------------------------------------------- */
/* A dome rather than a disc: a 2:1 box with only its top corners rounded
   is already a half circle, so nothing needs clipping.

   Anchored to the very bottom of the section — the negative offset undoes
   the footer's bottom padding — so the horizon is the end of the page and
   the headline crosses the sun's upper half. Everything that spills past
   the section is caught by .foot--big's overflow: hidden, which is why the
   halo and the rays need no clip of their own.

   z-index 0 keeps it below .foot-inner and the colophon (both 1), so the
   type always stays on top.

   A white .foot-glow used to sit behind this — the old "the sky opens up"
   effect from before there was a sun. It was a 140vmax radial centred at
   42% of the section, so it was still around 30% opaque where the
   section's overflow: hidden cut it off, and that drew a hard horizontal
   line across the page. Redundant next to a real sun, so it is gone. */
/* A TRUE CIRCLE, not a squashed dome. The previous version was a 2:1 box
   with rounded top corners — geometrically a half ellipse, which is
   exactly why it read as drawn rather than photographed. This is a full
   circle whose centre sits below the end of the page, so what shows above
   the edge is a real spherical cap: wide, shallow, and correct.

   --sun-d is the diameter and --sun-cap is how much of it clears the page
   edge. Those two alone set the curvature: a big diameter with a small cap
   gives the shallow arc of something enormous and far away. */
.sun {
  --sun-d: 170vw;
  /* A RATIO of the diameter, not a pixel count. Held at a flat 340px it
     broke on a phone: at 375px wide the diameter is 637px, so the radius
     is 318 — and a 340px "cap" is larger than the radius it is supposed to
     be a slice of. The result was more than half the disc, a 637px ball
     over half the screen. 0.185 is the desktop proportion, so nothing
     changes there. */
  --sun-cap: calc(var(--sun-d) * 0.185);

  position: absolute;
  z-index: 0;
  left: 50%;
  /* drop the whole disc below the page, then lift the cap back up */
  bottom: calc(-1 * var(--foot-pb) - (var(--sun-d) - var(--sun-cap)));
  width: var(--sun-d);
  height: var(--sun-d);
  pointer-events: none;

  opacity: 0;
  translate: -50% 70px;
  transition:
    opacity 1.6s var(--ease) .2s,
    translate 1.8s var(--ease) .2s;
}
.foot--big.in .sun { opacity: 1; translate: -50% 0; }

/* The 3D sun. Fills the section; the disc's size and position are computed
   in sun3d.js from the same numbers the CSS version uses, so the two land
   in the same place and the swap is invisible.

   .gl is only added once WebGL has actually reported success, which is why
   the flat sun is hidden by that class rather than by the mere presence of
   the script — a failed import must not leave the section empty. */
.sun-gl {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1.4s var(--ease);
}
.foot--big.gl .sun-gl { opacity: 1; }
.foot--big.gl .sun { display: none; }

/* The shaded disc itself — see the defs in index.html. Sits above the
   corona (which is a pseudo-element of the wrapper) so the corona never
   lightens the body. */
.sun-disc {
  position: relative;
  z-index: 1;
  display: block;
  width: 100%;
  height: 100%;
  /* the haze that a low sun is seen through: just enough to take the
     vector crispness off the limb */
  filter: blur(.7px);
}

/* Corona. Concentric with the disc, so it is physically where it belongs —
   most of it falls below the page and only the part above the arc shows.
   It starts at the limb (1 / 1.3 of its own radius) so it never lightens
   the disc itself. */
.sun::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--sun-d) * 1.3);
  height: calc(var(--sun-d) * 1.3);
  translate: -50% -50%;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(
    circle at 50% 50%,
    rgba(var(--sun-rays), 0) 76%,
    rgba(var(--sun-rays), .34) 79%,
    rgba(var(--sun-rays), .12) 88%,
    transparent 100%
  );
}

/* Lateral flare along the top of the arc — light spreading sideways, the
   way it does when the sun is low. Replaces the spoked rays, which were
   the least realistic part of the old version. */
.sun::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  width: 150%;
  height: calc(var(--sun-cap) * 2.4);
  translate: -50% -50%;
  pointer-events: none;
  background: radial-gradient(
    ellipse 50% 50% at 50% 50%,
    rgba(var(--sun-rays), .42) 0%,
    rgba(var(--sun-rays), .16) 42%,
    transparent 76%
  );
}

.foot-label {
  display: inline-flex;
  align-items: center;
  gap: .6rem;
  font-size: .7rem;
  letter-spacing: .16em;
  color: var(--ink-2);
  opacity: 0;
  translate: 0 14px;
  transition: opacity .8s var(--ease), translate .8s var(--ease);
}
.foot--big.in .foot-label { opacity: 1; translate: 0 0; }

.foot-label .dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #3f9d5c;
  box-shadow: 0 0 0 0 rgba(63, 157, 92, .55);
  animation: availPulse 2.6s ease-out infinite;
}

@keyframes availPulse {
  0%   { box-shadow: 0 0 0 0 rgba(63, 157, 92, .5); }
  70%  { box-shadow: 0 0 0 9px rgba(63, 157, 92, 0); }
  100% { box-shadow: 0 0 0 0 rgba(63, 157, 92, 0); }
}

/* Headline — each line rises out of its own mask */
.foot-title {
  margin: 1.6rem 0 clamp(2rem, 5vw, 3.2rem);
  font-size: clamp(2.5rem, 10.5vw, 10rem);
  font-weight: 900;
  line-height: .9;
  letter-spacing: -.045em;
  text-transform: uppercase;
}

.fl { display: block; overflow: hidden; padding-bottom: .04em; }
.fl > span {
  display: block;
  translate: 0 105%;
  transition: translate 1.15s var(--ease);
  transition-delay: calc(var(--i) * .13s + .1s);
}
.foot--big.in .fl > span { translate: 0 0; }

/* Email — a glass plate, echoing the nav pills and the hello */
.foot-mail {
  display: inline-flex;
  align-items: center;
  gap: clamp(1rem, 3vw, 2.5rem);
  padding: clamp(1rem, 2.2vw, 1.5rem) clamp(1.3rem, 3vw, 2.2rem);
  border: 1px solid var(--pill-line);
  border-radius: 999px;
  background: var(--pill);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  box-shadow: 0 10px 30px -12px var(--pill-shadow);
  opacity: 0;
  translate: 0 22px;
  transition:
    opacity .9s var(--ease) .5s,
    translate .9s var(--ease) .5s,
    transform .45s var(--ease),
    box-shadow .45s var(--ease),
    background .35s var(--ease);
}
.foot--big.in .foot-mail { opacity: 1; translate: 0 0; }

.foot-mail:hover {
  transform: translateY(-4px);
  box-shadow: 0 18px 42px -14px var(--pill-shadow);
}

.foot-mail-label { font-size: .66rem; letter-spacing: .16em; color: var(--ink-2); }
.foot-mail-addr {
  font-size: clamp(.85rem, 1.9vw, 1.25rem);
  letter-spacing: .01em;
}
.foot-mail-arrow {
  font-size: 1.2rem;
  transition: transform .45s var(--ease);
}
.foot-mail:hover .foot-mail-arrow { transform: translateX(6px); }

.foot-links {
  margin-top: clamp(2rem, 4vw, 3rem);
  opacity: 0;
  translate: 0 16px;
  transition: opacity .9s var(--ease) .68s, translate .9s var(--ease) .68s;
}
.foot--big.in .foot-links { opacity: 1; translate: 0 0; }

.foot--big .colophon {
  position: relative;
  z-index: 1;
  margin-top: auto;
  padding-top: clamp(3rem, 8vw, 5rem);
}

@media (max-width: 700px) {
  .foot-mail { flex-wrap: wrap; gap: .5rem 1rem; border-radius: 28px; }
}

/* --- short viewports --------------------------------------------- */
/* The finale is the last thing on the page, so anything taller than the
   window is not merely tight — it is unreachable. Scrolled to the end you
   see the LAST screenful, and the headline's first line sits above it.
   On a 15" laptop the block runs to roughly 1060px against ~890 of height.

   Keyed on height, not width, because that is the actual constraint. The
   rhythm tightens instead of the type shrinking: ~230px comes back from
   spacing alone, which is enough, and the headline keeps its scale. */
@media (max-height: 920px) {
  .foot--big {
    --foot-pb: 3rem;
    padding-top: clamp(2rem, 5vh, 4rem);
  }
  /* A height-based ceiling on top of the usual width-based scale. min()
     takes whichever is smaller, so this only bites on a short window —
     the very case where the headline runs out of room — and leaves taller
     screens at exactly the size they had. */
  .foot--big .foot-title {
    font-size: min(clamp(2.5rem, 10.5vw, 10rem), 15vh);
    margin: 1rem 0 1.6rem;
  }
  .foot--big .foot-links { margin-top: 1.4rem; }
  .foot--big .colophon { padding-top: 2rem; }
}

@media (prefers-reduced-motion: reduce) {
  /* .foot-glow removed: see the note where the sun is defined */
  .foot-label,
  .fl > span,
  .foot-mail,
  .foot-links {
    opacity: 1;
    translate: 0 0;
    scale: 1;
    transition: none;
  }
  .foot-label .dot { animation: none; }
  /* its own rule: `translate: 0 0` would undo the -50% that centres it */
  .sun { opacity: 1; translate: -50% 0; transition: none; }
}

.mail {
  display: inline-block;
  margin-top: 2rem;
  font-size: .85rem;
  letter-spacing: .06em;
  border-bottom: 1px solid var(--line);
  padding-bottom: 3px;
  transition: border-color .25s;
}
.mail:hover { border-color: var(--ink); }

.links { display: flex; flex-wrap: wrap; gap: 1.5rem; margin-top: 2.5rem; letter-spacing: .06em; }
.links a { color: var(--ink-2); transition: color .25s; }
.links a:hover { color: var(--ink); }
.colophon { margin-top: 2.5rem; padding-left: var(--gutter); letter-spacing: .06em; }

/* --- custom cursor ---------------------------------------------- */

.cursor {
  position: fixed;
  top: 0; left: 0;
  z-index: 100;
  pointer-events: none;
  opacity: 0;
  transition: opacity .3s, scale .2s var(--ease);
  filter: drop-shadow(0 4px 8px rgba(30, 70, 140, .35));
}
.cursor.on { opacity: 1; }
.cursor.tap { scale: .82; }
body.custom-cursor, body.custom-cursor * { cursor: none; }

/* --- reveal ----------------------------------------------------- */

.rv { opacity: 0; transform: translateY(18px); transition: opacity .7s var(--ease), transform .7s var(--ease); }
.rv.in { opacity: 1; transform: none; }

/* --- responsive ------------------------------------------------- */

@media (max-width: 900px) {
  .hero-cols { grid-template-columns: 1fr; gap: 1.25rem; }
  /* below the 3-column layout there are no rules to align to, so the
     expertise columns go back to flowing on their own */
  .cols { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: clamp(2rem, 5vw, 4rem); }

  /* single column: the nav has no rule 3 to align to, so the bar goes
     back to wordmark-left / nav-right */
  .top {
    display: flex;
    justify-content: space-between;
    padding-left: calc(var(--c1) + var(--gutter));
    padding-right: calc(100% - var(--c4) + var(--gutter));
  }
  .wordmark, .top-nav { padding-left: 0; padding-right: 0; }
  .top-nav { justify-content: flex-end; }
  /* Phones only: the word sits right at the top, above the discipline
     heading, and its strokes are thicker. CSS overrides the SVG
     presentation attribute, so desktop keeps stroke-width="10". */
  .hello { top: 22%; }
  #helloWord { stroke-width: 18; }
  /* backdrop-filter over a large area is the expensive part — drop the
     refraction pass on small screens and keep the lit glass */
  .hello-refract { display: none; }
  /* phones never had the sliding gradient in the first place — a blend
     mode over a clip-path is not the place to start */
  .hello-sheen { display: none; }
  /* fewer compositor layers = less memory pressure on mobile GPUs */
  .cloud, .cloud-i, .sky-layer { will-change: auto; }
  .top-nav { gap: 1rem; font-size: .7rem; }
  .status { font-size: .64rem; }
  #coords { display: none; }

  .row-btn {
    grid-template-columns: 2.5rem minmax(0, 1fr) 1.5rem;
    grid-template-areas: "i title mark" ". meta .";
    row-gap: .35rem;
    padding: 1.1rem var(--gutter);
  }
  .row-i { grid-area: i; align-self: start; padding-top: .45rem; }
  .row-title { grid-area: title; }
  .row-mark { grid-area: mark; align-self: start; }
  .row-meta { grid-area: meta; text-align: left; }
  .cursor { display: none; }
}

/* --- phones ------------------------------------------------------ */
/* Five nav items never fit beside the wordmark, so below this width
   they collapse behind a burger and drop down as a glass panel. */
@media (max-width: 640px) {
  /* A phone has no room to hold text 20px off a rule that is itself
     only ~13px from the edge: sections go back to --pad and the inner
     gutters collapse, which keeps the tuned mobile layout untouched. */
  /* 60px each side is a third of a phone's width, so the edge falls back
     to --pad — which also keeps the first rule on the text edge. */
  :root { --gutter: 0px; --edge: var(--pad); }
  .hero { padding-left: var(--pad); padding-right: var(--pad); }
  .foot { padding-left: var(--pad); padding-right: var(--pad); }

  .top { padding: .9rem var(--pad); }
  .wordmark { font-size: .78rem; }
  .burger { display: block; }

  .top-nav {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: .35rem var(--pad) 1rem;
    background: color-mix(in srgb, var(--sky-1) 72%, transparent);
    -webkit-backdrop-filter: blur(16px);
    backdrop-filter: blur(16px);
    border-bottom: 1px solid var(--line);
    font-size: .78rem;
    opacity: 0;
    translate: 0 -8px;
    pointer-events: none;
    transition: opacity .3s var(--ease), translate .35s var(--ease);
  }
  .menu-open .top-nav {
    opacity: 1;
    translate: 0 0;
    pointer-events: auto;
  }

  .top-nav > * {
    padding: .85rem .2rem;
    border-top: 1px solid var(--line);
    text-align: left;
  }
  /* the sliding oval makes no sense on stacked full-width rows */
  .top-nav a::before, .top-nav button::before { content: none; }
  .top-nav a:hover, .top-nav button:hover { transform: none; }

  .hero { min-height: calc(100svh - 5rem); }
  .discipline { font-size: 1.5rem; }

  .block { padding: clamp(2.5rem, 9vw, 4rem) var(--pad); }
  .cols { padding-top: 1.75rem; }
  .about { padding-top: 1.75rem; }

  .foot-mail { width: 100%; justify-content: space-between; }
  .foot-links { gap: 1rem; }

  /* No full-height finale on a phone — it left a screen of dead space
     under the links. The section is now only as tall as its content. */
  .foot--big { min-height: 0; padding-top: clamp(3rem, 12vw, 4.5rem); }
  .foot { --foot-pb: 2rem; }
  .foot--big .colophon { margin-top: 2.5rem; padding-top: 1.5rem; }
  .foot-title { margin-bottom: 1.75rem; }

  /* The fixed status bar has no background, so on a phone it just
     printed the clock on top of whatever was scrolling past. There's
     no room for it here and the colophon already carries the credit. */
  .status { display: none; }

  /* tighter menu rows — the panel was eating most of the screen */
  .top-nav { padding: .25rem var(--pad) .75rem; }
  .top-nav > * { padding: .7rem .2rem; }
}

/* Very narrow: the display sizes need their floors lowered or the
   longest words run past the edge. */
@media (max-width: 400px) {
  .mega { font-size: 9.5vw; }
  .foot-title { font-size: 9.2vw; }
  .project-title { font-size: 8.5vw; }
  .work-title { font-size: 8.5vw; }
  .row-title { font-size: 1.3rem; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  .rv { opacity: 1; transform: none; }
  .hello { opacity: 1; }
}

/* ---------------------------------------------------------------
   Touch devices keep :hover stuck after a tap, so every hover effect
   is gated behind an actual hover capability. Same approach as the
   reference site, which pairs `@media (hover: hover)` with each one.
   --------------------------------------------------------------- */

@media (hover: none) {
  .top-nav a:hover,
  .top-nav button:hover { transform: none; }
  .top-nav a:hover::before,
  .top-nav button:hover::before { opacity: 0; }

  .row-btn:hover { background: none; padding-left: var(--gutter); }
  .col ul li:hover { color: var(--ink-2); padding-left: 0; }
  .links a:hover { color: var(--ink-2); }
  .mail:hover { border-color: var(--line); }
  .work-all:hover { transform: none; border-color: var(--line); }

  .foot-mail:hover { transform: none; box-shadow: 0 10px 30px -12px var(--pill-shadow); }
  .foot-mail:hover .foot-mail-arrow { transform: none; }

  .card:hover .card-media img { transform: none; }
  .back:hover { transform: none; }
  .project-nav a:hover { color: var(--ink-2); }
  .pill:hover { transform: none; }
}
