/* ═══════════════════════════════════════════════════════════════════
   ██  SITE STYLESHEET — the whole design system
   ═══════════════════════════════════════════════════════════════════
   Loaded by index.html and by every generated case study in projects/,
   so the two share one set of tokens, one type scale, one nav and one
   atmosphere. A rule that only matches home-page markup simply does not
   apply on a case study; nothing here is home-specific by accident.

   Case-study-only rules live in assets/project.css, which loads after
   this file and overrides where a reading page needs to differ.
   ═══════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════
   DESIGN TOKENS — change these to restyle the whole site
   ═══════════════════════════════════════════════════════════ */
:root{
  /* ── PALETTE ── swap these 10 values to retheme the entire site ── */
  --ground:#0c1017;          /* page background — deep ink, cool bias */
  --ground-rgb:12,16,23;     /* same colour as channels, for translucent panels */
  --surface:#171f2c;         /* cards, nav, panels — kept well clear of the ground
                                so cards read as objects rather than merging into it */
  --surface-2:#212b3b;       /* insets, thumbnails, bar tracks */
  --line:#2e3a4d;            /* default borders */
  --line-bright:#425268;     /* emphasised borders */

  --ink:#e7ebf2;             /* primary text */
  --ink-2:#9aa7ba;           /* body text */
  --ink-3:#7c8899;           /* labels, captions */

  --amber:#f2a63b;           /* PRIMARY ACCENT — glows, bars, highlights */
  --amber-rgb:242,166,59;    /* same colour as channels, for every glow */
  --amber-hi:#ffc978;        /* lighter accent, hover states */
  --amber-lo:#b87721;        /* darker accent, gradients */
  --ice:#7fd1e8;             /* SECONDARY ACCENT — links, tech tags */
  --ice-rgb:127,209,232;
  --ice-lo:#4a9ab5;
  --violet:#5b6ee8;          /* third aurora blob only */

  --ok:#5fc98f;              /* status dots */
  --ok-rgb:95,201,143;
  --warn:#f2a63b;

  /* ── EFFECT TOKENS ── how the atmosphere behaves on this ground ── */
  --on-accent:#0a0d12;       /* text sitting on top of an accent fill */
  --cast:0,0,0;              /* shadow colour. Neutral black on the dark ground;
                                light mode warms it to brown, because a grey
                                shadow on warm paper is the single fastest way
                                to make a light theme look cheap. */
  --sheen:#ffffff;           /* bright end of gradients + sweeps */
  --sheen-rgb:255,255,255;   /* the light that sweeps across cards + buttons */
  --sheen-a:.34;
  --sheen-a2:.14;
  --btn-sheen-a:.4;
  --glare-a:.38;             /* cursor-following highlight on cards */
  --aurora-blend:screen;     /* screen glows on dark, multiply stains on light */
  --aurora-a:.5;
  /* normal, not screen. Screen made the cursor glow on the dark ground, but
     it also made it vanish over anything bright — and the site is now full of
     light-mode screenshots: project covers, the case-study gallery, the cards
     under More projects. A solid amber cursor reads on every one of them. */
  --cursor-blend:normal;
  --grain-a:.045;
  --scan-a:.1;               /* CRT scanline darkness */
  --vig-a:.2;                /* screen-edge vignette — heavy values crush the corners
                                and make the whole page read darker than it is */
  --vig2-a:.5;
  --shadow-a:.45;
  --shadow-a2:.55;
  --boot-edge:#000;
  --boot-scan-a:.3;
  --boot-vig-a:.92;
  --particles-a:.9;              /* drifting particle field behind everything */
  /* The field gets its own three colours rather than borrowing --amber /
     --ice / --ink-2. Those are tuned for text contrast, and what reads well
     as a letterform at 16px reads as grit at 2px. Splitting them means the
     field can be retuned without touching a single word on the page.
     On the dark ground the particles are emitted light, so the accents
     themselves are right. */
  --particle-1:#f2a63b;          /* 30% of the field — warm */
  --particle-2:#7fd1e8;          /* 30% — cool */
  --particle-3:#9aa7ba;          /* 40% — neutral, sits back */

  --display:'Chakra Petch',system-ui,sans-serif;
  --body:'IBM Plex Sans',system-ui,sans-serif;
  --mono:'IBM Plex Mono',ui-monospace,monospace;

  --gut:clamp(1.1rem,4vw,2.5rem);
  --maxw:74rem;

  color-scheme:dark;
}

/* ═══════════════════════════════════════════════════════════
   LIGHT THEME — pressed paper, bronze ink.

   Dark mode gets its drama from EMITTED light: glows, halos, a
   screen-blended aurora. None of that survives on a pale ground —
   an additive glow over near-white is arithmetically invisible, so
   simply reusing the dark effects at lower alpha gives you a theme
   with no signature of its own.

   So light mode swaps the vocabulary rather than the volume. Light
   is replaced by INK and CAST SHADOW: a warm paper ground with a
   blueprint lattice, aurora as a multiply watercolour stain, glows
   rewritten as tinted elevation, and a hard offset plate behind the
   portrait — the risograph move. Same site, different medium.
   ═══════════════════════════════════════════════════════════ */
:root[data-theme="light"]{
  --ground:#f6f1e8; --ground-rgb:246,241,232;
  --surface:#fffdf9; --surface-2:#efe6d8;
  --line:#ded1bd; --line-bright:#b6a288;
  --ink:#15110d; --ink-2:#4b4237; --ink-3:#7b6f61;
  --amber:#9c5605; --amber-rgb:156,86,5; --amber-hi:#c2740f; --amber-lo:#6e3c02;
  --ice:#0f6076; --ice-rgb:15,96,118; --ice-lo:#0a4353; --violet:#4b39bd;
  --ok:#1d7049; --ok-rgb:29,112,73; --warn:#9c5605;
  --on-accent:#fffaf3;
  --cast:74,48,14;           /* warm brown — everything below casts in this */
  --sheen:#e0a54a; --sheen-rgb:214,158,80;
  --sheen-a:.34; --sheen-a2:.12; --btn-sheen-a:.42; --glare-a:.16;
  --aurora-blend:multiply; --aurora-a:.24; --cursor-blend:normal;
  --grain-a:.035; --scan-a:.045; --vig-a:.07; --vig2-a:.12;
  --shadow-a:.18; --shadow-a2:.16;
  --boot-edge:#e6d9c6; --boot-scan-a:.06; --boot-vig-a:.2;
  --particles-a:.72;
  /* On paper the particles are pigment, not light, so they must stay darker
     than the ground to exist at all. The accent colours at full strength
     read as specks of dirt at this size, but desaturating them all the way
     to a whisper loses the field entirely — so these sit deliberately
     between the two: enough pigment to register, not so much contrast that
     the page looks dusty. The neutral carries 40% of the field, so it is
     held back further than the two accents. */
  /* The neutral is 40% of the field — twelve hundred colourless dots — and
     it is what makes the page look speckled rather than atmospheric. Pushed
     most of the way back to the paper it drops out, leaving the two accents
     to carry the field: roughly forty percent fewer specks competing with
     the text, at the same strength of colour. Dim the whole field instead
     and you are back to something too faint to be worth having. */
  --particle-1:#a05f0c;
  --particle-2:#3a7889;
  --particle-3:#c9bda9;

  color-scheme:light;
}

/* ═══════════════════════════════════════════════════════════
   LIGHT THEME — STRUCTURE
   Everything below only exists on the pale ground. Each rule
   replaces a dark-mode glow with the light-mode equivalent:
   ink, tint, cast shadow, or a printed edge.
   ═══════════════════════════════════════════════════════════ */

/* ── The ground is not flat white. A warm top light, a bronze stain in
      the lower corners, and a faint blueprint lattice — so the page has
      atmosphere in the gutters the way the dark ground has aurora. ── */
/* These six layers used to sit on `body` with background-attachment:fixed.
   That pins them to the viewport, which means the browser cannot simply
   translate the scrolled layer — it has to repaint the whole viewport's
   background on every scroll frame, and six layers of lattice and gradient
   is a lot to repaint sixty times a second.

   Painting them onto a fixed pseudo-element instead gets the identical
   picture from a layer the compositor rasterises once and then leaves
   alone. Same ground, none of the per-frame cost.

   z-index:-1 keeps it behind the content but still above the body's own
   background, which is where a negative-index child paints. */
:root[data-theme="light"] body{background:var(--ground)}
:root[data-theme="light"] body::before{
  content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;
  transform:translateZ(0);              /* force its own composited layer */
  background:
    linear-gradient(rgba(var(--amber-rgb),.055) 1px,transparent 1px) 0 0/40px 40px,
    linear-gradient(90deg,rgba(var(--amber-rgb),.055) 1px,transparent 1px) 0 0/40px 40px,
    radial-gradient(120% 85% at 50% -15%,#fffefa 0%,rgba(255,254,250,0) 58%),
    radial-gradient(80% 60% at 100% 105%,rgba(var(--amber-rgb),.09) 0%,transparent 62%),
    radial-gradient(70% 55% at -5% 100%,rgba(var(--ice-rgb),.07) 0%,transparent 60%),
    linear-gradient(180deg,#fdfaf4 0%,var(--ground) 42%,#f0e6d6 100%);
}
/* Bigger, softer blobs: a multiply stain wants more spread than a screen
   glow to read as watercolour rather than as a smudge.

   The blur radius is held at the dark theme's 95px rather than the 120px
   this started with. Three 60vw circles blurred and then scaled by a running
   animation is the single most expensive thing on the page — the scale means
   the blurred result cannot be rasterised once and reused — and the light
   theme was paying 26% more radius than the dark theme for it. Dark, at
   95px, scrolls smoothly; light, at 120px, did not. The spread that makes
   this read as watercolour comes mostly from the 60vw size below, which is
   untouched. */
:root[data-theme="light"] #aurora i{filter:blur(95px)}
/* Still larger than the dark theme's 48/44/38 — the multiply stain does want
   more spread than a screen glow — but pulled in from 60/54/48. Blur cost
   goes with area, so trimming the radius by an eighth takes about a quarter
   of the work out, and the wash still reaches well past the gutters. */
:root[data-theme="light"] #aurora .a1{width:52vw;height:52vw}
:root[data-theme="light"] #aurora .a2{width:48vw;height:48vw}
:root[data-theme="light"] #aurora .a3{width:42vw;height:42vw}

/* ── Elevation. On paper, depth is the whole game: everything that was a
      glow becomes a two-part cast — a tight contact shadow plus a long
      diffuse one, both warm. ── */
:root[data-theme="light"] .card,
:root[data-theme="light"] .panel,
:root[data-theme="light"] .ch,
:root[data-theme="light"] .btn{
  box-shadow:0 1px 1px rgba(var(--cast),.05),0 8px 20px -12px rgba(var(--cast),.28);
}
:root[data-theme="light"] header.nav.stuck{
  border-bottom-color:var(--line);
  box-shadow:0 1px 0 rgba(var(--cast),.04),0 14px 34px -22px rgba(var(--cast),.45);
}

/* Hover lifts the card off the page instead of lighting it up: it rises,
   the shadow lengthens and warms toward the accent, and the border inks in. */
:root[data-theme="light"] .card:hover{
  border-color:var(--amber);
  box-shadow:0 2px 4px rgba(var(--cast),.07),
             0 26px 50px -20px rgba(var(--amber-rgb),.45),
             0 10px 20px -14px rgba(var(--cast),.3);
}
:root[data-theme="light"] .btn:hover{
  box-shadow:0 2px 4px rgba(var(--cast),.08),0 14px 26px -14px rgba(var(--amber-rgb),.5);
}
:root[data-theme="light"] .btn.primary{
  box-shadow:0 2px 3px rgba(var(--cast),.12),0 12px 24px -12px rgba(var(--amber-rgb),.55);
}
:root[data-theme="light"] .btn.primary:hover{
  box-shadow:0 3px 5px rgba(var(--cast),.14),0 18px 32px -12px rgba(var(--amber-rgb),.7);
}
:root[data-theme="light"] .ch:hover{
  border-color:var(--amber);
  box-shadow:0 2px 4px rgba(var(--cast),.07),0 18px 32px -18px rgba(var(--amber-rgb),.5);
}
:root[data-theme="light"] .chip:hover{
  box-shadow:0 6px 14px -8px rgba(var(--amber-rgb),.55);
}
/* The CTA pulse can't glow, so it breathes as a shadow instead. */
:root[data-theme="light"] .btn.pulse{animation:cta-pulse-lite 2.6s ease-in-out infinite}
@keyframes cta-pulse-lite{
  0%,100%{box-shadow:0 2px 3px rgba(var(--cast),.12),0 10px 20px -12px rgba(var(--amber-rgb),.45)}
  50%    {box-shadow:0 3px 6px rgba(var(--cast),.16),0 20px 36px -12px rgba(var(--amber-rgb),.8)}
}

/* The cursor highlight becomes a warm stain the pointer drags across the
   card — a bright specular spot would be invisible on a near-white surface. */
:root[data-theme="light"] .card-glare{
  mix-blend-mode:multiply;
  background:radial-gradient(circle at var(--gx,50%) var(--gy,50%),
    rgba(var(--amber-rgb),var(--glare-a)),rgba(var(--amber-rgb),0) 55%);
}

/* ── Portrait: a hard offset plate in accent ink behind the notched frame,
      the way a two-colour print misregisters on purpose. Hover pushes the
      plate further out instead of raising the glow. ── */
:root[data-theme="light"] .portrait{
  filter:drop-shadow(7px 7px 0 rgba(var(--amber-rgb),.24))
         drop-shadow(0 18px 26px rgba(var(--cast),.22));
}
:root[data-theme="light"] .portrait-wrap:hover .portrait{
  filter:drop-shadow(12px 12px 0 rgba(var(--amber-rgb),.45))
         drop-shadow(0 26px 40px rgba(var(--cast),.3));
}
/* The fade under the photo is shared now — see .portrait-inner::after. It
   already warms itself through --cast, so there is nothing to override here. */
:root[data-theme="light"] .portrait-wrap:hover .portrait-inner{
  box-shadow:0 0 0 1px rgba(var(--amber-rgb),.4),0 20px 40px -20px rgba(var(--cast),.35);
}
:root[data-theme="light"] .portrait-halo{opacity:.5;filter:blur(22px) saturate(1.3)}
:root[data-theme="light"] .portrait-wrap:hover .portrait-halo{opacity:.85}
/* Orbits read as drafting guides rather than light rings. */
:root[data-theme="light"] .orbit-1{opacity:.75;border-color:var(--line-bright)}
:root[data-theme="light"] .orbit-2{border-color:rgba(var(--amber-rgb),.35)}
:root[data-theme="light"] .orbit-2::after{box-shadow:0 0 0 4px rgba(var(--amber-rgb),.18)}
:root[data-theme="light"] .portrait-wrap:hover .orbit-2{
  box-shadow:0 0 0 1px rgba(var(--amber-rgb),.12);
}

/* ── Type. The name's glow becomes a printed echo behind the letterforms. ── */
:root[data-theme="light"] .hero h1{text-shadow:3px 3px 0 rgba(var(--amber-rgb),.13)}
:root[data-theme="light"] .caret{box-shadow:2px 2px 0 rgba(var(--amber-rgb),.25)}
/* The hero floor grid is the blueprint's vanishing point — worth more here
   than in the dark, where the aurora already carries the background. */
:root[data-theme="light"] .hero-grid{opacity:.5;
  background-image:linear-gradient(rgba(var(--amber-rgb),.4) 1px,transparent 1px),
                   linear-gradient(90deg,rgba(var(--amber-rgb),.4) 1px,transparent 1px)}

/* Section rules get a printed accent tick at the head of the hairline. */
:root[data-theme="light"] .sec-head{
  border-bottom-width:2px;border-bottom-style:solid;
  border-image:linear-gradient(90deg,var(--amber) 0 4rem,var(--line) 4rem 100%) 1;
}

/* ── Small emitters: every `0 0 Npx` glow below becomes a contact ring,
      which is what a dot actually does against paper. ── */
:root[data-theme="light"] .status .dot,
:root[data-theme="light"] .card-bar .live{box-shadow:0 0 0 3px rgba(var(--ok-rgb),.22)}
:root[data-theme="light"] .tl-item::before{
  box-shadow:0 0 0 3px var(--ground),0 0 0 5px rgba(var(--amber-rgb),.28);
}
:root[data-theme="light"] .cur-dot{box-shadow:0 0 0 3px rgba(var(--amber-rgb),.22)}
:root[data-theme="light"] #progress{box-shadow:0 1px 4px rgba(var(--amber-rgb),.5)}
:root[data-theme="light"] .boot-logo-img{box-shadow:0 10px 30px -12px rgba(var(--cast),.5)}
:root[data-theme="light"] .boot-mark{text-shadow:2px 2px 0 rgba(var(--amber-rgb),.16)}
:root[data-theme="light"] .boot-fill{box-shadow:0 1px 6px rgba(var(--amber-rgb),.55)}
/* Bar tracks are inset on paper, not lit from behind. */
:root[data-theme="light"] .bar{box-shadow:inset 0 1px 2px rgba(var(--cast),.12)}
:root[data-theme="light"] .bar i{box-shadow:0 1px 3px rgba(var(--amber-rgb),.35)}
:root[data-theme="light"] .logo:hover .logo-mark{
  filter:drop-shadow(0 6px 12px rgba(var(--amber-rgb),.6));
}

/* The name's flowing gradient runs to white in the dark, which is maximum
   contrast. The same gradient on paper runs to pale gold — decorative, but
   it is the man's name, so light mode keeps every stop in the dark half of
   the bronze range and gets its movement from hue instead of brightness. */
:root[data-theme="light"] .name-flow{
  background-image:linear-gradient(100deg,var(--amber-lo) 0%,var(--amber-hi) 28%,
    var(--amber-lo) 52%,var(--amber) 78%,var(--amber-hi) 100%);
}

*,*::before,*::after{box-sizing:border-box}
/* The whole scale hangs off this one number. Every size in the sheet is a
   rem or a clamp() whose ends are rems, so 112.5% = 18px lifts the type,
   the spacing and the component sizing together instead of leaving the
   headings behind the body text — card padding, button height, chip size
   and the 20rem grid column all follow. Tuned on Windows at 100% display
   scaling, where a 16px base reads noticeably smaller than it does on a
   Mac. --maxw is deliberately left in rem so the column widens with it and
   the line length stays where it was. */
html{scroll-behavior:smooth;font-size:112.5%}
body{
  margin:0;background:var(--ground);color:var(--ink);
  font-family:var(--body);font-size:1rem;line-height:1.65;
  overflow-x:hidden;-webkit-font-smoothing:antialiased;
}
h1,h2,h3,.display{font-family:var(--display);letter-spacing:-.01em;text-wrap:balance}
a{color:var(--ice);text-decoration:none}
::selection{background:var(--amber);color:var(--on-accent)}

::-webkit-scrollbar{width:9px}
::-webkit-scrollbar-track{background:var(--ground)}
::-webkit-scrollbar-thumb{background:var(--surface-2);border:1px solid var(--amber-lo);border-radius:4px}
::-webkit-scrollbar-thumb:hover{background:var(--amber-lo)}

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

/* ═══════════════ BOOT SCREEN ═══════════════ */
/* Set by the head script when this visit has already seen the intro. Hiding
   it in CSS rather than waiting for JS is what stops it flashing for a frame
   on the way back from a project page. */
html.booted #boot{display:none}
#boot{
  position:fixed;inset:0;z-index:100000;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:0;
  background:radial-gradient(ellipse at 50% 42%,var(--surface-2) 0%,var(--ground) 58%,var(--boot-edge) 100%);
  font-family:var(--display);color:var(--amber);overflow:hidden;
  transition:opacity .9s ease,visibility .9s ease;
}
#boot.hidden{opacity:0;visibility:hidden;pointer-events:none}
#boot::before{content:"";position:absolute;inset:0;pointer-events:none;mix-blend-mode:overlay;
  background:repeating-linear-gradient(0deg,rgba(0,0,0,0) 0 2px,rgba(0,0,0,var(--boot-scan-a)) 2px 4px);
  animation:flicker .15s infinite}
#boot::after{content:"";position:absolute;inset:0;pointer-events:none;box-shadow:inset 0 0 14rem rgba(var(--cast),var(--boot-vig-a))}
/* Logo beside the name, not stacked above it.
   Sized in rem rather than px so the intro follows the same root scale as
   the rest of the site — it was the one screen left behind when the base
   moved off 16px, and a small mark on a full-bleed black field reads as an
   accident rather than a title card. */
.boot-mark{display:flex;flex-direction:row;align-items:center;gap:clamp(.8rem,2.6vw,1.5rem);
  font-size:clamp(1.75rem,6.4vw,4.2rem);font-weight:700;letter-spacing:.06em;line-height:1;
  text-shadow:0 0 18px rgba(var(--amber-rgb),.7),0 0 46px rgba(var(--amber-rgb),.3);animation:boot-flicker 2.4s infinite}
.boot-word b{color:var(--ink);font-weight:700}
.boot-sub{margin-top:1rem;font-family:var(--mono);font-size:.7rem;letter-spacing:.5em;
  text-transform:uppercase;color:rgba(var(--amber-rgb),.55)}
.boot-track{margin-top:2.2rem;width:min(22rem,72vw);height:.35rem;background:rgba(var(--amber-rgb),.1);
  border:1px solid rgba(var(--amber-rgb),.3);position:relative;overflow:hidden}
.boot-fill{position:absolute;inset:0 auto 0 0;width:0;
  background:linear-gradient(90deg,var(--amber-lo),var(--amber),var(--sheen));box-shadow:0 0 16px var(--amber);transition:width .2s ease}
.boot-pct{margin-top:.8rem;font-family:var(--mono);font-size:.75rem;letter-spacing:.3em;color:var(--ink-3)}
@keyframes boot-flicker{0%,100%{opacity:1}6%{opacity:.35}8%{opacity:1}49%{opacity:.92}51%{opacity:1}70%{opacity:.6}72%{opacity:1}}
@keyframes flicker{0%,100%{opacity:.97}50%{opacity:1}}

/* ═══════════════ AMBIENT LAYERS ═══════════════ */
/* Drifting particle field. Canvas rather than WebGL — this is a few
   hundred dots, and pulling in a 3D library to draw them would cost
   more than the whole rest of the page. */
#particles{position:fixed;inset:0;z-index:0;pointer-events:none;opacity:var(--particles-a)}

#aurora{position:fixed;inset:0;z-index:1;pointer-events:none;overflow:hidden;mix-blend-mode:var(--aurora-blend);opacity:var(--aurora-a)}
/* will-change tells the compositor to keep the blurred result on its own
   layer, which is what makes the translate-only drift above free. */
#aurora i{position:absolute;border-radius:50%;filter:blur(95px);display:block;
  will-change:transform}
#aurora .a1{width:48vw;height:48vw;left:-12vw;top:-14vw;background:radial-gradient(circle,var(--amber),transparent 64%);animation:drift1 29s ease-in-out infinite}
#aurora .a2{width:44vw;height:44vw;right:-14vw;top:16vh;background:radial-gradient(circle,var(--ice),transparent 64%);animation:drift2 37s ease-in-out infinite}
#aurora .a3{width:38vw;height:38vw;left:28vw;bottom:-16vh;background:radial-gradient(circle,var(--violet),transparent 64%);animation:drift3 32s ease-in-out infinite}
/* Translation only — the scale is deliberately gone.

   A blurred element that is merely moved can be rasterised once and then
   shifted by the compositor. Scale it and that cache is void: the blur has
   to be recomputed at the new size on every frame it changes, and these are
   three 60vw discs under a 95px kernel. That was the whole light-theme
   scroll cost. The drift reads the same; only the slow breathing is gone. */
@keyframes drift1{0%,100%{transform:translate(0,0)}50%{transform:translate(9vw,7vh)}}
@keyframes drift2{0%,100%{transform:translate(0,0)}50%{transform:translate(-7vw,9vh)}}
@keyframes drift3{0%,100%{transform:translate(0,0)}50%{transform:translate(6vw,-7vh)}}

/* The oversize box exists so the grain shuffle never drags an edge into
   view. The animation moves it by at most 4% of its own width, so 120%
   clears the viewport with room to spare — 200% was covering four times
   the viewport area and rasterising all of it. */
#grain{position:fixed;inset:-10%;width:120%;height:120%;z-index:46;pointer-events:none;opacity:var(--grain-a);
  will-change:transform;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  animation:grain .5s steps(2) infinite}
@keyframes grain{0%{transform:translate(0,0)}25%{transform:translate(-4%,2%)}50%{transform:translate(3%,-3%)}75%{transform:translate(-2%,4%)}100%{transform:translate(2%,-2%)}}

/* Vignette only. The CRT stripe that used to sit here covered the whole page,
   which is invisible against the dark ground but bands every bright image on
   it — the portrait now, and any project screenshot later. */
#scanlines{position:fixed;inset:0;z-index:40;pointer-events:none;
  background:radial-gradient(circle,rgba(var(--cast),0) 55%,rgba(var(--cast),var(--vig-a)) 92%);
  box-shadow:inset 0 0 10rem rgba(var(--cast),var(--vig2-a))}

#progress{position:fixed;top:0;left:0;height:3px;width:0;z-index:99998;
  background:linear-gradient(90deg,var(--amber-lo),var(--amber),var(--sheen));box-shadow:0 0 12px var(--amber)}

/* ═══════════════ CUSTOM CURSOR ═══════════════ */
.cur-dot,.cur-ring{position:fixed;top:0;left:0;pointer-events:none;z-index:99999;border-radius:50%;
  transform:translate(-50%,-50%);mix-blend-mode:var(--cursor-blend);will-change:transform}
.cur-dot{width:6px;height:6px;background:var(--amber);box-shadow:0 0 10px var(--amber),0 0 22px var(--amber)}
.cur-ring{width:30px;height:30px;border:1.5px solid rgba(var(--amber-rgb),.6);
  transition:width .22s ease,height .22s ease,border-color .22s ease,background-color .22s ease}
/* The hover and press states used to swap the ring to --ink, which is
   near-white on the dark theme — fine over the page, invisible the moment
   the pointer crossed anything bright, a light-mode screenshot most of all.
   Staying amber keeps it readable on any ground. The state still reads: the
   ring goes from 60% opacity at rest to solid, and changes size either way. */
.cur-ring.hover{width:54px;height:54px;border-color:var(--amber);background:rgba(var(--amber-rgb),.08)}
.cur-ring.down{width:20px;height:20px;border-color:var(--amber)}
body.cursor-on,body.cursor-on *{cursor:none !important}
@media (hover:none),(pointer:coarse){.cur-dot,.cur-ring{display:none !important}}

/* ═══════════════ LAYOUT ═══════════════ */
.shell{position:relative;z-index:10}
.wrap{max-width:var(--maxw);margin:0 auto;padding-inline:var(--gut)}
section{padding-block:clamp(4rem,9vw,7rem);position:relative}

.sec-head{display:flex;align-items:flex-end;justify-content:space-between;gap:1.5rem;
  flex-wrap:wrap;margin-bottom:2.5rem;border-bottom:1px solid var(--line);padding-bottom:1rem}
.sec-head h2{margin:0;font-size:clamp(1.8rem,4.5vw,3rem);font-weight:700;line-height:1}
.sec-head h2 em{font-style:normal;color:var(--amber)}
/* Measured, not guessed: at these sizes the mono eyebrow's ink starts 2.4px
   left of the display face's ink below it, because the two fonts carry very
   different left side bearings. Both boxes are flush; only the paint is not.
   The nudge is only applied where an eyebrow sits directly above a heading. */
.sec-head .eyebrow{margin-left:2px}
.eyebrow{font-family:var(--mono);font-size:.7rem;letter-spacing:.28em;text-transform:uppercase;color:var(--ink-3)}

/* ═══════════════ NAV ═══════════════ */
/* ═══ Nav geometry ported from Rex ═══
   fixed · z-50 · transition-all 300ms · transparent until scrolled, then
   backdrop-blur-md + bg/80 + border-white/5 · inner max-w-7xl · px-8
   md:px-20 · py-6 — height comes from the logo, it isn't fixed. */
header.nav{position:fixed;top:0;left:0;right:0;z-index:50;
  background:transparent;border-bottom:1px solid transparent;
  transition:background .3s ease,border-color .3s ease,backdrop-filter .3s ease}
header.nav.stuck{background:rgba(var(--ground-rgb),.8);
  backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
  border-bottom-color:rgba(var(--sheen-rgb),.06)}
.nav-in{max-width:80rem;margin:0 auto;padding:1.5rem 2rem;
  display:flex;align-items:center;justify-content:space-between;gap:1rem}
/* The nav's own breakpoint is 900px, not the 768px the rest of the layout
   uses. At 768 the padding jumps to 5rem a side — 160px gone — while the
   burger did not appear until 767, so between roughly 768 and 870 the last
   link was pushed past the viewport edge and clipped away by the body's
   overflow-x:hidden. Wide padding and the burger now switch at the same
   width, so the bar can never be wider than the room it has. */
@media(min-width:900px){.nav-in{padding:1.5rem 5rem}}
/* His wordmark is baked into the logo PNG; yours is a round mark plus
   live text, so the text is styled to sit alongside it at the same weight. */
.logo{display:flex;align-items:center;gap:.75rem;font-family:var(--display);
  color:var(--ink);white-space:nowrap;line-height:1}
.logo-word{font-size:1.5rem;font-weight:700;letter-spacing:-.01em;text-transform:uppercase}
.logo b{color:var(--amber);font-weight:700}
/* h-10 md:h-12, object-contain, hover drop-shadow glow — his values. */
.logo-mark{height:40px;width:40px;flex:none;border-radius:50%;object-fit:contain;
  transition:filter .3s ease,transform .3s ease}
.logo:hover .logo-mark{transform:scale(1.05);
  filter:drop-shadow(0 0 15px rgba(var(--amber-rgb),1))}
@media(min-width:900px){
  .logo-mark{height:48px;width:48px}
  .logo-word{font-size:1.75rem}
}
@media(max-width:420px){.logo-word{font-size:1.15rem}.logo-mark{height:36px;width:36px}}

.boot-logo-img{width:clamp(3rem,10.5vw,7rem);height:clamp(3rem,10.5vw,7rem);flex:none;
  border-radius:50%;box-shadow:0 0 34px rgba(var(--amber-rgb),.4)}
.boot-word{white-space:nowrap}
.nav-links{display:flex;gap:.25rem;align-items:center}
/* --ink-2 is body-copy grey; on a nav it read as disabled. The links are the
   main way around the site, so they take the primary ink and a size that
   holds its own against the wordmark. */
.nav-links a{font-family:var(--mono);font-size:1.02rem;font-weight:600;letter-spacing:.12em;
  text-transform:uppercase;color:var(--ink);padding:.5rem .85rem;position:relative;
  transition:color .2s ease}
.nav-links a:hover,.nav-links a.on{color:var(--amber)}
.nav-links a.on::after{content:"";position:absolute;left:.85rem;right:.85rem;bottom:.2rem;
  height:2px;background:var(--amber)}
/* Theme toggle first, then the links — so the four sections sit hard right. */
.nav-right{display:flex;align-items:center;gap:.9rem}
/* Bare icon, no box — matching the hamburger beside it. */
.tbtn{width:40px;height:40px;flex:none;background:transparent;border:0;padding:0;
  color:var(--ink-2);display:flex;align-items:center;justify-content:center;font-size:1.3rem;
  line-height:1;cursor:pointer;transition:color .25s ease,transform .25s ease}
.tbtn:hover{color:var(--amber);transform:scale(1.12)}
/* His hamburger is bare — accent-coloured icon, no box around it. */
.burger{display:none;background:none;border:0;color:var(--amber);
  width:40px;height:40px;align-items:center;justify-content:center;
  font-size:1.6rem;line-height:1;cursor:pointer;padding:0}
/* His mobile breakpoint is md (768px), and the panel hangs off the bar
   itself rather than a hard-coded offset, so nav height can vary. */
@media(max-width:899px){
  .nav-links{position:absolute;top:100%;left:0;right:0;flex-direction:column;
    align-items:stretch;gap:0;background:var(--surface);border-bottom:1px solid var(--line);
    padding:.5rem 2rem 1.2rem;transform:translateY(-130%);z-index:-1;
    transition:transform .35s cubic-bezier(.2,.8,.2,1)}
  .nav-links.open{transform:translateY(0)}
  .nav-links a{padding:.85rem 0;border-bottom:1px solid var(--line)}
  .nav-links a.on::after{display:none}
  .burger{display:flex}
}

/* ═══════════════ HERO ═══════════════ */
/* ═══ Hero geometry ported from Rex's Tailwind classes ═══
   min-h-screen · max-w-7xl · px-8 md:px-20 · pt-20 md:pt-24 · pb-20 md:pb-0
   · grid-cols-1 lg:grid-cols-2 · gap-12 · items-center             */
#home{min-height:100vh;display:flex;align-items:center;overflow:hidden;
  padding:5rem 2rem;}
@media(min-width:768px){#home{padding:6rem 5rem 0}}
.hero-grid{position:absolute;left:50%;bottom:-2px;width:220%;height:44vh;pointer-events:none;opacity:.3;
  transform:translateX(-50%) perspective(420px) rotateX(62deg);transform-origin:bottom center;
  background-image:linear-gradient(rgba(var(--amber-rgb),.26) 1px,transparent 1px),
                   linear-gradient(90deg,rgba(var(--amber-rgb),.26) 1px,transparent 1px);
  background-size:56px 56px;animation:grid-move 5s linear infinite;
  -webkit-mask-image:linear-gradient(to top,#000 0%,transparent 78%);
  mask-image:linear-gradient(to top,#000 0%,transparent 78%)}
@keyframes grid-move{from{background-position:0 0}to{background-position:0 56px}}

.hero{display:grid;grid-template-columns:minmax(0,1fr);gap:3rem;align-items:center;
  width:100%;max-width:80rem;margin-inline:auto;padding-inline:0}
/* Rex splits this 1fr 1fr, but his portrait fills its half. Ours is a fixed
   400px plate, so an even split strands ~130px of dead space on the left and
   squeezes the name into a wrap. Give the plate exactly its width and hand
   the remainder to the text. */
/* Rex's even split. It leaves the plate's column wider than the plate itself,
   which is what puts the text block on the horizontal midline — the thing that
   actually reads as "his layout". Giving the text that spare width instead
   moves the whole block left and breaks the resemblance. */
@media(min-width:1024px){.hero{grid-template-columns:1fr 1fr}}

/* His portrait is a square: w-72 (288) → md:w-80 (320) → lg:400px,
   with the notched tech-border and a 1px inner gutter (p-1). */
/* The rings and halo overhang the plate, so they live in an unclipped
   wrapper — clip-path on the plate itself would cut them off. */
.portrait-wrap{position:relative;width:288px;height:288px;justify-self:center;
  display:grid;place-items:center}
@media(min-width:768px){.portrait-wrap{width:320px;height:320px}}
@media(min-width:1024px){.portrait-wrap{width:400px;height:400px;justify-self:start}}

.portrait{position:relative;z-index:1;width:100%;height:100%;
  padding:1px;background:var(--surface-2);border:1px solid var(--amber);
  clip-path:polygon(20px 0,100% 0,100% calc(100% - 20px),calc(100% - 20px) 100%,0 100%,0 20px);
  /* drop-shadow rather than box-shadow: a box-shadow would be clipped away
     with the rest of the element, a filter is applied after the clip. */
  filter:drop-shadow(0 0 14px rgba(var(--amber-rgb),.25));
  transition:filter .5s ease}
.portrait-wrap:hover .portrait{filter:drop-shadow(0 0 34px rgba(var(--amber-rgb),.75))}
/* Two counter-rotating rings orbiting the portrait. Opposed directions
   and mismatched periods so they never lock into step. */
/* His rings: 350/450px dashed at 20s, 320/420px solid at 25s reverse.
   Fixed pixel sizes, not percentages — they overhang the portrait. */
.orbit{position:absolute;top:50%;left:50%;translate:-50% -50%;border-radius:50%;
  aspect-ratio:1;pointer-events:none;z-index:0}
.orbit-1{width:350px;border:1px dashed var(--line-bright);opacity:.6;
  animation:orbit-cw 20s linear infinite}
.orbit-2{width:320px;border:1px solid rgba(var(--amber-rgb),.2);
  animation:orbit-ccw 25s linear infinite}
@media(min-width:768px){.orbit-1{width:450px}.orbit-2{width:420px}}
/* A node riding the inner ring, so the rotation is unmistakable. */
.orbit-2::after{content:"";position:absolute;top:-5px;left:50%;translate:-50% 0;
  width:9px;height:9px;border-radius:50%;background:var(--amber);
  box-shadow:0 0 12px var(--amber),0 0 26px rgba(var(--amber-rgb),.6)}
@keyframes orbit-cw{to{rotate:360deg}}
@keyframes orbit-ccw{to{rotate:-360deg}}

/* Blinking caret for the typed hero name. */
/* His caret is a fixed 8×32/48/64px block. Ours is sized in em so it tracks
   the measured font size instead of stepping at breakpoints. */
.caret{display:inline-block;width:.13em;height:.86em;background:var(--amber);
  margin-left:.06em;vertical-align:middle;margin-bottom:.08em;
  box-shadow:0 0 18px var(--amber);animation:caret .8s steps(1) infinite}
/* Characters still being resolved, shown dim ahead of the cursor. */
.scramble{color:rgba(var(--amber-rgb),.45);-webkit-text-fill-color:rgba(var(--amber-rgb),.45)}
@keyframes caret{0%,50%{opacity:1}50.01%,100%{opacity:0}}

.portrait-halo{position:absolute;inset:-10px;z-index:0;pointer-events:none;opacity:.45;filter:blur(16px);
  background:conic-gradient(from 0deg,transparent 0%,var(--amber) 12%,transparent 28%,var(--ice) 48%,transparent 64%,var(--violet) 82%,transparent 100%);
  animation:halo 9s linear infinite;transition:opacity .5s ease}
.portrait-wrap:hover .portrait-halo{opacity:.9}
@keyframes halo{to{transform:rotate(360deg)}}
.portrait-inner{position:relative;z-index:1;width:100%;height:100%;overflow:hidden;
  background:var(--ground);display:flex;align-items:center;justify-content:center;
  clip-path:polygon(20px 0,100% 0,100% calc(100% - 20px),calc(100% - 20px) 100%,0 100%,0 20px)}
/* His bottom fade over the photo: from-black/80 to transparent.
   Ramped to the top of the frame, that fade is still 28% black across the
   face — it seats the bottom edge at the cost of veiling the subject, and it
   was the reason the portrait read darker here than on paper.
   One curve now serves both themes: same strength, same falloff, cast in each
   theme's own shadow colour — black on the dark ground, warm brown on paper,
   which is exactly what --cast is for. */
.portrait-inner::after{content:"";position:absolute;inset:0;z-index:2;pointer-events:none;
  opacity:1;transition:opacity .45s ease;
  background:linear-gradient(to top,rgba(var(--cast),.5),transparent 62%)}
/* Hover lifts the veil off the lower half. This is the hover's cheapest
   headroom: the photograph itself can only take so much filter before it
   looks processed, but taking the fade back costs the image nothing. */
.portrait-wrap:hover .portrait-inner::after{opacity:.72}

/* Rest is the photograph, not a dimmed copy of it — saturate .82 and
   brightness .9 were leaving it visibly flat against everything around it.
   Hover keeps a clear delta on top of that, but the filter alone can't carry
   it: past roughly 1.15 saturation the sky goes electric and the skin turns
   orange, which reads as a filter rather than as the image lighting up. So the
   response is split four ways — filter, scale, the fade above, and the rings. */
.portrait-inner img{width:100%;height:100%;object-fit:cover;
  filter:saturate(.95) contrast(1) brightness(1.04);
  transition:filter .45s ease,transform .7s cubic-bezier(.2,.8,.2,1)}
.portrait-wrap:hover .portrait-inner img{
  filter:saturate(1.07) contrast(1.04) brightness(1.09);transform:scale(1.045)}
.portrait-inner{transition:border-color .45s ease,box-shadow .45s ease}
.portrait-wrap:hover .portrait-inner{border-color:var(--amber);
  box-shadow:0 0 34px rgba(var(--amber-rgb),.28),0 0 90px rgba(var(--amber-rgb),.1)}
/* Scanlines thin out on hover so the colour reads cleanly. */
/* The rings pick up as well, so the whole cluster responds together. */
.portrait-wrap:hover .orbit-1{border-color:rgba(var(--amber-rgb),.7)}
.portrait-wrap:hover .orbit-2{border-color:rgba(var(--amber-rgb),.85);
  box-shadow:0 0 34px rgba(var(--amber-rgb),.3),inset 0 0 34px rgba(var(--amber-rgb),.12)}
.portrait-wrap .orbit{transition:border-color .45s ease,box-shadow .45s ease}
.portrait-ph{transition:color .45s ease}
.portrait-wrap:hover .portrait-ph{color:var(--amber)}
.portrait-ph{font-family:var(--mono);font-size:.7rem;letter-spacing:.2em;color:var(--ink-3);text-align:center;padding:1rem}

/* Intro column. Centred below 1024px, left-aligned above — his breakpoint. */
.hero-copy{position:relative;display:flex;flex-direction:column;
  align-items:center;text-align:center;width:100%}
@media(min-width:1024px){.hero-copy{align-items:flex-start;text-align:left}}
/* His decorative marker: a 40px rule with a dot, hanging off the left. */
.hero-copy::before,.hero-copy::after{content:"";position:absolute;left:-2.5rem;top:1.6rem;display:none}
.hero-copy::before{width:2.5rem;height:1px;background:rgba(var(--amber-rgb),.5)}
.hero-copy::after{width:8px;height:8px;border-radius:50%;background:var(--amber);translate:-50% -50%}
@media(min-width:1024px){.hero-copy::before,.hero-copy::after{display:block}}

/* Sits under the name now, so it reads as a caption to it. */
.status{display:flex;align-items:center;gap:.75rem;font-family:var(--display);font-size:.75rem;
  letter-spacing:.2em;text-transform:uppercase;color:var(--amber);margin-bottom:0}
.status .dot{width:7px;height:7px;border-radius:50%;background:var(--ok);box-shadow:0 0 10px var(--ok);animation:pulse 2s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
.status .rule{flex:1;height:1px;background:linear-gradient(90deg,var(--line-bright),transparent);max-width:120px}

/* text-4xl → md:text-6xl → lg:text-7xl, font-black, tracking-tighter.
   A four-word name wraps to two lines at this size, so the line height is
   pulled in tight — 1.25 leaves the two halves looking unrelated. */
/* Never wraps. The size is measured and set by fitName() below, which fills
   the column with whatever name is in PROFILE — so this holds at any width
   and for any length of name, not just the ones that happen to be tested.
   The clamp() is only a fallback for when scripting is off. */
/* width:100% matters more than it looks. .hero-copy is a column flexbox with
   align-items:flex-start, which shrink-wraps its children — so the heading
   would report its own text width, fitName() would size to that, and the two
   would spiral down into each other until the size hit its floor. */
.hero h1{position:relative;width:100%;font-size:clamp(1.9rem,4.3vw,4.15rem);
  font-weight:900;line-height:1.05;margin:0 0 1rem;text-transform:uppercase;
  letter-spacing:-.05em;white-space:nowrap;   /* Rex's tracking-tighter */
  text-shadow:0 0 28px rgba(var(--amber-rgb),.35)}
/* A hidden copy holds the final size, and the typed text is laid over it, so
   nothing below shifts while the name resolves from one line to two. */
.hero h1 .ghost{visibility:hidden}
.hero h1 .live{position:absolute;inset:0}
.name-flow{background-image:linear-gradient(100deg,var(--amber) 0%,var(--sheen) 30%,var(--amber-hi) 55%,var(--amber) 80%,var(--sheen) 100%);
  background-size:300% 100%;-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent;
  animation:name-flow 7s linear infinite}
@keyframes name-flow{to{background-position:300% 0}}

/* gap-3, mt-4 mb-8, px-3 py-1, text-xs, tracking-widest */
.roles{display:flex;flex-wrap:wrap;gap:.75rem;margin:1rem 0 2rem;justify-content:center}
@media(min-width:1024px){.roles{justify-content:flex-start}}
.chip{font-family:var(--body);font-size:.78rem;font-weight:700;letter-spacing:.1em;
  text-transform:uppercase;padding:.3rem .8rem;border:1px solid var(--line-bright);
  color:var(--ink);background:var(--surface);transition:color .25s,border-color .25s,box-shadow .25s}
/* His lead chip drops the box border for a 2px left rule. */
.chip.lead{background:rgba(var(--amber-rgb),.1);border:0;border-left:2px solid var(--amber);color:var(--amber)}
.chip:hover{color:var(--amber);border-color:var(--amber-lo);box-shadow:0 0 12px rgba(var(--amber-rgb),.16)}

/* max-w-xl, text-base md:text-lg, leading-relaxed, mb-10, lg:border-l lg:pl-6 */
.bio{color:var(--ink-2);max-width:36rem;margin:0 0 2.5rem;font-size:1rem;line-height:1.625;
  border:0;padding:0}
@media(min-width:768px){.bio{font-size:1.125rem}}
@media(min-width:1024px){.bio{border-left:1px solid var(--line);padding-left:1.5rem}}
.cta-row{display:flex;flex-wrap:wrap;gap:.8rem;justify-content:center}
@media(min-width:1024px){.cta-row{justify-content:flex-start}}

/* ═══════════════ BUTTONS ═══════════════ */
.btn{position:relative;overflow:hidden;isolation:isolate;display:inline-flex;align-items:center;gap:.6rem;
  font-family:var(--mono);font-size:.78rem;letter-spacing:.16em;text-transform:uppercase;font-weight:500;
  padding:.85rem 1.5rem;border:1px solid var(--line-bright);background:var(--surface);color:var(--ink);
  cursor:pointer;transition:border-color .25s,box-shadow .25s,color .25s}
.btn::before{content:"";position:absolute;top:0;left:-135%;width:55%;height:100%;z-index:3;pointer-events:none;
  background:linear-gradient(105deg,transparent,rgba(var(--sheen-rgb),var(--btn-sheen-a)),transparent);transform:skewX(-18deg)}
.btn:hover::before{animation:sheen .8s ease}
@keyframes sheen{from{left:-135%}to{left:165%}}
.btn:hover{border-color:var(--amber);color:var(--amber);box-shadow:0 0 22px rgba(var(--amber-rgb),.28),0 8px 28px rgba(var(--cast),var(--shadow-a))}
.btn.primary{background:var(--amber);color:var(--on-accent);border-color:var(--amber);font-weight:600}
.btn.primary:hover{color:var(--on-accent);background:var(--amber-hi);border-color:var(--amber-hi)}
.btn.pulse{animation:cta-pulse 2.6s ease-in-out infinite}
.btn.pulse:hover{animation:none}
@keyframes cta-pulse{0%,100%{box-shadow:0 0 0 0 rgba(var(--amber-rgb),0)}50%{box-shadow:0 0 22px 1px rgba(var(--amber-rgb),.4)}}
.magnetic{transition:transform .35s cubic-bezier(.2,.8,.2,1)}

/* ═══════════════ PROJECT CARDS ═══════════════ */
.filters{display:flex;flex-wrap:wrap;gap:.4rem}
.filters button{font-family:var(--mono);font-size:.85rem;font-weight:600;letter-spacing:.12em;
  text-transform:uppercase;padding:.5rem 1rem;border:1px solid var(--line);background:transparent;
  color:var(--ink-2);cursor:pointer;transition:color .22s ease,border-color .22s ease,background .22s ease}
.filters button:hover{color:var(--ink-2);border-color:var(--line-bright)}
.filters button.on{color:var(--amber);border-color:var(--amber-lo);background:rgba(var(--amber-rgb),.07)}

.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,20rem),1fr));gap:1.1rem}

@property --edge{syntax:'<angle>';initial-value:0deg;inherits:false}
/* The card is an <a>. Inheriting colour and dropping the underline keeps it
   looking like the panel it was, while the browser gives it a link cursor,
   keyboard focus and open-in-new-tab for free. */
.card{position:relative;background:var(--surface);border:1px solid var(--line);
  display:flex;flex-direction:column;overflow:hidden;
  color:inherit;text-decoration:none;
  transform-style:preserve-3d;transition:transform .18s cubic-bezier(.2,.7,.2,1),border-color .3s,box-shadow .3s;
  will-change:transform}
.card:focus-visible{outline:2px solid var(--amber);outline-offset:3px}
.card:hover{border-color:var(--amber-lo);
  box-shadow:0 0 28px rgba(var(--amber-rgb),.16),0 18px 46px rgba(var(--cast),var(--shadow-a2))}
.card::after{content:"";position:absolute;inset:0;z-index:6;pointer-events:none;padding:1px;opacity:0;
  transition:opacity .4s ease;
  background:conic-gradient(from var(--edge),transparent 0deg,transparent 170deg,
    var(--amber) 250deg,var(--sheen) 300deg,var(--ice) 330deg,transparent 360deg);
  -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}
.card:hover::after{opacity:1;animation:edge 2.6s linear infinite}
@keyframes edge{to{--edge:360deg}}

.card-glare{position:absolute;inset:0;z-index:5;pointer-events:none;opacity:0;transition:opacity .35s ease;mix-blend-mode:overlay;
  background:radial-gradient(circle at var(--gx,50%) var(--gy,50%),rgba(var(--sheen-rgb),var(--glare-a)),rgba(var(--sheen-rgb),0) 42%)}
.card:hover .card-glare{opacity:1}
.card-sheen{position:absolute;top:0;left:-120%;width:60%;height:100%;z-index:4;pointer-events:none;transform:skewX(-18deg);
  background:linear-gradient(105deg,transparent 0%,rgba(var(--sheen-rgb),var(--sheen-a2)) 45%,rgba(var(--sheen-rgb),var(--sheen-a)) 50%,rgba(var(--sheen-rgb),var(--sheen-a2)) 55%,transparent 100%)}
.card:hover .card-sheen{animation:sheen-card .9s ease}
@keyframes sheen-card{from{left:-120%}to{left:160%}}

.card-bar{display:flex;align-items:center;gap:.4rem;padding:.5rem .8rem;background:var(--surface-2);border-bottom:1px solid var(--line)}
.card-bar i{width:26px;height:4px;background:var(--line-bright);display:block}
.card-bar .live{margin-left:auto;width:7px;height:7px;border-radius:50%;background:var(--ok);box-shadow:0 0 8px var(--ok)}
.card-thumb{position:relative;aspect-ratio:16/9;background:var(--surface-2);overflow:hidden;
  display:flex;align-items:flex-end;border-bottom:1px solid var(--line)}
.card-thumb img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.card-thumb .ph{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  font-family:var(--mono);font-size:.68rem;letter-spacing:.2em;color:var(--ink-3);
  background:repeating-linear-gradient(135deg,var(--surface-2) 0 10px,var(--surface) 10px 20px)}
.card-thumb h3{position:relative;z-index:2;margin:0;padding:1rem;font-size:1.5rem;font-weight:700;
  letter-spacing:-.01em;width:100%;
  background:linear-gradient(to top,rgba(var(--ground-rgb),.94),transparent)}
.card-tag{position:absolute;top:.6rem;right:.6rem;z-index:3;font-family:var(--mono);font-size:.7rem;
  font-weight:600;letter-spacing:.1em;text-transform:uppercase;padding:.3rem .6rem;
  background:rgba(var(--ground-rgb),.85);border:1px solid var(--amber-lo);color:var(--amber)}
.card-body{padding:1.1rem;display:flex;flex-direction:column;gap:.9rem;flex:1}
.card-body p{margin:0;font-size:1rem;color:var(--ink-2);line-height:1.6;
  display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
.card-tech{display:flex;flex-wrap:wrap;gap:.4rem;margin-top:auto}
.card-tech span{font-family:var(--mono);font-size:.75rem;font-weight:600;letter-spacing:.08em;
  text-transform:uppercase;padding:.28rem .6rem;border:1px solid var(--line);color:var(--ink-2);
  transition:color .25s,border-color .25s}
.card:hover .card-tech span{color:var(--ice);border-color:var(--ice-lo)}

/* With the link row gone, nothing on the card says it is clickable except
   the cursor — and the cursor is invisible on touch. The title takes an
   arrow that slides out on hover and focus, which is the affordance the
   removed row used to carry. */
.card-thumb h3::after{
  content:"→";display:inline-block;margin-left:.5rem;
  color:var(--amber);opacity:0;transform:translateX(-6px);
  transition:opacity .28s ease,transform .28s cubic-bezier(.2,.7,.2,1)}
.card:hover .card-thumb h3::after,
.card:focus-visible .card-thumb h3::after{opacity:1;transform:translateX(0)}

/* ═══════════════ SKILLS + TIMELINE ═══════════════ */
.two-col{display:grid;grid-template-columns:1fr;gap:clamp(2rem,5vw,3.5rem);align-items:start}
@media(min-width:940px){.two-col{grid-template-columns:1.25fr .85fr}}

.tl{display:flex;flex-direction:column;gap:1.8rem;position:relative;padding-left:1.4rem}
.tl::before{content:"";position:absolute;left:4px;top:.5rem;bottom:.5rem;width:1px;background:var(--line-bright)}
.tl-item{position:relative}
.tl-item::before{content:"";position:absolute;left:-1.4rem;top:.55rem;width:9px;height:9px;border-radius:50%;
  background:var(--amber);box-shadow:0 0 0 3px var(--ground),0 0 12px var(--amber);transform:translateX(-.5px)}
.tl-top{display:flex;justify-content:space-between;align-items:baseline;gap:1rem;flex-wrap:wrap}
.tl-item h4{margin:0;font-family:var(--display);font-size:1.1rem;font-weight:600}
.tl-when{font-family:var(--mono);font-size:.7rem;letter-spacing:.1em;color:var(--ink-3);
  border:1px solid var(--line);padding:.15rem .5rem;white-space:nowrap;font-variant-numeric:tabular-nums}
.tl-org{font-family:var(--mono);font-size:.78rem;color:var(--amber);margin:.25rem 0 .5rem;
  display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}
.tl-org em{font-style:normal;font-size:.62rem;letter-spacing:.1em;text-transform:uppercase;
  border:1px solid var(--line);color:var(--ink-3);padding:.1rem .4rem}
.tl-item ul{margin:0;padding-left:1.1rem;color:var(--ink-2);font-size:.88rem;display:flex;flex-direction:column;gap:.2rem}
.tl-item ul li::marker{color:var(--ink-3)}

.panel{background:var(--surface);border:1px solid var(--line);padding:1.4rem;position:relative;
  clip-path:polygon(18px 0,100% 0,100% calc(100% - 18px),calc(100% - 18px) 100%,0 100%,0 18px)}
.panel::before{content:"";position:absolute;top:0;left:0;right:0;height:2px;
  background:linear-gradient(90deg,var(--amber),var(--ice),transparent)}
.panel h3{margin:0 0 1.2rem;font-size:1.25rem;font-weight:700}
.skill{margin-bottom:1rem}
.skill-top{display:flex;justify-content:space-between;align-items:baseline;gap:1rem;margin-bottom:.35rem}
.skill-top b{font-size:.85rem;font-weight:600}
.skill-top span{font-family:var(--mono);font-size:.66rem;letter-spacing:.1em;text-transform:uppercase;color:var(--amber)}
.bar{height:5px;background:var(--surface-2);border:1px solid var(--line);position:relative;overflow:hidden}
.bar i{position:absolute;inset:0 auto 0 0;width:0;display:block;
  background:linear-gradient(90deg,var(--amber-lo),var(--amber));transition:width 1.1s cubic-bezier(.2,.8,.2,1)}
.panel hr{border:0;border-top:1px dashed var(--line);margin:1.4rem 0}

/* ═══════════════ CONTACT ═══════════════ */
#contact .wrap{text-align:center}
#contact .sec-head{justify-content:center;align-items:center;text-align:center;border:0;flex-direction:column;gap:.6rem}
.contact-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,21rem),1fr));gap:.9rem;
  max-width:46rem;margin:0 auto;text-align:left}
.ch{display:flex;align-items:center;gap:.9rem;padding:1rem;background:var(--surface);
  border:1px solid var(--line);transition:border-color .3s,box-shadow .3s,transform .3s}
.ch:hover{border-color:var(--amber-lo);box-shadow:0 0 20px rgba(var(--amber-rgb),.14);transform:translateY(-2px)}
.ch .glyph{width:38px;height:38px;flex:none;display:flex;align-items:center;justify-content:center;
  border:1px solid var(--line-bright);font-family:var(--mono);font-size:.85rem;color:var(--amber)}
.ch .meta{display:flex;flex-direction:column;min-width:0;line-height:1.35}
.ch .lbl{font-family:var(--mono);font-size:.62rem;letter-spacing:.2em;text-transform:uppercase;color:var(--ink-3)}
.ch .val{font-size:.9rem;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.copy{margin-left:auto;flex:none;background:none;border:1px solid var(--line);color:var(--ink-3);
  font-family:var(--mono);font-size:.6rem;letter-spacing:.1em;text-transform:uppercase;padding:.3rem .5rem;cursor:pointer;
  transition:color .2s,border-color .2s}
.copy:hover{color:var(--amber);border-color:var(--amber-lo)}
.copy.done{color:var(--ok);border-color:var(--ok)}

footer.foot{border-top:1px solid var(--line);padding:2rem 0;text-align:center;
  font-family:var(--mono);font-size:.68rem;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3)}

/* ═══════════════ SCROLL REVEAL ═══════════════ */
.reveal{opacity:0;transform:translateY(22px);transition:opacity .7s ease,transform .7s cubic-bezier(.2,.8,.2,1)}
.reveal.in{opacity:1;transform:none}

/* ═══════════════ REDUCED MOTION ═══════════════ */
@media(prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  #aurora i,#grain,.hero-grid,.name-flow,.portrait-halo,.card:hover::after,.btn.pulse,.status .dot,#boot::before,.boot-mark,.orbit,.caret{animation:none !important}
  .reveal{opacity:1;transform:none;transition:none}
  *{transition-duration:.01ms !important}
}
