/* ==========================================================================
   Two layouts from one document.

   With WebGL (html[data-gl="on"]) the sections are transparent scroll drivers:
   they give the page its height, and their cards float over a fixed canvas,
   one visible at a time.

   Without it (html[data-gl="off"], set by main.js when the context fails, and
   the default under <noscript>) the exact same markup renders as a plain dark
   document. There is no second page and no second copy of the text: the canvas
   is decorative, the DOM is the site.
   ========================================================================== */

@font-face{font-family:'Geist';font-style:normal;font-weight:100 900;font-display:swap;
  src:url('../fonts/Geist-var.woff2') format('woff2')}
@font-face{font-family:'Geist Mono';font-style:normal;font-weight:100 900;font-display:swap;
  src:url('../fonts/GeistMono-var.woff2') format('woff2')}

:root{
  --bg:#05070a; --panel:#0b1016; --line:#182430; --line-strong:#223344;
  --fg:#dfe9f2; --muted:#7d8ea1; --accent:#22e3d0; --warn:#ff2e6e;
  --mono:'Geist Mono',ui-monospace,SFMono-Regular,Menlo,monospace;
  --sans:'Geist',ui-sans-serif,system-ui,sans-serif;
  --ease:cubic-bezier(.16,1,.3,1);
  --pad:clamp(18px,4vw,44px);
  color-scheme:dark;
}

*,*::before,*::after{box-sizing:border-box}
html{scroll-behavior:smooth; -webkit-text-size-adjust:100%}
body{
  margin:0; background:var(--bg); color:var(--fg);
  font:400 16px/1.6 var(--mono);
  overflow-x:hidden;
  padding-left:env(safe-area-inset-left); padding-right:env(safe-area-inset-right);
}
h1,h2{margin:0; font-weight:500; letter-spacing:-.02em; line-height:1.08}
p,ul{margin:0}
a{color:inherit}

/* --- the canvas ---------------------------------------------------------- */
#world{
  position:fixed; inset:0; width:100%; height:100%;
  display:block; z-index:0; background:var(--bg);
}
/* A CSS vignette on top of the shader one. The shader darkens the corners of the render;
   this darkens behind the copy specifically, which is what keeps body text legible when
   the camera flies past something bright.
   
   It follows the card's own strength. There is nothing to protect while no card is on
   screen, and dimming the scene then is dimming the thing the reader came for — which is
   as true of the left column on a laptop as it is of the whole screen on a phone. */
#vignette{
  position:fixed; inset:0; z-index:1; pointer-events:none;
  opacity:var(--copy-in, 0);
  background:
    linear-gradient(90deg, rgba(5,7,10,.92) 0%, rgba(5,7,10,.55) 28%, rgba(5,7,10,0) 62%),
    linear-gradient(0deg, rgba(5,7,10,.75) 0%, rgba(5,7,10,0) 38%);
}
[data-copy="off"] #vignette{opacity:0}
/* On a phone the copy spans the full width instead of sitting in a left column, so the
   scrim has to run the other way: dark under the text at the top, clear over the scene
   below it. The horizontal gradient would darken the wrong half. */
@media (max-aspect-ratio: 85/100){
  #vignette{
    background:linear-gradient(180deg,
      rgba(5,7,10,.94) 0%, rgba(5,7,10,.88) 42%, rgba(5,7,10,.35) 62%, rgba(5,7,10,0) 78%);
    /* And only while there is text to protect. On a wide screen the scrim sits in the
       left column where the copy always lives, so it can stay. Here it covers the top of
       the scene, and the scene is most of what a phone has — dimming it when nothing is
       written over it is dimming the thing the reader came for. */
  }

  [data-gl="on"] .beat:not(.beat--end) .beat__card{right:var(--pad); max-width:none}
  /* The last card stays sticky everywhere — it is taller than a phone and has to be
     scrolled through. Pinning it to the bottom of its own section put it thousands of
     pixels above the end of the page. */
  .beat--end .tools li{padding:12px 0}
  .beat--end .contact{margin-top:16px; padding:16px}


}
[data-gl="off"] #world,[data-gl="off"] #vignette{display:none}

/* --- chrome -------------------------------------------------------------- */
.skip{
  position:fixed; top:8px; left:8px; z-index:60; transform:translateY(-200%);
  background:var(--accent); color:#03211d; padding:10px 14px; border-radius:4px;
  font-size:13px; text-decoration:none;
}
.skip:focus{transform:none}

.hud{
  position:fixed; top:0; left:0; right:0; z-index:40;
  display:flex; align-items:center; gap:clamp(12px,3vw,32px);
  padding:14px var(--pad); padding-top:calc(14px + env(safe-area-inset-top));
  font-size:12.5px; letter-spacing:.08em; text-transform:uppercase;
  background:linear-gradient(180deg, rgba(5,7,10,.9), rgba(5,7,10,0));
}
.hud__mark{text-decoration:none; display:flex; align-items:baseline; gap:6px; flex:none}
.hud__mark b{font-weight:500; letter-spacing:.14em}
.hud__mark span{color:var(--accent); font-size:10.5px}
.hud__nav{display:flex; gap:clamp(8px,1.6vw,20px); overflow-x:auto; scrollbar-width:none}
.hud__nav::-webkit-scrollbar{display:none}
.hud__nav a{
  color:var(--muted); text-decoration:none; white-space:nowrap;
  padding-bottom:3px; border-bottom:1px solid transparent; transition:color .3s var(--ease);
}
.hud__nav a:hover,.hud__nav a:focus-visible{color:var(--fg)}
.hud__nav a.on{color:var(--accent); border-bottom-color:var(--accent)}
.hud__alt{margin-left:auto; color:var(--muted); text-decoration:none; flex:none}
.hud__alt:hover{color:var(--accent)}
@media (max-width:760px){ .hud__nav{display:none} }

/* Scroll progress, which doubles as the only affordance telling you the page is long. */
.rail{position:fixed; top:0; left:0; right:0; height:2px; z-index:41; background:rgba(34,227,208,.12)}
.rail i{display:block; height:100%; width:0; background:var(--accent); box-shadow:0 0 12px rgba(34,227,208,.8)}
[data-gl="off"] .rail{display:none}

/* --- the beats, WebGL layout --------------------------------------------- */
main{position:relative; z-index:2}

/* A viewport and a quarter per beat. The camera dwells in the middle of each scene
   rather than crossing it at a constant rate (see `_warpScroll`), and giving each beat
   more page to spend makes that dwell long enough to actually read the room in. */
.beat{
  min-height:125svh;
  display:flex; align-items:center;
  padding:calc(64px + env(safe-area-inset-top)) var(--pad) 64px;
}
/* The last beat is deliberately much taller than the others. The camera stops at three
   screens there and types out a paragraph on each, and reading takes scroll: at one
   viewport the whole tour went past in a flick. The engine is told about this too —
   `meta.pace` on the scene gives it a matching share of the flight — and the two have to
   move together, or the copy drifts away from the scene it describes. */
.beat--end{min-height:700svh; align-items:flex-start}

.beat__card{
  /* 46ch of this mono face is wider than a phone, so the ch cap has to yield to the
     viewport or the body copy runs off the right edge. */
  max-width:min(46ch, 100%); display:grid; gap:14px;
  will-change:opacity, transform;
}
/* A card is pinned to the viewport, not stuck inside its own section.
   
   Sticky only holds while its section is still in range, and every card outlives that
   window: measured on a phone, a card was cut off the top of the screen by anywhere
   from 60 to 1100 pixels by the time it was shown, and on a wide screen by 460. The
   mobile delay makes it worse because it shows a card later still, but the fault is
   structural and both layouts had it.
   
   Pinning is only a change of position, not of source: the text stays exactly where it
   is in the document, which is what the no-WebGL path and every screen reader read. */
[data-gl="on"] .beat__card{
  position:sticky; top:clamp(96px, 24svh, 220px);
  opacity:0;
}
[data-gl="on"] .beat:not(.beat--end) .beat__card{
  position:fixed; left:var(--pad); top:50%;
}
[data-gl="on"] .beat{align-items:flex-start}

/* No transition here on purpose. The rise and fall are driven per frame from the reader's
   own position (see `envelope` in main.js), so a card moves exactly as far as the page was
   scrolled and stops the instant scrolling does. A CSS transition on top of that would be
   a second, slower animation fighting the first. */

/* The three tool descriptions are dropped from the last card. The terminals in the scene
   have just typed those exact words one screen at a time, so repeating them buys nothing
   and it is what pushed this card past the height of the screen — on a phone and on a
   laptop both. The names and the links stay, and the full text is still in the markup for
   the no-WebGL path, a crawler and a screen reader. */
[data-gl="on"] .beat--end .tools span{display:none}
/* A small offset, not the generous one the short cards use: this card is nearly the
   height of the screen and every pixel above it is a pixel of its own text below the
   fold. */
[data-gl="on"] .beat--end .beat__card{top:12px; gap:11px}
[data-gl="on"] .beat--end .contact{margin-top:14px}
/* No padding under the last card. When sticky lets go, the card rests at the bottom of
   its section's content box, so every pixel below it pushes the card further above the
   end of the page — where there is no scroll left to recover it. Adding room here made
   the clipping worse, not better, which is the opposite of the intuition. */
[data-gl="on"] .beat--end{padding-bottom:0}

.eyebrow{
  font-size:11.5px; letter-spacing:.22em; text-transform:uppercase; color:var(--accent);
  display:flex; align-items:center; gap:10px;
}
.eyebrow::after{content:""; flex:1; height:1px; background:linear-gradient(90deg,var(--line-strong),transparent)}

h1{font-size:clamp(42px,8.5vw,84px)}
h2{font-size:clamp(26px,4.4vw,44px); max-width:18ch}
.role{color:var(--accent); font-size:clamp(14px,2vw,17px)}
.body{color:var(--muted); font-size:15px; line-height:1.7}
.meta{color:var(--line-strong); font-size:12px; letter-spacing:.06em}
.hint{color:var(--line-strong); font-size:12px; letter-spacing:.14em; text-transform:uppercase}
[data-gl="off"] [data-gl-only]{display:none}

.pills{display:flex; flex-wrap:wrap; gap:8px; list-style:none; padding:0}
.pills li{
  font-size:12px; padding:5px 11px; border:1px solid var(--line-strong); border-radius:999px;
  color:var(--fg); background:rgba(11,16,22,.6); backdrop-filter:blur(6px);
}

/* --- the workshop bench -------------------------------------------------- */
.tools{list-style:none; padding:0; display:grid; gap:2px; margin-top:8px}
.tools li{
  display:grid; gap:6px; padding:16px 0; border-top:1px solid var(--line);
  background:rgba(5,7,10,.45);
}
.tools b{font-weight:500; color:var(--accent); font-size:14.5px}
.tools span{color:var(--muted); font-size:13.5px; line-height:1.6}
.tools a{
  display:inline-block; margin-right:14px; font-size:12px; color:var(--fg);
  text-decoration:underline; text-decoration-color:var(--accent); text-underline-offset:4px;
}

.contact{
  margin-top:22px; padding:20px; display:grid; gap:12px;
  border:1px solid var(--line-strong); border-radius:6px; background:rgba(11,16,22,.72);
  backdrop-filter:blur(10px);
}
.contact__links{display:flex; flex-wrap:wrap; gap:8px 18px; font-size:13px}
.contact__links a{color:var(--fg); text-decoration:underline; text-decoration-color:var(--accent); text-underline-offset:4px}
.contact__links a:hover{color:var(--accent)}

footer{
  position:relative; z-index:2;
  display:flex; flex-wrap:wrap; gap:10px 24px; justify-content:space-between;
  padding:26px var(--pad) calc(34px + env(safe-area-inset-bottom));
  border-top:1px solid var(--line); color:var(--line-strong); font-size:12px;
  background:var(--bg);
}

/* --- no-WebGL layout ------------------------------------------------------ */
/* Same markup, read as a document. Cards get a surface, beats stop being full-height
   held frames, and the whole thing behaves like an ordinary dark CV page. */
[data-gl="off"] .beat{min-height:0; padding:clamp(32px,6vw,64px) var(--pad); display:block}
[data-gl="off"] .beat + .beat{border-top:1px solid var(--line)}
[data-gl="off"] .beat__card{max-width:70ch; opacity:1; transform:none}
[data-gl="off"] body{background:var(--bg)}

/* --- reduced motion ------------------------------------------------------- */
/* The engine already drops damping, glitch and most of the aberration when this is
   set. Here we only stop the DOM from animating on top of it. */
@media (prefers-reduced-motion: reduce){
  html{scroll-behavior:auto}
  [data-gl="on"] .beat__card{transition:none; opacity:1; transform:none}
}

/* --- auto scroll ---------------------------------------------------------- */
/* Sits below the header so it never collides with the CLASSIC link, and stays
   quiet until it is wanted: 20% opacity at rest, full on hover or focus. */
.auto{
  position:fixed; z-index:42;
  top:calc(64px + env(safe-area-inset-top)); right:var(--pad);
  display:grid; justify-items:end;
  /* The panel is absolutely positioned under the button rather than stacked in the
     grid, so revealing it cannot shift the button it is anchored to. */
  padding-bottom:64px;   /* keeps the hover target alive across the gap to the panel */
}
[data-gl="off"] .auto{display:none}

.auto__btn{
  display:flex; align-items:center; gap:8px;
  padding:9px 14px; border-radius:999px; cursor:pointer;
  border:1px solid var(--line-strong); background:rgba(11,16,22,.72);
  backdrop-filter:blur(8px); color:var(--fg);
  font:500 11.5px/1 var(--mono); letter-spacing:.14em; text-transform:uppercase;
  opacity:.2;
  /* Two transforms on one element would fight: the float lives on a keyframe and the
     hover scale is a custom property the keyframe multiplies in. */
  --scale:1;
  animation:auto-float 4.2s ease-in-out infinite;
  transition:opacity .35s var(--ease), border-color .35s var(--ease),
             box-shadow .35s var(--ease), --scale .3s var(--ease);
}
.auto__btn b{font-weight:500; color:var(--accent)}
.auto__btn:hover,.auto__btn:focus-visible{
  opacity:1; --scale:1.2; border-color:var(--accent);
  box-shadow:0 0 0 1px rgba(34,227,208,.3), 0 0 26px -8px rgba(34,227,208,.75);
}
.auto__btn[aria-pressed="true"]{opacity:1; border-color:var(--accent)}

.auto__dot{
  width:7px; height:7px; border-radius:50%; flex:none;
  background:var(--line-strong); transition:background .3s var(--ease);
}
.auto__btn[aria-pressed="true"] .auto__dot{
  background:var(--accent); box-shadow:0 0 10px var(--accent);
  animation:auto-pulse 1.6s ease-in-out infinite;
}

/* `--scale` needs a type or it interpolates as a discrete string and the hover jumps. */
@property --scale{syntax:'<number>'; inherits:false; initial-value:1}

@keyframes auto-float{
  0%,100%{transform:translateY(0) scale(var(--scale))}
  50%    {transform:translateY(-5px) scale(var(--scale))}
}
@keyframes auto-pulse{
  0%,100%{opacity:1} 50%{opacity:.35}
}

/* The speed panel, which only exists while auto scroll is running. */
/* Only present while auto scroll is running, and only visible while the pointer is on
   the control or the panel itself. It slides back up under the button when the pointer
   leaves, so the resting state stays a single quiet pill. */
.auto__panel{
  position:absolute; top:100%; right:0; margin-top:10px;
  display:grid; grid-template-columns:auto 1fr auto; align-items:center; gap:12px;
  padding:12px 16px; border-radius:12px; width:max-content;
  border:1px solid var(--line-strong); background:rgba(11,16,22,.82);
  backdrop-filter:blur(10px);
  font:400 11px/1 var(--mono); letter-spacing:.12em; text-transform:uppercase;
  color:var(--muted);
  opacity:0; pointer-events:none; transform:translateY(-16px) scale(.94);
  transition:opacity .3s var(--ease), transform .34s var(--ease);
}
.auto__panel[hidden]{display:none}
/* focus-within keeps it open for keyboard use of the slider, where there is no pointer
   to hover with. */
.auto:hover .auto__panel,
.auto:focus-within .auto__panel{
  opacity:1; pointer-events:auto; transform:none;
  animation:auto-float 5.4s ease-in-out infinite .4s;
}
.auto__panel output{color:var(--accent); min-width:4ch; text-align:right}

.auto__panel input[type="range"]{
  -webkit-appearance:none; appearance:none;
  width:130px; height:2px; border-radius:2px; background:var(--line-strong);
  cursor:pointer; margin:0;
}
.auto__panel input[type="range"]::-webkit-slider-thumb{
  -webkit-appearance:none; appearance:none;
  width:13px; height:13px; border-radius:50%; border:0;
  background:var(--accent); box-shadow:0 0 10px -1px rgba(34,227,208,.9);
}
.auto__panel input[type="range"]::-moz-range-thumb{
  width:13px; height:13px; border-radius:50%; border:0;
  background:var(--accent); box-shadow:0 0 10px -1px rgba(34,227,208,.9);
}
.auto__panel input[type="range"]:focus-visible{outline:2px solid var(--accent); outline-offset:6px}

@media (max-width:600px){
  .auto__panel input[type="range"]{width:96px}
}

/* Reduced motion keeps the control and drops the drift; the button still reveals on
   hover, it just no longer bobs. */
@media (prefers-reduced-motion: reduce){
  .auto__btn,.auto__panel{animation:none}
  .auto__btn:hover,.auto__btn:focus-visible{transform:scale(1.2)}
  .auto__btn[aria-pressed="true"] .auto__dot{animation:none}
}

/* --- unpublished-edit marker ---------------------------------------------- */
/* Only ever present in the author's own browser, where the scene editor has left a
   local copy of an edit. A preview that looks exactly like the published site is a trap:
   sooner or later you screenshot an unpublished change and think it shipped. */
.preview-mark{
  position:fixed; left:var(--pad); bottom:14px; z-index:45;
  padding:7px 13px; border-radius:999px; text-decoration:none;
  border:1px solid var(--warn, #ff2e6e); background:rgba(5,7,10,.86);
  backdrop-filter:blur(8px); color:#ff2e6e;
  font:400 10.5px/1 var(--mono); letter-spacing:.16em; text-transform:uppercase;
}
.preview-mark:hover{background:rgba(255,46,110,.14)}
