/* Harson.ai — neon.
   ------------------------------------------------------------------
   THE READER — unchanged from dawn: someone part-way through buying a
   Harson app, or an Omarchy user who found the Portal plugin, checking
   the company is real. What changes is the answer to "what earns their
   trust". Dawn answered it with calm. This answers it with capability:
   the page itself is the proof, because a template cannot produce it.

   WHY THIS EXISTS — the `tty` direction was built on the rule
   "Terminal, not hacker: no green-on-black, no glow, no rain", and was
   rejected with "thats not what i meant". So the brief is the exact
   thing tty refused. Rain, glow and neon it is, on Rodney's own words:
   "we are developers afterall".

   WHAT SURVIVES FROM DAWN — the plane architecture, which he liked:
   distinct depths moving at their own rate on scroll and to the
   pointer. Only the contents of the planes change: a perspective grid
   instead of ridgelines, a skyline instead of pines, falling glyphs
   instead of stars.
   ------------------------------------------------------------------ */


/* ============ THE PALETTE ====================================
   Tokyo Night pushed to neon. Every colour on the page is here.
   ============================================================= */
:root{
  --void:      #04050c;   /* the ground everything sits on */
  --deep:      #0a0d24;   /* upper sky */
  --horizon:   #2a1258;   /* where the grid meets the sky */
  --grid:      #3d5bff;   /* the perspective floor */
  --cyan:      #22d3ee;   /* primary neon */
  --magenta:   #ff2d95;   /* secondary neon */
  --rain:      #57ffa8;   /* falling glyphs — green, but cyan-shifted
                             so it belongs with the rest */
  --amber:     #ffb86b;
  --sun:       #ff4d7d;

  --ink:       #dbe6ff;
  --ink-soft:  #8d9ac8;
  --ink-bright:#ffffff;
  --panel:     #0b0f26;
  --line:      rgba(120,160,255,.16);

  --step-6: clamp(2.5rem,7.5vw,6rem);
  --step-3: clamp(1.4rem,3vw,2.3rem);
}

*,*::before,*::after{box-sizing:border-box}
body{
  margin:0;background:var(--void);color:var(--ink);
  font:400 clamp(1rem,1.05vw,1.1rem)/1.7 "JetBrains Mono",ui-monospace,monospace;
  -webkit-font-smoothing:antialiased;overflow-x:hidden;
}
h1,h2,h3{margin:0;font-family:"Chakra Petch",system-ui,sans-serif;
  font-weight:600;line-height:1.02;letter-spacing:-.01em;color:var(--ink-bright)}
a{color:inherit;text-underline-offset:.25em}
.wrap{width:min(74rem,calc(100% - 2.4rem));margin-inline:auto}
.narrow{width:min(46rem,calc(100% - 2.4rem));margin-inline:auto}

/* ================= THE SCENE =================================
   Seven planes. Same idea as dawn: near things move more, on scroll
   AND to the pointer. All transforms, so it stays on the compositor.
   ============================================================= */
.scene{position:relative;min-height:min(100svh,48rem);max-height:54rem;
  /* clip, NOT hidden: `hidden` makes this a scroll container, which makes every
     `animation-timeline: view()` on the layers below resolve against THIS box
     instead of the page. It never scrolls, so the parallax sat frozen. */
  overflow:clip;display:grid;align-items:center;
  background:
    radial-gradient(120% 70% at 50% 100%, var(--horizon) 0%, transparent 62%),
    linear-gradient(var(--void) 0%, var(--deep) 46%, #14093a 78%, #2a1258 100%)}
.scene::after{content:"";position:absolute;inset:0;background:var(--void);
  opacity:0;pointer-events:none;z-index:7}
/* Pointer and tilt drive `translate`; the scroll keyframes drive `transform`.
   They are independent properties, so the two compose. Both on `transform`
   meant the running scroll animation replaced the pointer offset outright. */
.layer{position:absolute;inset:0;pointer-events:none;--depth:0;
  translate:calc(var(--mx,0)*var(--depth)*12px) calc(var(--my,0)*var(--depth)*9px);
  transition:translate .5s cubic-bezier(.2,.8,.2,1)}

/* --- the rain. A canvas, because 40 columns of falling glyphs is a
   job for a canvas, not 40 animated DOM nodes. --- */
#rain{--depth:.25;width:100%;height:100%;opacity:.85;
  -webkit-mask-image:linear-gradient(#000 0%,#000 62%,transparent 95%);
          mask-image:linear-gradient(#000 0%,#000 62%,transparent 95%)}

/* --- the sun: a neon disc with a hard horizon line through it, the
   one shape that says "this is that world" in a single glance --- */
.sun{--depth:.35;left:auto;right:12%;top:14%;bottom:auto;
  width:min(21rem,38vw);aspect-ratio:1;border-radius:50%;
  background:
    repeating-linear-gradient(var(--void) 0 3px, transparent 3px 9px),
    linear-gradient(#ffd36b 0%, var(--sun) 55%, var(--magenta) 100%);
  filter:drop-shadow(0 0 55px rgba(255,45,149,.4))}
/* The glow breathes by fading a pseudo-element, not by animating a filter.
   An animated filter is repainted every frame and, with the rain canvas in
   the same stacking context, that cost lands on every rain frame too. */
.sun::after{content:"";position:absolute;inset:-26%;border-radius:50%;z-index:-1;
  background:radial-gradient(circle,rgba(255,45,149,.4),transparent 66%);
  opacity:.55;animation:sun-breathe 7s ease-in-out infinite}
@keyframes sun-breathe{50%{opacity:1}}

/* There is an eye in the sun — and you only find out when it blinks.

   An almond of shadow in the middle of the disc, under the banding, with
   a soft dark heart in it: at rest you'd take it for a fold in the
   gradient. It leans toward the pointer on the same --mx the parallax
   uses (tilt, on a phone), slowly enough that you notice it has moved,
   not that it is moving.

   Once every thirty-six seconds it wakes: the almond fades in, the pupil
   surfaces, sharp, ringed cold, with a glint in it — an open eye, looking
   at you — the lids close, the sun is just the sun for a beat, it opens,
   holds the stare, and then it is gone. Completely. Thirty seconds of
   plain sun before it happens again: long enough to wonder whether you
   saw anything.

   The eye shape is a mask on .face, and the lids are a clip on that same
   element closing over it, which is why shut looks like sun and not like a
   dark slit.

   --eye is the dial; 0 is the plain sun. */
.sun{--eye:1}
.face{position:absolute;inset:0;opacity:var(--eye);isolation:isolate;
  -webkit-mask-image:var(--eye-shape);mask-image:var(--eye-shape);
  -webkit-mask-size:100% 100%;mask-size:100% 100%;
  -webkit-mask-position:50% 50%;mask-position:50% 50%;
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  animation:eye-blink 36s linear infinite, eye-appear 36s linear infinite}
:root{--eye-shape:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><path fill='white' d='M24,50 C34,35.5 66,35.5 76,50 C66,62.5 34,62.5 24,50 Z'/></svg>")}
/* the almond itself: a soft socket of shadow */
.face::before{content:"";position:absolute;inset:0;mix-blend-mode:multiply;opacity:.6;
  background:radial-gradient(ellipse 30% 11% at 50% 50%, rgba(40,8,48,.55), rgba(40,8,48,.22) 60%, transparent 100%);
  animation:socket 36s linear infinite}
.iris{position:absolute;left:50%;top:50%;width:34%;aspect-ratio:1;z-index:1;
  margin:-17% 0 0 -17%;border-radius:50%;
  translate:calc(var(--mx,0) * 13%) calc(var(--my,0) * 6%);
  transition:translate .7s cubic-bezier(.2,.8,.2,1)}
/* the resting state: a dark heart and a faint rim, nothing more */
.iris::before{content:"";position:absolute;inset:0;border-radius:50%;
  mix-blend-mode:multiply;--sh:40,8,48;
  background:
    radial-gradient(circle, rgba(var(--sh),.7) 0 16%, rgba(var(--sh),.32) 26%, transparent 40%),
    radial-gradient(circle, transparent 0 43%, rgba(var(--sh),.2) 49.5%)}
/* the stare: the sharp pupil that exists only just after a blink */
.pupil{position:absolute;left:50%;top:50%;width:40%;aspect-ratio:1;
  margin:-20% 0 0 -20%;border-radius:50%;background:var(--void);opacity:0;
  box-shadow:0 0 0 1.5px rgba(34,211,238,.65), 0 0 14px rgba(34,211,238,.4);
  animation:stare 36s linear infinite}
.pupil::after{content:"";position:absolute;left:26%;top:22%;width:16%;height:12%;
  border-radius:50%;background:#dbe6ff;opacity:.9}
/* The cycle is 36s (1% = 360ms), and the eye exists for six of them:
     12%  the almond fades in, a fold in the gradient — 5.4s after load
     18.5 the pupil surfaces and the socket deepens: an open eye, looking
     23.5 the lids close: 216ms down, 144 held, 252 up. The clip closes
          toward 58% of the height, so the upper lid travels further than
          the lower one, which is what a blink looks like
     25.2 open again, still staring
     27.5 it recedes, and by 30 it is gone. Not faint: gone. The sun is
          just the sun for the next thirty seconds, which is long enough
          to wonder whether you saw anything at all */
/* Named eye-blink, not blink: the cursor further down already owns
   @keyframes blink, and a later definition of the same name silently wins.
   The lids are a clip-path on the masked element, not an animated mask-size:
   the mask never changes, the visible band of it closes to nothing at 58%
   of the height and reopens. clip-path is reliably animatable; mask-size
   silently was not. */
@keyframes eye-blink{0%,23.5%,25.2%,100%{clip-path:inset(0 0 0 0)}
  24.1%,24.5%{clip-path:inset(58% 0 42% 0)}}
@keyframes eye-appear{0%,12%,30%,100%{opacity:0} 16%,27.5%{opacity:var(--eye)}}
@keyframes stare{0%,18.5%,28.5%,100%{opacity:0} 20%,27%{opacity:1}}
@keyframes socket{0%,12%,30%,100%{opacity:.6} 18%,27%{opacity:1}}

/* --- the perspective floor. Two gradients in a skewed plane: the
   Tron horizon, done in CSS so it recolours with the palette. --- */
.grid{--depth:.8;top:auto;bottom:0;height:46%;
  perspective:260px;perspective-origin:50% 0;
  -webkit-mask-image:linear-gradient(transparent 0%, #000 22%, #000 100%);
          mask-image:linear-gradient(transparent 0%, #000 22%, #000 100%)}
/* The floor itself: laid flat in 3D so the lines actually converge on the
   horizon instead of being a flat lattice pretending to. It is a child
   because .layer already owns a transform for the parallax. */
.grid::before{content:"";position:absolute;left:-60%;right:-60%;top:0;height:340%;
  transform:rotateX(76deg);transform-origin:50% 0;
  background-image:
    repeating-linear-gradient(90deg, transparent 0 3.4%,
      rgba(61,91,255,.85) 3.4% calc(3.4% + 2px)),
    repeating-linear-gradient(0deg, transparent 0 5.2%,
      rgba(34,211,238,.55) 5.2% calc(5.2% + 2px));
  /* The far edge of a plane at 76deg compresses into a single hard line
     across the page. Fade it in its own (pre-rotation) space so the
     distance dissolves instead of terminating. */
  -webkit-mask-image:linear-gradient(transparent 0 5%, #000 26%, #000 100%);
          mask-image:linear-gradient(transparent 0 5%, #000 26%, #000 100%);
  animation:grid-run 3.2s linear infinite}
/* Only the horizontal lines travel, which is what reads as motion toward
   you; moving both axes just looks like the whole floor sliding. */
@keyframes grid-run{to{background-position:0 0, 0 5.2%}}
/* a soft glow where the floor meets the sky — no hard edge */
.grid::after{content:"";position:absolute;left:0;right:0;top:-4%;height:22%;
  background:linear-gradient(transparent, rgba(34,211,238,.18), transparent);
  filter:blur(10px)}

/* --- skylines. Two depths, the near one darker and taller. --- */
/* The svg used to be position:absolute inside a height:auto div, which
   collapsed the div to ZERO height. A percentage translate resolves against
   the element's own height, so `-13%` and `-26%` of nothing meant these two
   planes — the nearest and most visible ones — never moved on scroll at all.
   Letting the svg size the div fixes the parallax; the div is already
   bottom-anchored, so nothing shifts visually. */
.city{color:#0a0e2e;bottom:0;top:auto;height:auto;line-height:0}
.city svg{width:100%;height:auto;display:block}
.city-far{--depth:.6;opacity:.85}
.city-far svg{filter:drop-shadow(0 0 22px rgba(61,91,255,.5))}
.city-near{--depth:1.5;color:#04050f}
.city-near svg{filter:drop-shadow(0 -2px 26px rgba(255,45,149,.28))}

/* Lit windows in the near skyline. Staggered per-window delay (set
   inline when the SVG is generated) so the city looks occupied rather
   than switched on all at once. */
.city-near .win rect{opacity:.5;animation:win-flick 9s steps(1) infinite}
/* The far skyline had no lights at all, which is what made it read as a
   backdrop rather than as distance. Dimmer, slower, and on its own cycle so
   the two skylines never blink in step. */
.city-far .win rect{opacity:.3;animation:win-flick 13s steps(1) infinite}

/* Aircraft warning lights on the masts. Real skylines have them and almost
   no drawn ones do — it is the single detail that makes a silhouette read
   as a working city instead of a cardboard cut-out. */
.city .beacon{filter:drop-shadow(0 0 5px #ff3b6b)}
.city .beacon circle{fill:#ff3b6b;animation:beacon 4.2s ease-in-out infinite}
.city-far .beacon circle{fill:#ff6b90;animation-name:beacon-far}
@keyframes beacon{0%,38%{opacity:.12} 48%,55%{opacity:1} 65%,100%{opacity:.12}}
@keyframes beacon-far{0%,38%{opacity:.06} 48%,55%{opacity:.5} 65%,100%{opacity:.06}}
@keyframes win-flick{
  0%,44%{opacity:.55} 45%,52%{opacity:.12}
  53%,80%{opacity:.75} 81%,88%{opacity:.25} 89%,100%{opacity:.6}}

/* --- easter eggs in the near skyline ------------------------------
   Same rule as the eye in the sun: rare, brief, and each on its own
   clock (the searchlight on the eye's 36; the rest 41, 47, 53 — chosen so
   they drift against each other and never line up). You have to be watching. All four sit
   in their off state under reduced motion.                             */

/* A searchlight on the tallest tower, three seconds after the eye has
   gone. A real one: a cone of light with its lamp at the parapet, fanning
   up into the sky, swinging from left to right in one slow pass. It
   shares the eye's 36s clock, so it follows every blink.
   The cone is drawn before the silhouette (the lamp sits behind the
   parapet) and reaches far above the skyline's own box, so the svg has
   to be allowed to overflow; the scene clips it.                       */
.city-near svg{overflow:visible}
.city-near .cone{mix-blend-mode:screen;opacity:0;transform-origin:440.7px 52px;
  animation:cone-turn 36s linear infinite, cone-glow 36s linear infinite}
.city-near .lamp{opacity:0;filter:drop-shadow(0 0 5px #fff);animation:cone-glow 36s linear infinite}
/* 38% = 13.7s in. Three and a quarter seconds of sweep, fading at both ends. */
@keyframes cone-turn{0%,38%{transform:rotate(-54deg)} 47%,100%{transform:rotate(54deg)}}
@keyframes cone-glow{0%,38%,47%,100%{opacity:0} 39%,46%{opacity:1}}

/* The tallest tower loses power: sixteen windows go out over a second
   and a half and come back the same way. A brown-out, not a switch.   */
.city-near .tower rect{opacity:.55;animation:blackout 47s linear infinite}
@keyframes blackout{0%,60%{opacity:.55} 60.4%,64%{opacity:0} 64.6%,100%{opacity:.55}}

/* A neon sign on the seventh building buzzes on — the classic stutter,
   three tries before it holds — stays lit six seconds, and dies.      */
.city-near .sign{opacity:0;filter:drop-shadow(0 0 5px #ff2d95);animation:neon-on 53s linear infinite}
@keyframes neon-on{0%,84%{opacity:0} 84.3%{opacity:.9} 84.5%{opacity:.1} 84.9%{opacity:.9}
  85.2%{opacity:.2} 85.5%,90%{opacity:1} 90.5%,100%{opacity:0}}

/* One window, one building, goes red. Three flashes and a hold, then
   nothing, for forty seconds.                                          */
.city-near .alarm{opacity:0;filter:drop-shadow(0 0 4px #ff3b3b);animation:alarm 41s linear infinite}
@keyframes alarm{0%,90%{opacity:0} 90.3%{opacity:1} 90.6%{opacity:0} 90.9%{opacity:1}
  91.2%{opacity:0} 91.5%,92.6%{opacity:1} 93%,100%{opacity:0}}

/* --- CRT scanlines over everything, plus a slow sweep --- */
.scan{--depth:0;mix-blend-mode:overlay;opacity:.5;
  background:repeating-linear-gradient(rgba(255,255,255,.055) 0 1px,
                                       transparent 1px 3px)}
.scan::after{content:"";position:absolute;left:0;right:0;top:0;height:22%;
  background:linear-gradient(transparent, rgba(120,200,255,.09), transparent);
  animation:sweep 7.5s linear infinite;will-change:transform}
/* translateY, not `top`: `top` triggers layout on every frame. */
@keyframes sweep{from{transform:translateY(-120%)}to{transform:translateY(560%)}}

/* scroll rates — sky barely, city most */
@keyframes rise-slow{to{transform:translate3d(0,-5%,0)}}
@keyframes rise-mid {to{transform:translate3d(0,-13%,0)}}
@keyframes rise-fast{to{transform:translate3d(0,-26%,0)}}
@supports (animation-timeline:view()){
  #rain     {animation:rise-slow linear both;animation-timeline:view()}
  .sun      {animation:rise-slow linear both;animation-timeline:view()}
  .grid     {animation:rise-mid  linear both;animation-timeline:view()}
  .city-far {animation:rise-mid  linear both;animation-timeline:view()}
  .city-near{animation:rise-fast linear both;animation-timeline:view()}
}

/* ---- header ------------------------------------------------- */
.top{position:absolute;top:0;left:0;right:0;z-index:6;padding:1.4rem 0}
.top .wrap{display:flex;align-items:center;justify-content:space-between;gap:1rem}
/* The brand lockup: the mark, then the wordmark. The mark is one SVG —
   brain half in white, machine half in cyan. `gap` spaces them, so the
   two never collide when the nav wraps on a narrow phone. */
.brand{display:flex;align-items:center;gap:.6rem;
  font-family:"Chakra Petch",sans-serif;font-size:1.45rem;font-weight:700;
  text-decoration:none;color:var(--ink-bright);letter-spacing:-.02em;
  text-shadow:0 0 18px rgba(34,211,238,.55)}
.brand img{display:block;width:2.1rem;height:2.1rem;flex:none;
  filter:drop-shadow(0 0 10px rgba(34,211,238,.45))}
.brand .wm span{color:var(--cyan)}
.top nav{display:flex;gap:1.4rem;font-size:.82rem;letter-spacing:.04em}
.top nav a{color:var(--ink-soft);text-decoration:none;transition:color .2s,text-shadow .2s}
.top nav a:hover{color:var(--cyan);text-shadow:0 0 12px rgba(34,211,238,.7)}

/* ---- hero ---------------------------------------------------- */
.hero{position:relative;z-index:5;padding:9rem 0 11rem}
.eyebrow{font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;
  color:var(--cyan);margin:0 0 1.5rem;display:flex;align-items:baseline;gap:.6rem;
  flex-wrap:nowrap}
.eyebrow b{font-weight:400}
.eyebrow::before{content:"";width:9px;height:9px;background:var(--cyan);
  box-shadow:0 0 12px var(--cyan);animation:blink 1.2s steps(1) infinite}
@keyframes blink{50%{opacity:0}}

/* The headline carries the page. RGB split on a timer rather than
   constantly — a permanent glitch reads as broken, an occasional one
   reads as deliberate. */
.hero h1{font-size:var(--step-6);max-width:15ch;margin:0 0 1.6rem;
  font-weight:700;text-wrap:balance;position:relative;
  text-shadow:0 0 34px rgba(34,211,238,.35)}
.hero h1 em{font-style:normal;color:var(--cyan);
  text-shadow:0 0 26px rgba(34,211,238,.85)}
.glitch{position:relative;display:inline-block}
/* `/ ""` gives the generated text empty alt text, so a screen reader announces
   the headline once instead of "software, software, software". */
.glitch::before,.glitch::after{content:attr(data-t) / "";position:absolute;
  left:0;top:0;width:100%;pointer-events:none;opacity:.85}
.glitch::before{color:var(--magenta);animation:g1 5.5s steps(1) infinite}
.glitch::after {color:var(--cyan);   animation:g2 5.5s steps(1) infinite}
@keyframes g1{0%,88%,100%{transform:none;clip-path:inset(0 0 100% 0)}
  90%{transform:translate(-3px,1px);clip-path:inset(12% 0 62% 0)}
  93%{transform:translate(2px,-2px);clip-path:inset(58% 0 20% 0)}}
@keyframes g2{0%,88%,100%{transform:none;clip-path:inset(0 0 100% 0)}
  91%{transform:translate(3px,-1px);clip-path:inset(30% 0 48% 0)}
  94%{transform:translate(-2px,2px);clip-path:inset(70% 0 8% 0)}}
/* :not(.eyebrow) — otherwise this rule outranks .eyebrow on specificity and
   the eyebrow renders as body copy. */
.hero>p:not(.eyebrow){font-size:clamp(1rem,1.25vw,1.15rem);color:var(--ink);
  max-width:44ch;margin:0;line-height:1.75}

/* ================= CONTENT ==================================== */
.sheet{position:relative;z-index:5;background:var(--void);
  border-top:1px solid var(--line)}
section{padding:clamp(3.5rem,8vw,6.5rem) 0}
.head{font-size:.72rem;letter-spacing:.2em;text-transform:uppercase;
  color:var(--cyan);margin:0 0 2rem;padding-bottom:.8rem;
  border-bottom:1px solid var(--line);display:flex;gap:.7rem;align-items:baseline}
.head::before{content:"//";color:var(--magenta)}

.facts{display:grid;grid-template-columns:repeat(3,1fr);gap:2rem;margin:0}
@media(max-width:820px){.facts{grid-template-columns:repeat(2,1fr)}}
@media(max-width:480px){.facts{grid-template-columns:1fr}}
.facts dt{font-size:.68rem;letter-spacing:.14em;text-transform:uppercase;
  color:var(--ink-soft);margin-bottom:.35rem}
.facts dd{margin:0;font-weight:500;color:var(--ink-bright);
  font-family:"Chakra Petch",sans-serif;font-size:1.05rem}
.facts a{color:var(--cyan)}

.rows{display:grid}
.line{display:grid;grid-template-columns:3rem 1fr;gap:1rem;align-items:baseline;
  padding:1.3rem 0;border-bottom:1px solid var(--line);
  transition:background .25s,padding-left .25s}
.line:last-child{border-bottom:0}
.line:hover{background:linear-gradient(90deg,rgba(34,211,238,.06),transparent);
  padding-left:.6rem}
.line .n{font-family:"JetBrains Mono",monospace;color:var(--magenta);font-size:.82rem}
.line h3{font-size:clamp(1.2rem,2.3vw,1.75rem)}
.line p{margin:.25rem 0 0;color:var(--ink-soft);font-size:.9rem;max-width:56ch}

.stmt{background:linear-gradient(180deg,#07091c,#0d0a2c);
  border-block:1px solid var(--line);position:relative;overflow:clip}
.stmt::before{content:"";position:absolute;inset:0;
  background:repeating-linear-gradient(115deg,transparent 0 22px,
    rgba(61,91,255,.05) 22px 23px)}
.stmt h2{font-size:var(--step-3);max-width:26ch;position:relative}
.stmt p{color:var(--ink);max-width:48ch;margin:1.4rem 0 0;position:relative}

.work{display:grid;grid-template-columns:repeat(auto-fit,minmax(16rem,1fr));gap:1rem}
.card{background:var(--panel);border:1px solid var(--line);padding:1.6rem;
  display:flex;flex-direction:column;gap:.65rem;text-decoration:none;
  position:relative;overflow:hidden;transition:border-color .25s,transform .25s}
/* the sweep only happens on the card you are pointing at */
.card::after{content:"";position:absolute;top:0;left:-120%;width:60%;height:100%;
  background:linear-gradient(105deg,transparent,rgba(34,211,238,.13),transparent);
  transition:left .6s ease}
.card:hover{border-color:var(--cyan);transform:translateY(-4px)}
.card:hover::after{left:140%}
.card .tag{font-size:.66rem;letter-spacing:.14em;text-transform:uppercase;
  color:var(--amber)}
.card .tag.live{color:var(--rain)}
.card h3{font-size:1.3rem}
.card p{margin:0;color:var(--ink-soft);font-size:.88rem}
.card .go{margin-top:auto;font-size:.82rem;color:var(--cyan);letter-spacing:.04em}

.close{text-align:center}
.close h2{font-size:var(--step-3);max-width:24ch;margin-inline:auto}
.close a{display:inline-block;margin-top:1.5rem;font-family:"Chakra Petch",sans-serif;
  font-size:clamp(1.05rem,2vw,1.5rem);color:var(--cyan);
  text-shadow:0 0 22px rgba(34,211,238,.6)}
footer{border-top:1px solid var(--line);padding:1.8rem 0 2.6rem;
  color:var(--ink-soft);font-size:.78rem}
footer .wrap{display:flex;flex-wrap:wrap;gap:.5rem 1.4rem;justify-content:space-between}
footer a{color:var(--ink-soft)}

/* ================= SCROLL ====================================
   Everything below is driven by scroll position rather than a timer,
   so it tracks the finger exactly and costs nothing when still.
   ============================================================= */

/* a progress rule across the top */
.progress{position:fixed;inset:0 0 auto 0;height:2px;z-index:60;transform-origin:0 50%;
  transform:scaleX(0);background:linear-gradient(90deg,var(--cyan),var(--magenta));
  box-shadow:0 0 12px rgba(34,211,238,.6)}
@supports (animation-timeline:scroll()){
  .progress{animation:prog linear both;animation-timeline:scroll(root block)}
}
@keyframes prog{to{transform:scaleX(1)}}

@keyframes rise-in{from{opacity:0;transform:translateY(1.6rem)}to{opacity:1;transform:none}}
@keyframes head-in{from{opacity:0;transform:translateX(-1.4rem)}to{opacity:1;transform:none}}
@keyframes diag-drift{to{background-position:140px 0}}
@keyframes scene-veil{to{opacity:.6}}

@supports (animation-timeline:view()){
  /* section headings arrive from the left */
  .head{animation:head-in linear both;animation-timeline:view();
    animation-range:entry 0% entry 45%}
  /* the facts and the numbered rows come up in sequence */
  .facts>div,.line{animation:rise-in linear both;animation-timeline:view();
    animation-range:entry 0% entry 50%}
  /* shipped cards stagger rather than arriving as a block */
  .work .card:nth-child(1){animation-range:entry 0% entry 42%}
  .work .card:nth-child(2){animation-range:entry 7% entry 52%}
  .work .card:nth-child(3){animation-range:entry 14% entry 62%}
  /* the diagonal hatching in the statement band drifts as you pass it */
  .stmt::before{animation:diag-drift linear both;animation-timeline:view()}
  /* The scene falls back as you leave it, so the page reads as depth rather
     than two stacked screens. Done by fading an overlay rather than filtering
     .scene, which would re-filter the rain canvas on every one of its frames. */
  .scene::after{animation:scene-veil linear both;animation-timeline:view();
    animation-range:exit 0% exit 100%}
}
@supports (animation-timeline:view()){
  .reveal{animation:rise-in linear both;animation-timeline:view();
    animation-range:entry 0% entry 55%}
}

/* Every effect above is decoration. With reduced motion the scene is
   still a scene — the rain stops, the sweeps stop, nothing moves, and
   the page reads exactly the same. */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation:none!important;transition:none!important}
  .progress{display:none}
  .reveal{opacity:1!important;transform:none!important}
  .glitch::before,.glitch::after{display:none}
  .scan::after{display:none}
}

/* The sun sat under the h1 between roughly 760 and 860px — white on #ffd36b
   is 1.42:1. Move it out of the text column earlier. */
@media(max-width:880px){
  .sun{right:-6%;width:min(17rem,42vw)}
}
@media(max-width:700px){
  .top{padding:1.1rem 0}
  .top .wrap{flex-direction:column;align-items:flex-start;gap:.5rem}
  .top nav{flex-wrap:wrap;gap:.5rem .9rem;font-size:.76rem}
  .hero{padding:8.5rem 0 6.5rem}
  .sun{right:-8%;top:7%;width:min(15rem,52vw)}
  .grid{height:44%}
  .hero h1{max-width:100%}
}

/* Visible focus on the dark ground in every browser, not just Chrome's default. */
:focus-visible{outline:2px solid var(--cyan);outline-offset:3px;border-radius:2px}
/* Nav links were 54x21; WCAG 2.2 wants 24px minimum and Android 48dp. */
.top nav a{padding:.45rem 0;display:inline-block}

/* ================= FALLBACK ==================================
   Scroll-driven CSS animation is not everywhere yet. Where it is
   missing, every @supports block above is skipped and the page has
   NO motion at all — no reveals, no parallax, no progress rule.
   The script adds .js-scroll only in that case and feeds the same
   values in from a passive scroll listener.

   Nothing below hides anything unless .js-scroll is present, so a
   browser with JS disabled gets the full page, just static.
   ============================================================= */
.js-scroll .progress{transform:scaleX(var(--pg,0))}
.js-scroll #rain,
.js-scroll .sun      {transform:translate3d(0,calc(var(--sp,0) * -5%),0)}
.js-scroll .grid,
.js-scroll .city-far {transform:translate3d(0,calc(var(--sp,0) * -13%),0)}
.js-scroll .city-near{transform:translate3d(0,calc(var(--sp,0) * -26%),0)}
.js-scroll .scene::after{opacity:calc(var(--sp,0) * .6)}

.js-scroll .reveal,
.js-scroll .line,
.js-scroll .facts>div{opacity:0;transform:translateY(1.6rem);
  transition:opacity .55s ease,transform .55s cubic-bezier(.2,.8,.2,1)}
.js-scroll .head{opacity:0;transform:translateX(-1.4rem);
  transition:opacity .55s ease,transform .55s cubic-bezier(.2,.8,.2,1)}
.js-scroll .work .card:nth-child(2){transition-delay:.08s}
.js-scroll .work .card:nth-child(3){transition-delay:.16s}
.js-scroll .is-in{opacity:1;transform:none}

@media (prefers-reduced-motion:reduce){
  .js-scroll .reveal,.js-scroll .line,.js-scroll .facts>div,.js-scroll .head{
    opacity:1;transform:none;transition:none}
}
