<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no" />
<title>NOUS · pretext</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500&display=block" rel="stylesheet">
<style>
  :root { color-scheme: dark; }

  :root {
    --background:       color-mix(in srgb, #041C1C 100%, transparent);
    --background-base:  #041C1C;
    --background-alpha: 1;
    --background-blend: difference;
    --midground:        color-mix(in srgb, #ffe6cb 100%, transparent);
    --midground-base:   #ffe6cb;
    --midground-alpha:  1;
    --foreground:       color-mix(in srgb, #ffffff 0%, transparent);
    --foreground-base:  #ffffff;
    --foreground-alpha: 0;
    --foreground-blend: difference;
  }

  html, body {
    margin: 0;
    width: 100%;
    min-height: 100vh;
    overflow: hidden;
    background: #000;
  }

  /* literally center the composition in 100vh */
  body {
    display: flex;
    align-items: center;
    justify-content: center;
  }

  #stage {
    position: relative;
    z-index: 2;
    width: min(1240px, 100vw);
    height: min(720px, 100vh);
    cursor: grab;
    overflow: visible;
  }
  #stage:active { cursor: grabbing; }

  #c,
  #orbCanvas {
    display: block;
    position: absolute;
    left: calc((100vw - min(1240px, 100vw)) / -2);
    top: calc((100vh - min(720px, 100vh)) / -2);
    width: 100vw;
    height: 100vh;
    pointer-events: none;
  }
  #c { z-index: 3; }
  #orbCanvas {
    z-index: 4;
    opacity: 1;
  }
  #noiseCanvas {
    display: block;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
  }
  #textLayer {
    position: absolute;
    inset: 0;
    z-index: 2;
    color: var(--midground-base);
    font: 400 10px/15px "Geist Mono", "JetBrains Mono", "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
    pointer-events: auto;
    user-select: text;
    -webkit-user-select: text;
  }
  #textLayer *::selection {
    background: var(--selection-bg, var(--midground));
    color: var(--background-base);
  }
  .flow-line {
    position: absolute;
    height: 20px;
    white-space: pre;
    overflow: visible;
    color: var(--midground-base);
  }
  #annotationLayer {
    position: absolute;
    inset: 0;
    z-index: 4;
    pointer-events: none;
    overflow: visible;
  }
  .annot {
    position: absolute;
    pointer-events: none;
    will-change: opacity, transform;
  }
  .tok-keyword { color: color-mix(in srgb, var(--midground-base) 54%, #ff174d); }
  .tok-string  { color: color-mix(in srgb, var(--midground-base) 52%, #ff6b3d); }
  .tok-comment { color: color-mix(in srgb, var(--midground-base) 42%, #7f1d1d); }
  .tok-number  { color: color-mix(in srgb, var(--midground-base) 50%, #ff2a6d); }
  .tok-const   { color: color-mix(in srgb, var(--midground-base) 48%, #ff003c); }

  .layer { position: fixed; inset: 0; pointer-events: none; }
  .bg-lens   { z-index: 1; background-color: var(--background); mix-blend-mode: var(--background-blend); }
  .vignette  { z-index: 99; background: radial-gradient(ellipse at 0% 0%, rgba(255,189,56,0) 60%, rgba(255,189,56,0.35) 100%); mix-blend-mode: lighten; opacity: 0.22; }
  .fg-lens   { z-index: 100; background-color: var(--foreground); mix-blend-mode: var(--foreground-blend); }
  .noise {
    z-index: 101;
    mix-blend-mode: difference;
  }
  #noiseCanvas {
    width: 100%;
    height: 100%;
    display: block;
  }
  .terminal-cursor {
    position: absolute;
    width: 0.6em;
    height: 1em;
    background: #fff;
    z-index: 6;
    pointer-events: none;
    mix-blend-mode: difference;
    animation: term-blink 1s steps(1, end) infinite;
  }
  @keyframes term-blink {
    0%, 60% { opacity: 1; }
    60.01%, 100% { opacity: 0; }
  }
  .cursor-anchor {
    display: inline-block;
    width: 0;
    height: 1em;
    vertical-align: text-top;
    pointer-events: none;
  }

  @font-face {
    font-family: "Mondwest";
    src: url("https://esm.sh/@nous-research/ui@0.4.0/dist/fonts/Mondwest-Regular.woff2") format("woff2");
    font-weight: 400; font-style: normal; font-display: block;
  }
  @font-face {
    font-family: "Geist Mono";
    src: url("https://cdn.jsdelivr.net/npm/@fontsource/geist-mono@5.2.5/files/geist-mono-latin-400-normal.woff2") format("woff2");
    font-weight: 400; font-style: normal; font-display: block;
  }
  @font-face {
    font-family: "JetBrains Mono";
    src: url("https://cdn.jsdelivr.net/npm/@fontsource/jetbrains-mono@5.2.5/files/jetbrains-mono-latin-400-normal.woff2") format("woff2");
    font-weight: 400; font-style: normal; font-display: block;
  }

  .font-primer { position: fixed; left: -9999px; top: -9999px; font: 400 26px "Mondwest", serif; }
</style>
</head>
<body>
<div id="stage"><div id="textLayer" aria-label="self-learning source stream"></div><div id="terminalCursor" class="terminal-cursor" hidden></div><canvas id="c"></canvas><canvas id="orbCanvas"></canvas></div>
<div class="layer bg-lens"></div>
<div class="layer vignette"></div>
<div class="layer fg-lens"></div>
<div class="layer noise"><canvas id="noiseCanvas"></canvas></div>
<div class="font-primer" aria-hidden="true">Aa Mondwest priming glyphs 0123456789</div>

<script type="module">
import {
  prepareWithSegments,
  layoutNextLineRange,
  materializeLineRange,
} from "https://esm.sh/@chenglou/pretext@0.0.6";
import gsap from "https://esm.sh/gsap@3.12.5";
import GUI from "https://esm.sh/lil-gui@0.19.2";

const SELF_LEARNING_SOURCE = String.raw`
type ToolId=u16; type FeatureId=u32; type W=f32;
struct Obs{tool:ToolId,reward:f32,lat:u32,feat:&'static[FeatureId],tok:u16}
struct Belief{prior:W,conf:W,last:u64,uses:u32}

#[inline] fn sig(x:f32)->f32{1.0/(1.0+(-x).exp())}
#[inline] fn decay(now:u64,last:u64)->f32{1.0-((now-last)as f32/900.0).min(1.0)*0.22}
#[inline] fn clamp8(x:f32)->f32{x.clamp(-8.0,8.0)}

fn score(theta:&[W],b:&[Belief],o:&Obs)->f32{
    let sparse=o.feat.iter().fold(0.0,|a,id|a+theta[*id as usize]);
    let belief=b[o.tool as usize].prior*b[o.tool as usize].conf.max(0.15);
    sparse+belief-(o.lat as f32*0.00072)-(o.tok as f32*0.00009)
}

fn update(theta:&mut[W],b:&mut[Belief],o:&Obs,now:u64,eta:f32){
    let p=sig(score(theta,b,o));
    let e=(o.reward-p).clamp(-1.0,1.0);
    let step=eta*e/(o.feat.len().max(1)as f32).sqrt();

    for id in o.feat{
        let w=&mut theta[*id as usize];
        *w=clamp8(*w+step-0.0003*w.signum());
    }

    let slot=&mut b[o.tool as usize];
    let d=decay(now,slot.last);
    let r=(o.reward-o.lat as f32*0.00035).clamp(-1.0,1.0);
    slot.prior=slot.prior*d+r*0.14+e*0.06;
    slot.conf=(slot.conf*0.93+r.abs()*0.07).min(1.0);
    slot.uses=slot.uses.saturating_add(1);
    slot.last=now;
}

fn consolidate(trace:&[Obs],theta:&mut[W],b:&mut[Belief],now:u64){
    let eta=0.042/(1.0+(trace.len()as f32*0.003));
    let mut credit=1.0;
    for o in trace.iter().rev(){
        update(theta,b,o,now,eta*credit);
        credit*=0.985;
    }
}

fn choose<'a>(theta:&[W],b:&[Belief],xs:&'a[Obs],temp:f32)->&'a Obs{
    xs.iter().max_by(|a,bx|{
        let sa=score(theta,b,a)/temp.max(0.05);
        let sb=score(theta,b,bx)/temp.max(0.05);
        sa.partial_cmp(&sb).unwrap()
    }).unwrap()
}
`;
const CODE_CHUNK = SELF_LEARNING_SOURCE
  .trim()
  .replace(/\t/g, "  ")
  .replace(/\n{3,}/g, "\n\n");
const STREAM_REPEAT_COUNT = 6;
const streamCorpus = Array.from({ length: STREAM_REPEAT_COUNT }, () => CODE_CHUNK).join("\n\n");

const stage = document.getElementById("stage");
const textLayer = document.getElementById("textLayer");
const canvas = document.getElementById("c");
const orbCanvas = document.getElementById("orbCanvas");
const ctx = canvas.getContext("2d", { alpha: true });
const orbCtx = orbCanvas.getContext("2d", { alpha: true });
let W = 0, H = 0, DPR = 1;
let canvasLeft = 0, canvasTop = 0, canvasW = 0, canvasH = 0;

let BODY_FONT = '400 10px "Geist Mono", "JetBrains Mono", "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
let BODY_LINE_H = 15;
let ASCII_FONT = '8px monospace';
let CELL_W = 4.8;
let CELL_H = 8;
const CHARS = ' ·:;+=░▒▓█';
const EMPTY = 0;
const CHARS_LAST = CHARS.length - 1;

const LOGO = ['N', 'O', 'U', 'S'].map((letter, i) => ({
  letter,
  rows: [
    i === 0
      ? '██░░░██,██░░░██,███░░██,█░██░██,█░░████,█░░░███,██░░░██,██░░░██,██░░░██'
      : i === 1
        ? '░█████░,██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,░█████░'
        : i === 2
          ? '██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,██░░░██,░█████░'
          : '░█████░,██░░░██,██░░░░░,░████░░,░░░░██░,░░░░░██,██░░░██,██░░░██,░█████░'
  ][0].split(',')
}));

function hash(x, y) {
  const n = Math.sin(x * 127.1 + y * 311.7) * 43758.5453;
  return n - Math.floor(n);
}
function charAt(t) {
  return CHARS[Math.min(CHARS.length - 1, Math.max(0, Math.floor(t * CHARS.length)))];
}
function gridDist(v, step) {
  return Math.abs(v / step - Math.round(v / step)) * step;
}
function rot(a, b, angle) {
  const c = Math.cos(angle), s = Math.sin(angle);
  return [a * c - b * s, a * s + b * c];
}
function hexToRGB(h) {
  const v = parseInt(h.slice(1), 16);
  return [(v >> 16) & 255, (v >> 8) & 255, v & 255];
}

let fontsReady = false;
let bodyPrepared = null;
function rebuildLayouts() {
  bodyPrepared = prepareWithSegments(streamCorpus, BODY_FONT, { whiteSpace: "pre-wrap" });
}

const LENS_0 = { bgBlend: "difference", bgColor: "#041C1C", bgOpacity: 1, mgColor: "#ffe6cb", mgOpacity: 1, fgBlend: "difference", fgColor: "#FFFFFF", fgOpacity: 0 };
const LENS_5I = { bgBlend: "multiply", bgColor: "#ffffff", bgOpacity: 1, mgColor: "#fffff5", mgOpacity: 1, fgBlend: "difference", fgColor: "#041a1f", fgOpacity: 1 };
const LENS = { current: null, mgBase: null, depthColors: null };
function colorMix(color, alpha) { return `color-mix(in srgb, ${color} ${Math.round(alpha * 100)}%, transparent)`; }
function precomputeDepthColors(mgColor) {
  const [r, g, b] = hexToRGB(mgColor);
  return Array.from({ length: 64 }, (_, i) => {
    const depth = i / 63;
    const m = 0.4 + depth * 0.6;
    return `rgba(${(r * m) | 0},${(g * m) | 0},${(b * m) | 0},${depth})`;
  });
}
function applyLens(preset) {
  LENS.current = preset;
  LENS.mgBase = preset.mgColor;
  LENS.depthColors = precomputeDepthColors(preset.mgColor);
  const s = document.documentElement.style;
  for (const [name, color, alpha] of [["foreground", preset.fgColor, preset.fgOpacity], ["midground", preset.mgColor, preset.mgOpacity], ["background", preset.bgColor, preset.bgOpacity]]) {
    s.setProperty(`--${name}`, colorMix(color, alpha));
    s.setProperty(`--${name}-base`, color);
    s.setProperty(`--${name}-alpha`, `${alpha}`);
  }
  s.setProperty("--background-blend", preset.bgBlend);
  s.setProperty("--foreground-blend", preset.fgBlend);
}
applyLens(LENS_0);
addEventListener("keydown", e => { if (e.key === "x" || e.key === "X") applyLens(LENS.current === LENS_0 ? LENS_5I : LENS_0); });
const SELECTION_COLORS = [
  "oklch(85% 0.12 330)",
  "oklch(85% 0.12 300)",
  "oklch(85% 0.12 270)",
  "oklch(85% 0.12 230)",
  "oklch(85% 0.12 180)",
  "oklch(85% 0.12 150)",
  "oklch(85% 0.12 120)",
  "oklch(85% 0.12 90)",
  "oklch(85% 0.12 60)",
  "oklch(85% 0.12 30)",
  "oklch(88% 0.10 80)",
];
let selectionIdx = 0;
function cycleSelectionColor() {
  requestAnimationFrame(() => {
    document.documentElement.style.setProperty("--selection-bg", SELECTION_COLORS[(selectionIdx = (selectionIdx + 1) % SELECTION_COLORS.length)]);
  });
}
document.addEventListener("selectstart", cycleSelectionColor);
addEventListener("keydown", e => {
  if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a") {
    cycleSelectionColor();
    e.preventDefault();
    const selection = getSelection();
    const range = document.createRange();
    range.selectNodeContents(textLayer);
    selection.removeAllRanges();
    selection.addRange(range);
  }
});

let asciiMask, asciiChar, asciiGlyph, orbMask, orbChar, orbGlyph, obstacleRows, intervals;
let cols = 0, rows = 0;
let canvasCols = 0, canvasRows = 0;
let lastFrameTime = 0;
const drawnLines = { x: [], y: [], w: [], text: [], count: 0 };
let lineNodes = [];
let terminalAnchorEl = null;
let terminalLineEl = null;
let orbShapeControl = null;
let autoCubeCall = null;
let lastTextSignature = "";
const ORB = {
  panX: 0, panY: 0,
  orbitX: -1.42, orbitY: 0,
  radius: 0.135,
  zoom: 3,
  cubeScale: 1.8,
  autoSpin: 0.18,
  lat: 19,
  lon: 30,
  wire: 0.2,
  shape: "sphere",
  shapeMix: 0,
  drag: null,
  positioned: false,
  staged: false,
};
const SCENE = {
  orbBuildDuration: 1.45,
};

let nousStartTime = null;
const AUTO_CUBE_AFTER_NOUS_RESOLVE = 0.0;
function setOrbShape(shape, { reveal = false, syncControl = true } = {}) {
  ORB.shape = shape;
  if (syncControl) {
    ctrls.orbShape = shape;
    orbShapeControl?.updateDisplay();
  }
  if (reveal) {
    gsap.killTweensOf(orbCanvas);
    orbCanvas.style.opacity = "1";
  }
  gsap.killTweensOf(ORB, "shapeMix");
  gsap.to(ORB, {
    shapeMix: shape === "cube" ? 1 : 0,
    duration: Math.max(0.01, ctrls.shapeMorphDuration ?? 0.65),
    ease: "power2.inOut",
    onUpdate: () => { lastTextSignature = ""; },
  });
  lastTextSignature = "";
}
function sceneTimeNow() {
  return (typeof performance !== "undefined" ? performance.now() : Date.now()) * 0.001 - sceneEpoch;
}
function armNous(atTime = sceneTimeNow()) {
  if (nousStartTime !== null) return;
  const startAt = Math.max(0, atTime);
  nousStartTime = startAt;
  const delay = Math.max(0, startAt - sceneTimeNow()) + Math.max(0, ctrls.orbContainNous ?? 1.0);
  gsap.killTweensOf(orbCanvas);
  gsap.to(orbCanvas, {
    opacity: 0,
    duration: Math.max(0.05, ctrls.orbFadeDuration ?? 0.7),
    delay,
    ease: "power2.out",
  });
  autoCubeCall?.kill();
  const nousResolveAt = startAt + (LOGO.length - 1) * 0.4 + 1.0;
  const cubeAt = nousResolveAt + AUTO_CUBE_AFTER_NOUS_RESOLVE;
  autoCubeCall = gsap.delayedCall(Math.max(0, cubeAt - sceneTimeNow()), () => {
    setOrbShape("cube", { reveal: true, syncControl: false });
  });
}
function orbBuildStartTime() {
  return Math.max(0, ctrls.orbLead ?? 0);
}
function codeTypingStartTime() {
  return Math.max(0, orbBuildStartTime() + SCENE.orbBuildDuration * 0.62 - 0.3);
}

function logoCenterPan() {
  const aspect = cols / rows / 2;
  const h = 0.38;
  const w = h * 2.6 * 0.55;
  const halfStageX = (w / 2 / aspect) * W;
  const centerX = W * 0.5;
  const centerY = H * 0.5;
  return {
    panX: centerX / W - 0.5,
    panY: centerY / H - 0.5,
    left: centerX - halfStageX,
    right: centerX + halfStageX,
  };
}

function startSceneTimeline() {
  if (ORB.staged || !cols || !rows || !W || !H) return;
  ORB.staged = true;
  const now = sceneTimeNow();
  const startAt = Math.max(0, orbBuildStartTime() + SCENE.orbBuildDuration * 0.72 - now);
  gsap.timeline({ delay: startAt })
    .to(ORB, {
      orbitX: 0.55,
      orbitY: `+=${Math.PI * 1.85}`,
      duration: 2.4,
      ease: "power3.inOut",
      onUpdate: () => { lastTextSignature = ""; },
    }, "<")
    .to(ORB, {
      radius: 0.125,
      duration: 2.4,
      ease: "power3.inOut",
      onUpdate: () => { lastTextSignature = ""; },
    }, "<")
    .to(ORB, {
      wire: ctrls.orbWireLow,
      duration: ctrls.wireDropDuration,
      ease: "power2.inOut",
      onUpdate: () => { lastTextSignature = ""; },
      onComplete: () => {
        armNous(sceneTimeNow() + Math.max(0, ctrls.nousAfterWire));
      },
    });
}

function resize() {
  const r = stage.getBoundingClientRect();
  W = Math.max(1, Math.floor(r.width));
  H = Math.max(1, Math.floor(r.height));
  canvasLeft = r.left;
  canvasTop = r.top;
  canvasW = Math.max(1, window.innerWidth);
  canvasH = Math.max(1, window.innerHeight);
  DPR = Math.min(window.devicePixelRatio || 1, 2);
  canvas.width = Math.floor(canvasW * DPR);
  canvas.height = Math.floor(canvasH * DPR);
  orbCanvas.width = Math.floor(canvasW * DPR);
  orbCanvas.height = Math.floor(canvasH * DPR);
  ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
  orbCtx.setTransform(DPR, 0, 0, DPR, 0, 0);
  ctx.font = ASCII_FONT;
  orbCtx.font = ASCII_FONT;
  CELL_W = ctx.measureText("M").width || CELL_W;
  cols = Math.ceil(W / CELL_W);
  rows = Math.ceil(H / CELL_H);
  canvasCols = Math.ceil(canvasW / CELL_W);
  canvasRows = Math.ceil(canvasH / CELL_H);
  asciiMask = new Uint8Array(canvasCols * canvasRows);
  asciiChar = new Uint8Array(canvasCols * canvasRows);
  asciiGlyph = new Array(canvasCols * canvasRows);
  orbMask = new Uint8Array(canvasCols * canvasRows);
  orbChar = new Uint8Array(canvasCols * canvasRows);
  orbGlyph = new Array(canvasCols * canvasRows);
  obstacleRows = Array.from({ length: rows }, () => []);
  intervals = new Float32Array(64);
  if (!ORB.positioned && W > 100 && H > 100) {
    ORB.panX = 0;
    ORB.panY = 0;
    ORB.positioned = true;
  }
  startSceneTimeline();
}
addEventListener("resize", () => { lastTextSignature = ""; resize(); });
function orbCenter() {
  const rNorm = ORB.radius * ORB.zoom;
  return {
    x: (0.5 + ORB.panX) * W,
    y: (0.5 + ORB.panY) * H,
    canvasX: (0.5 + ORB.panX) * W + canvasLeft,
    canvasY: (0.5 + ORB.panY) * H + canvasTop,
    r: rNorm * H,
    rNorm,
  };
}

stage.addEventListener("pointerdown", e => {
  const rect = stage.getBoundingClientRect();
  const center = orbCenter();
  const hitScale = ORB.shape === "cube" ? ORB.cubeScale : 1;
  const dx = e.clientX - rect.left - center.x;
  const dy = e.clientY - rect.top - center.y;
  if (Math.hypot(dx, dy) > center.r * 1.35 * hitScale) return;
  e.preventDefault();
  stage.setPointerCapture(e.pointerId);
  ORB.drag = {
    id: e.pointerId,
    mode: (e.ctrlKey || e.metaKey) ? "rotate" : "move",
    dx,
    dy,
    lastX: e.clientX,
    lastY: e.clientY,
  };
});
stage.addEventListener("pointermove", e => {
  if (!ORB.drag || ORB.drag.id !== e.pointerId) return;
  if (ORB.drag.mode === "rotate" || e.ctrlKey || e.metaKey) {
    const deltaX = e.clientX - ORB.drag.lastX;
    const deltaY = e.clientY - ORB.drag.lastY;
    ORB.orbitY += (deltaX / Math.max(1, W)) * Math.PI * 2;
    ORB.orbitX = Math.max(-1.5, Math.min(1.5, ORB.orbitX + (deltaY / Math.max(1, H)) * 4));
    ORB.drag.mode = "rotate";
    ORB.drag.lastX = e.clientX;
    ORB.drag.lastY = e.clientY;
    return;
  }
  const rect = stage.getBoundingClientRect();
  const cx = e.clientX - rect.left - ORB.drag.dx;
  const cy = e.clientY - rect.top - ORB.drag.dy;
  ORB.panX = cx / W - 0.5;
  ORB.panY = cy / H - 0.5;
  lastTextSignature = "";
});
stage.addEventListener("wheel", e => {
  const rect = stage.getBoundingClientRect();
  const center = orbCenter();
  const hitScale = ORB.shape === "cube" ? ORB.cubeScale : 1;
  const dx = e.clientX - rect.left - center.x;
  const dy = e.clientY - rect.top - center.y;
  if (Math.hypot(dx, dy) > center.r * 1.5 * hitScale) return;
  e.preventDefault();
  ORB.zoom *= Math.exp(-e.deltaY * 0.0012);
  ORB.zoom = Math.max(0.25, Math.min(12, ORB.zoom));
  lastTextSignature = "";
}, { passive: false });
stage.addEventListener("pointerup", e => {
  if (ORB.drag?.id === e.pointerId) ORB.drag = null;
});
stage.addEventListener("pointercancel", e => {
  if (ORB.drag?.id === e.pointerId) ORB.drag = null;
});
stage.addEventListener("dblclick", e => {
  const rect = stage.getBoundingClientRect();
  const center = orbCenter();
  const hitScale = ORB.shape === "cube" ? ORB.cubeScale : 1;
  const dx = e.clientX - rect.left - center.x;
  const dy = e.clientY - rect.top - center.y;
  if (Math.hypot(dx, dy) > center.r * 1.45 * hitScale) return;
  e.preventDefault();
  setOrbShape(ORB.shape === "sphere" ? "cube" : "sphere", { reveal: true });
});

function rasterizeNOUS(time) {
  asciiMask.fill(0);
  asciiGlyph.fill("");
  for (const r of obstacleRows) r.length = 0;

  // NOUS uses the same cell-shader/reveal language as bb-ascii's source logo.
  if (nousStartTime === null || time < nousStartTime) {
    mergeObstacleRows();
    return;
  }
  const t = Math.max(0, time - nousStartTime);
  const aspect = cols / rows / 2;
  const h = 0.38;
  const baseW = h * 2.6 * 0.55;
  const letterWidth = 7;
  const baseLetterPitch = 7.6;
  const letterGap = Math.max(0, ctrls.nousLetterSpacing ?? 0);
  const letterAdvance = baseLetterPitch + letterGap;
  const baseLogoUnits = LOGO.length * baseLetterPitch - 0.6;
  const logoUnits = LOGO.length * baseLetterPitch + (LOGO.length - 1) * letterGap - 0.6;
  const w = baseW * (logoUnits / baseLogoUnits);
  const nousRowMin = new Float32Array(rows);
  const nousRowMax = new Float32Array(rows);
  nousRowMin.fill(Infinity);
  nousRowMax.fill(-Infinity);

  for (let y = 0; y < rows; y++) {
    const ny = y / rows - 0.5;
    if (Math.abs(ny) > h / 2) continue;

    for (let x = 0; x < cols; x++) {
      const nx = (x / cols - 0.5) * aspect;
      if (Math.abs(nx) > w / 2) continue;

      const lx = (nx + w / 2) / w;
      const ly = (ny + h / 2) / h;
      const tx = lx * logoUnits;
      const idx = Math.floor(tx / letterAdvance);
      const cx = Math.floor(tx - idx * letterAdvance);
      const cy = Math.floor(ly * 9);

      if (idx < 0 || idx >= LOGO.length || cx >= letterWidth) continue;

      const { letter, rows: glyphRows } = LOGO[idx];
      const row = glyphRows[cy];
      if (!row || cx >= row.length || row[cx] !== '█') continue;

      const localT = Math.max(0, t - idx * 0.4);
      const phase1 = Math.min(1, localT);
      const phase2 = Math.min(1, Math.max(0, localT - 1));
      const reveal = phase1 * 1.5 - hash(cx, cy + idx * 10) * 0.5;

      if (phase1 < 1 && reveal <= 0) continue;

      const morph = phase2 + hash(cx, cy) * 0.3;
      const ch = phase1 < 1
        ? (reveal >= 1 ? letter : charAt(reveal))
        : charAt(morph);
      const drawC = Math.floor((canvasLeft + x * CELL_W + CELL_W * 0.5) / CELL_W);
      const drawR = Math.floor((canvasTop + y * CELL_H + CELL_H * 0.5) / CELL_H);
      let cellIdx = -1;
      if (drawR >= 0 && drawR < canvasRows && drawC >= 0 && drawC < canvasCols) {
        cellIdx = drawR * canvasCols + drawC;
        asciiMask[cellIdx] = 1;
        asciiChar[cellIdx] = Math.max(0, CHARS.indexOf(ch));
        if (CHARS.indexOf(ch) < 0) asciiGlyph[cellIdx] = ch;
      }
      const x0 = x * CELL_W;
      const x1 = x0 + CELL_W;
      if (x0 < nousRowMin[y]) nousRowMin[y] = x0;
      if (x1 > nousRowMax[y]) nousRowMax[y] = x1;
      // bb-ascii turns late morph cells into solid dark cell backgrounds. In
      // this transparent/lensed canvas, a full-block char carries that punch.
      if (cellIdx >= 0 && phase1 >= 1 && morph > 0.8) asciiChar[cellIdx] = CHARS.length - 1;
    }
  }

  // Slightly dilate/smooth the measured NOUS silhouette so surrounding text
  // does not visually "kiss" sharp letter corners (notably on the S).
  const smoothMin = new Float32Array(rows);
  const smoothMax = new Float32Array(rows);
  smoothMin.fill(Infinity);
  smoothMax.fill(-Infinity);
  for (let y = 0; y < rows; y++) {
    if (!Number.isFinite(nousRowMin[y])) continue;
    for (let oy = -1; oy <= 1; oy++) {
      const yy = y + oy;
      if (yy < 0 || yy >= rows) continue;
      if (!Number.isFinite(nousRowMin[yy])) continue;
      if (nousRowMin[yy] < smoothMin[y]) smoothMin[y] = nousRowMin[yy];
      if (nousRowMax[yy] > smoothMax[y]) smoothMax[y] = nousRowMax[yy];
    }
  }

  const pad = Math.max(4, CELL_W * 1.25);
  const bleedRows = 2;
  for (let y = 0; y < rows; y++) {
    if (!Number.isFinite(smoothMin[y])) continue;
    for (let oy = -bleedRows; oy <= bleedRows; oy++) {
      const row = y + oy;
      if (row < 0 || row >= rows) continue;
      const bleedPad = pad + Math.abs(oy) * CELL_W * 0.28;
      obstacleRows[row].push([
        Math.max(0, smoothMin[y] - bleedPad),
        Math.min(W, smoothMax[y] + bleedPad),
      ]);
    }
  }

  mergeObstacleRows();
}

function mergeObstacleRows() {
  for (const spans of obstacleRows) {
    if (spans.length < 2) continue;
    spans.sort((a, b) => a[0] - b[0]);
    let w = 0;
    for (let i = 1; i < spans.length; i++) {
      const last = spans[w], cur = spans[i];
      if (cur[0] <= last[1] + CELL_W * 3.1) last[1] = Math.max(last[1], cur[1]);
      else spans[++w] = cur;
    }
    spans.length = w + 1;
  }
}

const CUBE_VERTS = [
  [-1, -1, -1], [1, -1, -1], [1, 1, -1], [-1, 1, -1],
  [-1, -1, 1], [1, -1, 1], [1, 1, 1], [-1, 1, 1],
];
const CUBE_EDGES = [
  [0, 1], [1, 2], [2, 3], [3, 0],
  [4, 5], [5, 6], [6, 7], [7, 4],
  [0, 4], [1, 5], [2, 6], [3, 7],
];
function projectCubeVerts(center, radiusPx, orbitX, orbitY, scale = 1) {
  const camZ = 3.3;
  const projScale = radiusPx * 1.34 * scale;
  return CUBE_VERTS.map(([vx, vy, vz]) => {
    const [rx, ry, rz] = rotateOrbVec(vx, vy, vz, orbitX, orbitY);
    const inv = 1 / (camZ + rz);
    return {
      x: center.canvasX + rx * projScale * inv,
      y: center.canvasY + ry * projScale * inv,
      d: (rz + 1) * 0.5,
    };
  });
}
function convexHull(points) {
  if (points.length <= 2) return points.slice();
  const pts = points
    .map(p => ({ x: p.x, y: p.y }))
    .sort((a, b) => (a.x - b.x) || (a.y - b.y));
  const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
  const lower = [];
  for (const p of pts) {
    while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) lower.pop();
    lower.push(p);
  }
  const upper = [];
  for (let i = pts.length - 1; i >= 0; i--) {
    const p = pts[i];
    while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) upper.pop();
    upper.push(p);
  }
  lower.pop();
  upper.pop();
  return lower.concat(upper);
}
function makeEmptySpanRows() {
  return Array.from({ length: rows }, () => ({ left: Infinity, right: -Infinity }));
}
function addSpan(spanRows, row, left, right) {
  if (row < 0 || row >= rows || right <= left) return;
  if (left < spanRows[row].left) spanRows[row].left = left;
  if (right > spanRows[row].right) spanRows[row].right = right;
}
function collectCubeObstacleRows(center, radiusPx, orbitX, orbitY, obstacleScale) {
  const spanRows = makeEmptySpanRows();
  if (obstacleScale <= 0.0001) return spanRows;
  const projected = projectCubeVerts(center, radiusPx, orbitX, orbitY, ORB.cubeScale * obstacleScale)
    .map(p => ({ x: p.x - canvasLeft, y: p.y - canvasTop }));
  const hull = convexHull(projected);
  if (hull.length < 3) return spanRows;

  let yMin = Infinity;
  let yMax = -Infinity;
  for (const p of hull) {
    if (p.y < yMin) yMin = p.y;
    if (p.y > yMax) yMax = p.y;
  }
  const r0 = Math.max(0, Math.floor((yMin - CELL_H) / CELL_H));
  const r1 = Math.min(rows - 1, Math.ceil((yMax + CELL_H) / CELL_H));
  const pad = Math.max(4, CELL_W * 1.6);

  for (let row = r0; row <= r1; row++) {
    const scanY = row * CELL_H + CELL_H * 0.5;
    const xs = [];
    for (let i = 0; i < hull.length; i++) {
      const a = hull[i];
      const b = hull[(i + 1) % hull.length];
      const y0 = a.y;
      const y1 = b.y;
      if ((y0 <= scanY && scanY < y1) || (y1 <= scanY && scanY < y0)) {
        const t = (scanY - y0) / (y1 - y0);
        xs.push(a.x + (b.x - a.x) * t);
      }
    }
    if (xs.length < 2) continue;
    xs.sort((a, b) => a - b);
    for (let i = 0; i + 1 < xs.length; i += 2) {
      const left = Math.max(0, xs[i] - pad);
      const right = Math.min(W, xs[i + 1] + pad);
      if (right - left > CELL_W) addSpan(spanRows, row, left, right);
    }
  }
  return spanRows;
}
function collectSphereObstacleRows(center, radiusNorm, obstacleScale, rPad) {
  const spanRows = makeEmptySpanRows();
  const obstacleRadiusNorm = radiusNorm * obstacleScale;
  const obstaclePadNorm = (rPad / H) * obstacleScale;
  if (obstacleRadiusNorm <= 0.0001) return spanRows;

  const radiusPx = radiusNorm * H;
  const rMin = Math.floor((center.canvasY - radiusPx - rPad) / CELL_H);
  const rMax = Math.ceil((center.canvasY + radiusPx + rPad) / CELL_H);
  for (let r = rMin; r <= rMax; r++) {
    const y = r * CELL_H + CELL_H * 0.5;
    const stageY = y - canvasTop;
    const pyForObstacle = stageY / H - 0.5 - ORB.panY;
    if (Math.abs(pyForObstacle) > obstacleRadiusNorm + obstaclePadNorm) continue;
    const halfNorm = Math.sqrt(Math.max(0, (obstacleRadiusNorm + obstaclePadNorm) ** 2 - pyForObstacle * pyForObstacle));
    const halfPx = halfNorm * H;
    const stageRow = Math.floor(stageY / CELL_H);
    addSpan(spanRows, stageRow, center.x - halfPx, center.x + halfPx);
  }
  return spanRows;
}
function pushMorphedObstacleRows(sphereRows, cubeRows, mix) {
  const centerX = W * (0.5 + ORB.panX);
  for (let row = 0; row < rows; row++) {
    const sphereValid = Number.isFinite(sphereRows[row].left);
    const cubeValid = Number.isFinite(cubeRows[row].left);
    if (!sphereValid && !cubeValid) continue;
    const sLeft = sphereValid ? sphereRows[row].left : centerX;
    const sRight = sphereValid ? sphereRows[row].right : centerX;
    const cLeft = cubeValid ? cubeRows[row].left : centerX;
    const cRight = cubeValid ? cubeRows[row].right : centerX;
    const left = sLeft + (cLeft - sLeft) * mix;
    const right = sRight + (cRight - sRight) * mix;
    if (right - left > CELL_W) obstacleRows[row].push([Math.max(0, left), Math.min(W, right)]);
  }
}
function rotateOrbVec(vx, vy, vz, orbitX, orbitY) {
  let a = rot(vy, vz, orbitX); vy = a[0]; vz = a[1];
  a = rot(vx, vz, orbitY); vx = a[0]; vz = a[1];
  return [vx, vy, vz];
}
function rasterizeCubeWire(center, radiusPx, buildPhase, orbitX, orbitY, weight = 1) {
  const unit = Math.max(1, Math.min(CELL_W, CELL_H));
  const wirePx = Math.max(1, ORB.wire * radiusPx * 0.17);
  const brush = Math.max(0, Math.ceil((wirePx - unit * 0.45) / unit));
  const projected = projectCubeVerts(center, radiusPx, orbitX, orbitY, ORB.cubeScale);

  for (let ei = 0; ei < CUBE_EDGES.length; ei++) {
    const [a, b] = CUBE_EDGES[ei];
    const p0 = projected[a];
    const p1 = projected[b];
    const dx = p1.x - p0.x;
    const dy = p1.y - p0.y;
    const len = Math.hypot(dx, dy);
    const steps = Math.max(2, Math.ceil(len / Math.max(1, unit * 0.45)));
    for (let s = 0; s <= steps; s++) {
      const t = s / steps;
      const px = p0.x + dx * t;
      const py = p0.y + dy * t;
      const depth = p0.d + (p1.d - p0.d) * t;
      const c = Math.floor(px / CELL_W);
      const r = Math.floor(py / CELL_H);
      for (let oy = -brush; oy <= brush; oy++) {
        for (let ox = -brush; ox <= brush; ox++) {
          if (brush > 0 && ox * ox + oy * oy > brush * brush + 0.25) continue;
          const cc = c + ox;
          const rr = r + oy;
          if (rr < 0 || rr >= canvasRows || cc < 0 || cc >= canvasCols) continue;
          const reveal = buildPhase * 1.45 - hash(cc + ei * 13, rr + ei * 17) * 0.5;
          if (buildPhase < 1 && reveal <= 0) continue;
          const idx = rr * canvasCols + cc;
          const val = buildPhase < 1
            ? Math.min(CHARS_LAST, Math.floor(reveal * CHARS_LAST))
            : Math.min(CHARS_LAST, Math.floor((0.45 + depth * 0.55) * CHARS_LAST));
          const weightedVal = Math.min(CHARS_LAST, Math.floor(val * weight));
          if (weightedVal <= 0) continue;
          orbMask[idx] = 1;
          orbGlyph[idx] = "";
          if (weightedVal > orbChar[idx]) orbChar[idx] = weightedVal;
        }
      }
    }
  }
}

function rasterizeOrb(time) {
  orbMask.fill(0);
  orbChar.fill(0);
  orbGlyph.fill("");
  const buildStart = orbBuildStartTime();
  if (time < buildStart) return;
  const shapeMix = Math.max(0, Math.min(1, ORB.shapeMix ?? (ORB.shape === "cube" ? 1 : 0)));
  const sphereWeight = 1 - shapeMix;
  const cubeWeight = shapeMix;
  const sphereOrbitX = ORB.orbitX;
  const sphereOrbitY = ORB.orbitY + time * ORB.autoSpin;
  const cubeOrbitX = ORB.orbitX + time * ORB.autoSpin * 0.72;
  const cubeOrbitY = ORB.orbitY + time * ORB.autoSpin * 1.35;
  const latStep = Math.PI / ORB.lat;
  const lonStep = (Math.PI * 2) / ORB.lon;
  const rPad = 15.5;
  const center = orbCenter();
  const radiusPx = center.r;
  const radiusNorm = center.rNorm;
  const buildPhase = Math.min(1, Math.max(0, (time - buildStart) / SCENE.orbBuildDuration));
  const spacePhase = Math.min(1, Math.max(0, (time - buildStart) / Math.max(0.001, ctrls.orbSpaceDuration)));
  const spaceGrow = Math.pow(spacePhase, Math.max(0.01, ctrls.orbSpaceEase));
  const spaceStart = Math.max(0, Math.min(1, ctrls.orbSpaceStart));
  const obstacleScale = spaceStart + (1 - spaceStart) * spaceGrow;
  const rMin = Math.floor((center.canvasY - radiusPx - rPad) / CELL_H);
  const rMax = Math.ceil((center.canvasY + radiusPx + rPad) / CELL_H);

  const sphereObstacleRows = collectSphereObstacleRows(center, radiusNorm, obstacleScale, rPad);
  const cubeObstacleRows = collectCubeObstacleRows(center, radiusPx, cubeOrbitX, cubeOrbitY, obstacleScale);
  pushMorphedObstacleRows(sphereObstacleRows, cubeObstacleRows, shapeMix);

  if (cubeWeight > 0.001) {
    rasterizeCubeWire(center, radiusPx, buildPhase, cubeOrbitX, cubeOrbitY, cubeWeight);
  }
  if (sphereWeight <= 0.001) {
    mergeObstacleRows();
    return;
  }

  const cMin = Math.floor((center.canvasX - radiusPx - rPad) / CELL_W);
  const cMax = Math.ceil((center.canvasX + radiusPx + rPad) / CELL_W);
  for (let r = rMin; r <= rMax; r++) {
    const y = r * CELL_H + CELL_H * 0.5;
    const stageY = y - canvasTop;
    for (let c = cMin; c <= cMax; c++) {
      // Direct port of bb-ascii/app/nous/sphere/page.tsx:
      // px uses width/height normalization, py is row-normalized, then both
      // are offset by pan. This is what makes the sphere circular on screen.
      const stageX = c * CELL_W + CELL_W * 0.5 - canvasLeft;
      const px = (stageX / W - 0.5 - ORB.panX) * (W / H);
      const py = stageY / H - 0.5 - ORB.panY;
      const d2 = px * px + py * py;
      if (d2 > radiusNorm * radiusNorm) continue;

      const pz = Math.sqrt(radiusNorm * radiusNorm - d2);
      const depth = (pz / radiusNorm) * 0.5 + 0.5;

      let sx = px / radiusNorm, sy = py / radiusNorm, sz = pz / radiusNorm;
      let a = rot(sy, sz, sphereOrbitX); sy = a[0]; sz = a[1];
      a = rot(sx, sz, sphereOrbitY); sx = a[0]; sz = a[1];

      const latD = gridDist(Math.asin(Math.max(-1, Math.min(1, sy))) + Math.PI / 2, latStep);
      const lonD = gridDist(Math.atan2(sz, sx) + Math.PI, lonStep);
      const wire = ORB.wire * (0.3 + depth * 0.7);
      const minD = Math.min(latD, lonD);
      if (minD > wire) continue;

      const edge = 1 - minD / wire;
      if (r >= 0 && r < canvasRows && c >= 0 && c < canvasCols) {
        const reveal = buildPhase * 1.5 - hash(c, r) * 0.5;
        if (buildPhase < 1 && reveal <= 0) continue;
        const idx = r * canvasCols + c;
        const val = buildPhase < 1
          ? Math.min(CHARS_LAST, Math.floor(reveal * CHARS_LAST))
          : Math.min(CHARS_LAST, Math.floor(edge * (0.4 + depth * 0.6) * CHARS.length));
        const weightedVal = Math.min(CHARS_LAST, Math.floor(val * sphereWeight));
        if (weightedVal <= 0) continue;
        orbMask[idx] = 1;
        orbGlyph[idx] = "";
        if (weightedVal > orbChar[idx]) orbChar[idx] = weightedVal;
      }
    }
  }

  mergeObstacleRows();
}

let typedChars = 0;
let typedPrepared = null;
let typedDirty = true;
let typedBudget = 0;

function tickTyping(time, dt) {
  if (!bodyPrepared) return;
  if (time < codeTypingStartTime()) return;
  const cps = Math.max(1, ctrls.typeSpeed ?? 42);
  typedBudget += dt * cps;
  const steps = Math.min(240, Math.floor(typedBudget));
  if (steps > 0) {
    typedBudget -= steps;
    typedChars = Math.min(streamCorpus.length, typedChars + steps);
    typedDirty = true;
  }
}

function getTypedPrepared() {
  if (!typedDirty && typedPrepared) return typedPrepared;
  const cap = Math.max(0, Math.floor(typedChars));
  const visible = streamCorpus.slice(0, cap);
  typedPrepared = prepareWithSegments(visible, BODY_FONT, { whiteSpace: "pre-wrap" });
  typedDirty = false;
  return typedPrepared;
}

function layoutParagraphs() {
  drawnLines.count = 0;
  if (typedChars <= 0) return;
  const prepared = getTypedPrepared();
  ctx.font = BODY_FONT;
  const MARGIN_X = Math.max(32, Math.min(72, W * 0.06));
  const MIN_LINE_W = 120;
  const yStart = Math.max(32, H * 0.08);
  const yEnd = H - 32;
  let cursor = { segmentIndex: 0, graphemeIndex: 0 };

  for (let y = yStart; y < yEnd; y += BODY_LINE_H) {
    const row = Math.max(0, Math.min(rows - 1, Math.floor(y / CELL_H)));
    const spans = obstacleRows[row] || [];
    let n = 0;
    let x = MARGIN_X;

    for (const [a, b] of spans) {
      const left = Math.max(MARGIN_X, a);
      const right = Math.min(W - MARGIN_X, b);
      if (left - x >= MIN_LINE_W) { intervals[n++] = x; intervals[n++] = left - 10; }
      x = Math.max(x, right + 10);
    }
    if (W - MARGIN_X - x >= MIN_LINE_W) { intervals[n++] = x; intervals[n++] = W - MARGIN_X; }
    if (!n) { intervals[n++] = MARGIN_X; intervals[n++] = W - MARGIN_X; }

    for (let k = 0; k < n; k += 2) {
      const ix0 = intervals[k], ix1 = intervals[k + 1];
      const range = layoutNextLineRange(prepared, cursor, ix1 - ix0);
      if (!range) return;
      const line = materializeLineRange(prepared, range);
      const i = drawnLines.count++;
      drawnLines.x[i] = ix0;
      drawnLines.y[i] = y;
      drawnLines.w[i] = ix1 - ix0;
      drawnLines.text[i] = line.text;
      cursor = range.end;
    }
  }
}

function escapeHTML(text) {
  return text
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;");
}

function highlightSource(text) {
  const token = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/[^\n]*|\b(?:as|break|const|else|enum|false|fn|for|if|impl|in|let|match|mut|pub|return|self|static|struct|true|type|use|where|while)\b|\b[A-Z_]{2,}\b|\b\d+(?:\.\d+)?\b)/g;
  let out = "";
  let last = 0;
  for (const match of text.matchAll(token)) {
    const value = match[0];
    out += escapeHTML(text.slice(last, match.index));
    const cls = value.startsWith("#")
      ? "tok-comment"
      : value.startsWith('"') || value.startsWith("'")
        ? "tok-string"
        : /^\d/.test(value)
          ? "tok-number"
          : /^[A-Z_]{2,}$/.test(value)
            ? "tok-const"
            : "tok-keyword";
    out += `<span class="${cls}">${escapeHTML(value)}</span>`;
    last = match.index + value.length;
  }
  out += escapeHTML(text.slice(last));
  return out;
}

function renderTextLayer() {
  const parts = [LENS.mgBase, drawnLines.count];
  for (let i = 0; i < drawnLines.count; i++) {
    parts.push(drawnLines.x[i] | 0, drawnLines.y[i] | 0, drawnLines.w[i] | 0, drawnLines.text[i]);
  }
  const signature = parts.join("|");
  if (signature === lastTextSignature) return;
  lastTextSignature = signature;

  ctx.font = BODY_FONT;
  textLayer.style.color = LENS.mgBase;
  textLayer.style.setProperty("--midground-base", LENS.mgBase);

  while (lineNodes.length < drawnLines.count) {
    const el = document.createElement("div");
    el.className = "flow-line";
    textLayer.appendChild(el);
    lineNodes.push(el);
  }
  terminalAnchorEl = null;
  terminalLineEl = null;
  for (let i = 0; i < lineNodes.length; i++) {
    const el = lineNodes[i];
    if (i >= drawnLines.count) {
      el.hidden = true;
      continue;
    }
    const text = drawnLines.text[i];
    const isTail = i === drawnLines.count - 1;
    el.hidden = false;
    el.innerHTML = isTail
      ? `${highlightSource(text)}<span class="cursor-anchor" aria-hidden="true"></span>`
      : highlightSource(text);
    el.style.left = `${drawnLines.x[i]}px`;
    el.style.top = `${drawnLines.y[i] - BODY_LINE_H + 4}px`;
    el.style.width = `${drawnLines.w[i]}px`;
    el.style.textAlign = "left";
    el.style.wordSpacing = "0px";
    if (isTail) {
      terminalAnchorEl = el.querySelector(".cursor-anchor");
      terminalLineEl = el;
    }
  }
}


let sceneEpoch = 0;

function restartScene() {
  gsap.killTweensOf(ORB);
  gsap.killTweensOf(orbCanvas);
  autoCubeCall?.kill();
  autoCubeCall = null;
  ORB.panX = 0;
  ORB.panY = 0;
  ORB.zoom = ctrls.orbZoom;
  ORB.cubeScale = ctrls.cubeScale;
  ORB.shape = ctrls.orbShape;
  ORB.shapeMix = ctrls.orbShape === "cube" ? 1 : 0;
  ORB.orbitX = -1.42;
  ORB.orbitY = 0;
  ORB.wire = ctrls.orbWire;
  orbCanvas.style.opacity = "1";
  ORB.staged = false;
  ORB.positioned = true;
  typedChars = 0;
  typedBudget = 0;
  typedPrepared = null;
  typedDirty = true;
  nousStartTime = null;
  sceneEpoch = (typeof performance !== "undefined" ? performance.now() : Date.now()) * 0.001;
  lastTextSignature = "";
  startSceneTimeline();
}

function applyFontSizes() {
  const bodyPx = ctrls.bodyFontSize;
  const asciiPx = ctrls.asciiFontSize;
  BODY_FONT = `400 ${bodyPx}px "JetBrains Mono", "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace`;
  BODY_LINE_H = Math.round(bodyPx * 1.45);
  ASCII_FONT = `${asciiPx}px monospace`;
  CELL_H = asciiPx;
  textLayer.style.font = `400 ${bodyPx}px/${BODY_LINE_H}px "JetBrains Mono", "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace`;
  rebuildLayouts();
  typedPrepared = null;
  typedDirty = true;
  resize();
  lastTextSignature = "";
}

function applyColors() {
  if (!LENS.current) return;
  LENS.current.bgColor = ctrls.bgColor;
  LENS.current.mgColor = ctrls.mgColor;
  LENS.current.fgColor = ctrls.fgColor;
  applyLens(LENS.current);
  lastTextSignature = "";
}

const ctrls = {
  bodyFontSize: 10,
  asciiFontSize: 8,
  autoSpin: 0.18,
  orbShape: "sphere",
  shapeMorphDuration: 0.65,
  cubeScale: 1.8,
  orbLead: 0.35,
  orbZoom: 3,
  orbRadius: 0.135,
  orbWire: 0.2,
  orbSpaceStart: 0.82,
  orbSpaceDuration: 0.7,
  orbSpaceEase: 1.1,
  orbWireLow: 0.01,
  wireDropDuration: 1.2,
  orbFadeDuration: 0.85,
  orbContainNous: 1.0,
  nousAfterWire: 0,
  nousLetterSpacing: 2.7,
  typeSpeed: 816,
  bgColor: "#041C1C",
  mgColor: "#ffe6cb",
  fgColor: "#FFFFFF",
  noiseEnabled: true,
  noiseColor: "#eaeaea",
  noiseDensity: 0.11,
  noiseOpacity: 0.25,
  noiseSize: 1,
  noiseBlend: "difference",
  restart: () => restartScene(),
};

const DEV_MODE = new URLSearchParams(window.location.search).has("dev");
if (DEV_MODE) {
  const gui = new GUI({ title: "controls" });
  gui.add(ctrls, "bodyFontSize", 10, 22, 1).name("body font").onChange(applyFontSizes);
  gui.add(ctrls, "asciiFontSize", 8, 20, 1).name("ascii font").onChange(applyFontSizes);
  gui.add(ctrls, "autoSpin", 0, 0.6, 0.01).name("auto-spin").onChange(v => { ORB.autoSpin = v; });
  orbShapeControl = gui.add(ctrls, "orbShape", ["sphere", "cube"]).name("orb shape").onChange(v => {
    setOrbShape(v);
  });
  gui.add(ctrls, "shapeMorphDuration", 0.05, 2, 0.01).name("shape morph (s)");
  gui.add(ctrls, "orbLead", 0, 6, 0.05).name("orb start (s)").onFinishChange(() => restartScene());
  gui.add(ctrls, "orbZoom", 0.25, 12, 0.01).name("orb zoom").onChange(v => { ORB.zoom = v; });
  gui.add(ctrls, "cubeScale", 0.8, 3.5, 0.01).name("cube scale").onChange(v => { ORB.cubeScale = v; });
  gui.add(ctrls, "orbRadius", 0.08, 0.4, 0.005).name("orb radius").onChange(v => { ORB.radius = v; });
  gui.add(ctrls, "orbSpaceStart", 0.4, 1, 0.01).name("space start");
  gui.add(ctrls, "orbSpaceDuration", 0.2, 4, 0.05).name("space grow (s)");
  gui.add(ctrls, "orbSpaceEase", 0.4, 3, 0.05).name("space ease");
  gui.add(ctrls, "orbWire", 0.01, 0.2, 0.005).name("wire start").onChange(v => { ORB.wire = v; });
  gui.add(ctrls, "orbWireLow", 0.005, 0.12, 0.001).name("wire low").onFinishChange(() => restartScene());
  gui.add(ctrls, "wireDropDuration", 0.4, 6, 0.05).name("wire drop (s)").onFinishChange(() => restartScene());
  gui.add(ctrls, "orbContainNous", 0, 3, 0.05).name("orb contain NOUS");
  gui.add(ctrls, "orbFadeDuration", 0.1, 3, 0.05).name("orb fade on NOUS");
  gui.add(ctrls, "nousAfterWire", 0, 4, 0.05).name("NOUS after wire").onFinishChange(() => restartScene());
  gui.add(ctrls, "nousLetterSpacing", 0, 4, 0.05).name("NOUS spacing");
  gui.add(ctrls, "typeSpeed", 0, 4000, 10).name("type speed");
  gui.addColor(ctrls, "bgColor").name("background").onChange(applyColors);
  gui.addColor(ctrls, "mgColor").name("midground").onChange(applyColors);
  gui.addColor(ctrls, "fgColor").name("foreground").onChange(applyColors);
  const noiseFolder = gui.addFolder("noise");
  noiseFolder.add(ctrls, "noiseEnabled").name("on");
  noiseFolder.addColor(ctrls, "noiseColor").name("color");
  noiseFolder.add(ctrls, "noiseDensity", 0, 1, 0.01).name("density");
  noiseFolder.add(ctrls, "noiseOpacity", 0, 1, 0.01).name("opacity");
  noiseFolder.add(ctrls, "noiseSize", 0.1, 10, 0.1).name("size");
  noiseFolder.add(ctrls, "noiseBlend", [
    "normal", "multiply", "screen", "difference", "exclusion",
    "color-dodge", "color-burn", "hard-light", "soft-light", "overlay", "lighten",
  ]).name("blend").onChange(v => { document.querySelector(".noise").style.mixBlendMode = v; });
  gui.add(ctrls, "restart").name("restart anim");
}

const terminalCursorEl = document.getElementById("terminalCursor");
function getLastCharRect(lineEl) {
  if (!lineEl) return null;
  const walker = document.createTreeWalker(lineEl, NodeFilter.SHOW_TEXT);
  let lastTextNode = null;
  let node = walker.nextNode();
  while (node) {
    if (node.nodeValue && node.nodeValue.length) lastTextNode = node;
    node = walker.nextNode();
  }
  if (!lastTextNode) return null;
  const end = lastTextNode.nodeValue.length;
  if (!end) return null;
  const range = document.createRange();
  range.setStart(lastTextNode, end - 1);
  range.setEnd(lastTextNode, end);
  const rects = range.getClientRects();
  const rect = rects.length ? rects[rects.length - 1] : range.getBoundingClientRect();
  return rect && (rect.width || rect.height) ? rect : null;
}
function updateTerminalCursor() {
  if (!drawnLines.count || !terminalAnchorEl || !terminalLineEl) {
    terminalCursorEl.hidden = true;
    return;
  }
  const stageRect = stage.getBoundingClientRect();
  const charRect = getLastCharRect(terminalLineEl);
  const anchorRect = terminalAnchorEl.getBoundingClientRect();
  if (!anchorRect.width && !anchorRect.height) {
    terminalCursorEl.hidden = true;
    return;
  }
  const fontSize = parseInt(BODY_FONT.match(/(\d+)px/)?.[1] || "12", 10);
  const fallbackW = Math.max(6, Math.round(fontSize * 0.6));
  const leftPx = (charRect?.left ?? (anchorRect.left - fallbackW)) - stageRect.left;
  const topPx = (charRect?.top ?? anchorRect.top) - stageRect.top;
  const widthPx = Math.max(1, Math.round(charRect?.width || fallbackW));
  const heightPx = Math.max(1, Math.round(charRect?.height || (BODY_LINE_H - 4)));
  const left = Math.round(leftPx);
  const top = Math.round(topPx);
  terminalCursorEl.style.left = `${left}px`;
  terminalCursorEl.style.top = `${top}px`;
  terminalCursorEl.style.height = `${heightPx}px`;
  terminalCursorEl.style.width = `${widthPx}px`;
  terminalCursorEl.style.fontSize = `${fontSize}px`;
  terminalCursorEl.hidden = false;
}

const noiseCanvas = document.getElementById("noiseCanvas");
const noiseGL = noiseCanvas.getContext("webgl", { alpha: true, premultipliedAlpha: true })
  || noiseCanvas.getContext("experimental-webgl", { alpha: true, premultipliedAlpha: true });
let noiseProgram = null;
let noiseLocs = null;
function initNoise() {
  if (!noiseGL) return;
  const vertSrc = `
    attribute vec2 aPos;
    varying vec2 vUv;
    void main() {
      vUv = aPos * 0.5 + 0.5;
      gl_Position = vec4(aPos, 0.0, 1.0);
    }
  `;
  const fragSrc = `
    precision mediump float;
    varying vec2 vUv;
    uniform vec2 uRes;
    uniform float uDpr;
    uniform float uSize;
    uniform float uDensity;
    uniform float uOpacity;
    uniform vec3 uColor;
    float hash(vec2 p) {
      vec3 p3 = fract(vec3(p.xyx) * 0.1031);
      p3 += dot(p3, p3.yzx + 33.33);
      return fract((p3.x + p3.y) * p3.z);
    }
    void main() {
      float n = hash(floor(vUv * uRes / (uSize * uDpr)));
      gl_FragColor = vec4(uColor, step(1.0 - uDensity, n)) * uOpacity;
    }
  `;
  function compile(type, src) {
    const sh = noiseGL.createShader(type);
    noiseGL.shaderSource(sh, src);
    noiseGL.compileShader(sh);
    return sh;
  }
  const vs = compile(noiseGL.VERTEX_SHADER, vertSrc);
  const fs = compile(noiseGL.FRAGMENT_SHADER, fragSrc);
  noiseProgram = noiseGL.createProgram();
  noiseGL.attachShader(noiseProgram, vs);
  noiseGL.attachShader(noiseProgram, fs);
  noiseGL.linkProgram(noiseProgram);
  noiseGL.useProgram(noiseProgram);

  const buf = noiseGL.createBuffer();
  noiseGL.bindBuffer(noiseGL.ARRAY_BUFFER, buf);
  noiseGL.bufferData(noiseGL.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), noiseGL.STATIC_DRAW);
  const aPos = noiseGL.getAttribLocation(noiseProgram, "aPos");
  noiseGL.enableVertexAttribArray(aPos);
  noiseGL.vertexAttribPointer(aPos, 2, noiseGL.FLOAT, false, 0, 0);

  noiseLocs = {
    uRes: noiseGL.getUniformLocation(noiseProgram, "uRes"),
    uDpr: noiseGL.getUniformLocation(noiseProgram, "uDpr"),
    uSize: noiseGL.getUniformLocation(noiseProgram, "uSize"),
    uDensity: noiseGL.getUniformLocation(noiseProgram, "uDensity"),
    uOpacity: noiseGL.getUniformLocation(noiseProgram, "uOpacity"),
    uColor: noiseGL.getUniformLocation(noiseProgram, "uColor"),
  };

  noiseGL.enable(noiseGL.BLEND);
  noiseGL.blendFunc(noiseGL.ONE, noiseGL.ONE_MINUS_SRC_ALPHA);
  noiseGL.clearColor(0, 0, 0, 0);
}

function resizeNoise() {
  if (!noiseGL) return;
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  noiseCanvas.width = innerWidth * dpr;
  noiseCanvas.height = innerHeight * dpr;
  noiseGL.viewport(0, 0, noiseCanvas.width, noiseCanvas.height);
  if (noiseLocs) {
    noiseGL.uniform2f(noiseLocs.uRes, noiseCanvas.width, noiseCanvas.height);
    noiseGL.uniform1f(noiseLocs.uDpr, dpr);
  }
}
addEventListener("resize", resizeNoise);
initNoise();
resizeNoise();

function drawNoiseFrame() {
  if (!noiseGL || !noiseProgram) return;
  noiseGL.clear(noiseGL.COLOR_BUFFER_BIT);
  if (!ctrls.noiseEnabled) return;
  noiseGL.useProgram(noiseProgram);
  noiseGL.uniform1f(noiseLocs.uSize, ctrls.noiseSize);
  noiseGL.uniform1f(noiseLocs.uDensity, ctrls.noiseDensity);
  noiseGL.uniform1f(noiseLocs.uOpacity, ctrls.noiseOpacity);
  const [r, g, b] = hexToRGB(ctrls.noiseColor);
  noiseGL.uniform3f(noiseLocs.uColor, r / 255, g / 255, b / 255);
  noiseGL.drawArrays(noiseGL.TRIANGLE_STRIP, 0, 4);
}

function draw(time) {
  if (!fontsReady || !bodyPrepared) { requestAnimationFrame(draw); return; }
  const t = time * 0.001 - sceneEpoch;
  const dt = lastFrameTime ? Math.min(0.05, (time - lastFrameTime) / 1000) : 0.016;
  lastFrameTime = time;
  ctx.clearRect(0, 0, canvasW, canvasH);
  orbCtx.clearRect(0, 0, canvasW, canvasH);

  rasterizeNOUS(t);
  rasterizeOrb(t);
  tickTyping(t, dt);
  layoutParagraphs();
  renderTextLayer();
  updateTerminalCursor();
  drawNoiseFrame();

  ctx.font = ASCII_FONT;
  ctx.textBaseline = "middle";
  ctx.textAlign = "center";
  ctx.fillStyle = LENS.mgBase;
  orbCtx.font = ASCII_FONT;
  orbCtx.textBaseline = "middle";
  orbCtx.textAlign = "center";
  orbCtx.fillStyle = LENS.mgBase;
  for (let r = 0; r < canvasRows; r++) {
    const base = r * canvasCols;
    for (let c = 0; c < canvasCols; c++) {
      const idx = base + c;
      if (asciiMask[idx] === EMPTY) continue;
      ctx.fillText(asciiGlyph[idx] || CHARS[asciiChar[idx]], c * CELL_W + CELL_W * 0.5, r * CELL_H + CELL_H * 0.5);
    }
  }
  for (let r = 0; r < canvasRows; r++) {
    const base = r * canvasCols;
    for (let c = 0; c < canvasCols; c++) {
      const idx = base + c;
      if (orbMask[idx] === EMPTY) continue;
      orbCtx.fillText(orbGlyph[idx] || CHARS[orbChar[idx]], c * CELL_W + CELL_W * 0.5, r * CELL_H + CELL_H * 0.5);
    }
  }

  requestAnimationFrame(draw);
}

resize();
const DS_CDN = "https://esm.sh/@nous-research/ui@0.4.0/dist/fonts";
const FACES = [new FontFace("Mondwest", `url(${DS_CDN}/Mondwest-Regular.woff2) format("woff2")`, { weight: "400", display: "block" })];
(async () => {
  try {
    const loaded = await Promise.all(FACES.map(f => f.load()));
    for (const f of loaded) document.fonts.add(f);
    await document.fonts.load(BODY_FONT, "Aa");
    await document.fonts.load(ASCII_FONT, "Aa");
  } catch (err) {
    console.warn("DS font load failed, using fallbacks:", err);
  }
  rebuildLayouts();
  fontsReady = true;
  requestAnimationFrame(draw);
})();
</script>
</body>
</html>
