/* 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}}

/* --- 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}
@keyframes win-flick{
  0%,44%{opacity:.55} 45%,52%{opacity:.12}
  53%,80%{opacity:.75} 81%,88%{opacity:.25} 89%,100%{opacity:.6}}

/* --- 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}
.brand{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 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}
}
