/* ============================================================
   Nibiru v2 — site script
   ------------------------------------------------------------
   - Hero constellation (Three.js)
   - Comet-trail cursor
   - MMVC pinned narrative (scroll-driven camera focus + panel sync)
   - Mission Control chat (window.claude.complete + telemetry)
   - Launch-sequence typewriter + live system canvas
   - Footer wide-shot constellation
   - Nav condense / back-to-top
   ============================================================ */
(() => {
  'use strict';

  const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const hasThree = typeof THREE !== 'undefined';

  /* ====================== NAV CONDENSE ====================== */
  const nav = document.getElementById('nav');
  const toTop = document.getElementById('toTop');
  const onScroll = () => {
    const y = window.scrollY || 0;
    nav.classList.toggle('condensed', y > 40);
    toTop.classList.toggle('visible', y > window.innerHeight * 0.8);
  };
  window.addEventListener('scroll', onScroll, { passive: true });
  toTop.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' }));
  onScroll();

  /* ====================== CURSOR GLOW (subtle) ====================== */
  // A small soft star-glow follows the cursor — much subtler than before.
  // Disabled on touch / small screens.
  if (!reducedMotion && window.matchMedia('(hover: hover)').matches && innerWidth > 900) {
    const trail = document.getElementById('comet-trail');
    const tctx = trail.getContext('2d');
    let mx = -100, my = -100, tx = -100, ty = -100;
    const resize = () => {
      trail.width = innerWidth * devicePixelRatio;
      trail.height = innerHeight * devicePixelRatio;
      trail.style.width = innerWidth + 'px';
      trail.style.height = innerHeight + 'px';
      tctx.setTransform(1, 0, 0, 1, 0, 0);
      tctx.scale(devicePixelRatio, devicePixelRatio);
    };
    resize();
    window.addEventListener('resize', resize);
    window.addEventListener('mousemove', (e) => { mx = e.clientX; my = e.clientY; });
    const tick = () => {
      // ease the rendered position so it lags slightly
      tx += (mx - tx) * 0.18;
      ty += (my - ty) * 0.18;
      tctx.clearRect(0, 0, innerWidth, innerHeight);
      const r = 18;
      const g = tctx.createRadialGradient(tx, ty, 0, tx, ty, r);
      g.addColorStop(0, 'rgba(244,238,219,0.22)');
      g.addColorStop(0.4, 'rgba(184,107,255,0.10)');
      g.addColorStop(1, 'rgba(91,141,255,0)');
      tctx.fillStyle = g;
      tctx.beginPath();
      tctx.arc(tx, ty, r, 0, Math.PI * 2);
      tctx.fill();
      requestAnimationFrame(tick);
    };
    requestAnimationFrame(tick);
  }

  /* ====================== HERO GALAXY ====================== */
  const heroCanvas = document.getElementById('constellation');
  let heroSystem = null;
  if (hasThree && heroCanvas && !reducedMotion) {
    heroSystem = makeGalaxy(heroCanvas);
  }

  /* ====================== MMVC PINNED NARRATIVE ====================== */
  const mmvcTrack = document.getElementById('mmvcTrack');
  const mmvcPanels = document.querySelectorAll('.mmvc-copy .panel');
  const mmvcSteps = document.querySelectorAll('.mmvc-progress .step');
  const mmvcCanvas = document.getElementById('mmvc-canvas');

  // Dedicated MMVC system (separate canvas inside .mmvc-visual)
  let mmvcSystem = null;
  if (hasThree && mmvcCanvas && !reducedMotion) {
    mmvcSystem = makeNibiruSystem(mmvcCanvas, {
      withStarfield: true,
      labels: false,
      autoRotate: true,
      cameraDistance: 6.5,
      modules: [
        { name: 'Retriever',    sig: '', angle: 0,    radius: 2.2, color: 0xb86bff, size: 0.16 },
        { name: 'ReActPlanner', sig: '', angle: 1.3,  radius: 2.6, color: 0xffb574, size: 0.18 },
        { name: 'Vision',       sig: '', angle: 2.5,  radius: 2.0, color: 0x5b8dff, size: 0.14 },
        { name: 'Tool::calc',   sig: '', angle: 3.7,  radius: 2.8, color: 0x7ad6a3, size: 0.12 },
        { name: 'Memory',       sig: '', angle: 4.9,  radius: 2.3, color: 0xff8fb1, size: 0.14 },
      ]
    });
  }

  let currentMmvcStep = -1;
  const setMmvcStep = (n) => {
    if (n === currentMmvcStep) return;
    currentMmvcStep = n;
    mmvcPanels.forEach((p, i) => p.classList.toggle('active', i === n));
    mmvcSteps.forEach((p, i) => p.classList.toggle('active', i === n));
    if (mmvcSystem) mmvcSystem.focus(n); // 0=Model, 1=AI, 2=Module, 3=Controller, 4=View
  };

  const PANEL_COUNT = 5;
  const stickyCanvas = () => {
    if (!mmvcTrack) return;
    const trackRect = mmvcTrack.getBoundingClientRect();
    const trackTop = trackRect.top;
    const total = mmvcTrack.offsetHeight - window.innerHeight;
    const progress = Math.max(0, Math.min(1, -trackTop / total));
    let step = Math.floor(progress * PANEL_COUNT);
    if (step > PANEL_COUNT - 1) step = PANEL_COUNT - 1;
    if (progress >= 0 && trackTop <= 0) {
      setMmvcStep(step);
    } else if (trackTop > 0) {
      setMmvcStep(0);
    }
  };
  window.addEventListener('scroll', stickyCanvas, { passive: true });
  window.addEventListener('resize', stickyCanvas);
  stickyCanvas();

  /* ====================== MISSION CONTROL CHAT ====================== */
  const mcBody = document.getElementById('mcBody');
  const mcInput = document.getElementById('mcInput');
  const mcSend = document.getElementById('mcSend');
  const mcReset = document.getElementById('mcReset');
  const mcTele = document.getElementById('mcTele');
  let totalTokens = 0;

  let currentModel = 'nibiru-base-7B';
  const teleUpdate = (latency, tokens, model) => {
    if (model) currentModel = model;
    mcTele.innerHTML = `
      <span>MODEL <strong>${currentModel}</strong></span>
      <span>LATENCY <strong>${latency != null ? latency + ' ms' : '—'}</strong></span>
      <span>TOKENS <strong>${tokens}</strong></span>
    `;
  };

  const mcAddLine = (cls, text) => {
    const l = document.createElement('div');
    l.className = 'mc-line ' + cls;
    l.textContent = text;
    mcBody.appendChild(l);
    mcBody.scrollTop = mcBody.scrollHeight;
    return l;
  };

  const moduleTrace = [
    '  · Controller::dispatch(prompt)',
    '  · Module<Retriever>::query(prompt) → 5 docs',
    '  · Module<ReActPlanner>::run(prompt, ctx)',
    '  · Model::stream(messages) → tokens',
  ];

  const askAgent = async (prompt) => {
    if (!prompt.trim()) return;
    mcInput.value = '';
    mcInput.disabled = true; mcSend.disabled = true;
    mcAddLine('usr', prompt);

    // Trace lines (cosmetic — the "modules lighting up")
    for (const t of moduleTrace) {
      await sleep(160 + Math.random() * 100);
      mcAddLine('trc', t);
    }

    mcAddLine('asst-label', '◇ assistant');
    const out = document.createElement('div');
    out.className = 'mc-line asst-stream';
    mcBody.appendChild(out);
    const cursor = document.createElement('span');
    cursor.className = 'mc-cursor';
    out.appendChild(cursor);

    const start = performance.now();
    let text = '';
    let modelLabel = null;
    try {
      const res = await fetch('/api/oracle', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          messages: [{ role: 'user', content: prompt }]
        })
      });
      if (!res.ok) throw new Error('oracle ' + res.status);
      const data = await res.json();
      text = (data.answer || '').trim();
      modelLabel = data.model || null;
    } catch (e) {
      text = 'Link interrupted. Re-establishing controller… try again in a moment.';
    }
    // Stream-render the response by chunks of 1–2 chars
    out.textContent = '';
    out.appendChild(cursor);
    for (let i = 0; i < text.length; i++) {
      out.insertBefore(document.createTextNode(text[i]), cursor);
      totalTokens += 0.25; // pseudo-token counter
      if (i % 4 === 0) {
        teleUpdate(Math.round(performance.now() - start), Math.floor(totalTokens));
        mcBody.scrollTop = mcBody.scrollHeight;
      }
      await sleep(8 + Math.random() * 18);
    }
    cursor.remove();
    const elapsed = Math.round(performance.now() - start);
    teleUpdate(elapsed, Math.floor(totalTokens), modelLabel);
    mcAddLine('done', `✓ done · ${elapsed} ms · ${Math.floor(totalTokens)} tok`);

    mcInput.disabled = false; mcSend.disabled = false;
    mcInput.focus();
  };

  mcSend.addEventListener('click', () => askAgent(mcInput.value));
  mcInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') askAgent(mcInput.value); });
  mcReset.addEventListener('click', () => {
    mcBody.innerHTML = '';
    mcAddLine('sys', '› session reset · controller online · 4 modules in orbit');
    totalTokens = 0;
    teleUpdate(null, 0);
  });
  document.querySelectorAll('.mc-suggestion').forEach(b => {
    b.addEventListener('click', () => {
      const map = {
        'explain mmvc': 'Explain the MMVC pattern in two sentences. Why split Model into Model and Module?',
        'rag': 'Show a tiny PHP snippet that builds a RAG agent with Nibiru: a Retriever module wrapping a model, plus a ReActPlanner.',
        'tools': "How do I add a custom 'calculator' tool to a Nibiru agent? Give a short PHP snippet using tool('calc', fn).",
        'haiku': 'Write a haiku about a small AI agent orbiting a PHP server.'
      };
      askAgent(map[b.dataset.q] || b.textContent);
    });
  });

  /* ====================== LAUNCH SEQUENCE ====================== */
  const launchBody = document.getElementById('launchBody');
  const launchPlay = document.getElementById('launchPlay');
  const launchCanvas = document.getElementById('launch-canvas');
  const lsModules = document.getElementById('lsModules');
  const lsTokens = document.getElementById('lsTokens');
  const lsLatency = document.getElementById('lsLatency');

  // Build a small system canvas dedicated to the launch demo
  let launchSys = null;
  if (hasThree && launchCanvas && !reducedMotion) {
    launchSys = makeNibiruSystem(launchCanvas, {
      withStarfield: false,
      labels: false,
      autoRotate: true,
      modules: [] // start empty; launch sequence adds them
    });
  }

  // Tokenized PHP code script. Each step writes a chunk and may register a module.
  const LAUNCH_SCRIPT = [
    { code: '<?php\n', delay: 80 },
    { code: '<span class="tk-c">// Nibiru — Model, Module, View, Controller</span>\n', delay: 160 },
    { code: '<span class="tk-k">use</span> Nibiru\\{Model, Module, Controller, View};\n', delay: 220 },
    { code: '<span class="tk-k">use</span> Nibiru\\Std\\{Retriever, ReActPlanner, ChatView};\n\n', delay: 300 },

    { code: '<span class="tk-v">$model</span>     = <span class="tk-cls">Model</span>::<span class="tk-fn">load</span>(<span class="tk-s">\'nibiru-base-7B\'</span>);\n', delay: 360, fx: () => addLaunchSatellite('Model', 0xffb574, 0, true) },

    { code: '<span class="tk-v">$retriever</span> = <span class="tk-k">new</span> <span class="tk-cls">Retriever</span>(<span class="tk-attr">index</span>: <span class="tk-s">\'./docs\'</span>, <span class="tk-attr">k</span>: <span class="tk-n">5</span>);\n', delay: 380, fx: () => addLaunchSatellite('Retriever', 0xb86bff, 1) },

    { code: '<span class="tk-v">$planner</span>   = <span class="tk-k">new</span> <span class="tk-cls">ReActPlanner</span>(\n', delay: 240 },
    { code: '    <span class="tk-attr">model</span>: <span class="tk-v">$model</span>,\n', delay: 200 },
    { code: '    <span class="tk-attr">tools</span>: [<span class="tk-v">$retriever</span>],\n', delay: 200 },
    { code: ');\n\n', delay: 200, fx: () => addLaunchSatellite('ReActPlanner', 0x5b8dff, 2) },

    { code: '<span class="tk-v">$agent</span> = <span class="tk-k">new</span> <span class="tk-cls">Controller</span>(\n', delay: 240 },
    { code: '    <span class="tk-v">$planner</span>,\n', delay: 200 },
    { code: '    <span class="tk-attr">view</span>: <span class="tk-k">new</span> <span class="tk-cls">ChatView</span>(),\n', delay: 200 },
    { code: ');\n\n', delay: 220, fx: () => addLaunchSatellite('ChatView', 0x7ad6a3, 3) },

    { code: '<span class="tk-k">foreach</span> (<span class="tk-v">$agent</span>-><span class="tk-fn">stream</span>(<span class="tk-s">\'hello\'</span>) <span class="tk-k">as</span> <span class="tk-v">$tok</span>) {\n', delay: 280, fx: () => simulateTokens() },
    { code: '    <span class="tk-k">echo</span> <span class="tk-v">$tok</span>;\n', delay: 200 },
    { code: '}\n', delay: 220 },
  ];

  let satelliteCount = 0;
  function addLaunchSatellite(name, color, idx, isCenter = false) {
    if (!launchSys) return;
    if (isCenter) {
      // model already exists at center; just blink it
      launchSys.flashCenter();
      return;
    }
    const angle = (idx - 1) * (Math.PI * 2 / 4);
    launchSys.addModule({ name, angle, radius: 2.2 + (idx % 2) * 0.4, color, size: 0.15 });
    satelliteCount++;
    if (lsModules) lsModules.textContent = satelliteCount;
  }

  async function simulateTokens() {
    if (!lsTokens || !lsLatency) return;
    const start = performance.now();
    let n = 0;
    const total = 87;
    const step = () => {
      n++;
      lsTokens.textContent = n;
      lsLatency.textContent = Math.round(performance.now() - start) + ' ms';
      if (launchSys) launchSys.pulseGravity();
      if (n < total) setTimeout(step, 18 + Math.random() * 24);
    };
    step();
  }

  let launchRunning = false;
  async function runLaunch() {
    if (launchRunning) return;
    launchRunning = true;
    launchPlay.textContent = '▶ Replay';
    launchBody.innerHTML = '';
    if (launchSys) launchSys.reset();
    satelliteCount = 0;
    if (lsModules) lsModules.textContent = '0';
    if (lsTokens) lsTokens.textContent = '0';
    if (lsLatency) lsLatency.textContent = '— ms';

    const caret = '<span class="caret"></span>';
    let html = '';
    for (const part of LAUNCH_SCRIPT) {
      if (part.fx) part.fx();
      // type out gradually for nicer effect
      const raw = part.code;
      // For the typewriter feel we render full chunk then a small delay
      html += raw;
      launchBody.innerHTML = html + caret;
      await sleep(part.delay);
    }
    launchBody.innerHTML = html;
    launchRunning = false;
  }
  if (launchPlay) launchPlay.addEventListener('click', runLaunch);

  // Auto-launch when the section enters view
  if ('IntersectionObserver' in window && launchPlay) {
    const io = new IntersectionObserver((entries) => {
      entries.forEach(e => {
        if (e.isIntersecting) {
          runLaunch();
          io.disconnect();
        }
      });
    }, { threshold: 0.4 });
    io.observe(document.getElementById('code'));
  }

  /* ====================== FOOTER WIDE-SHOT ====================== */
  const footerCanvas = document.getElementById('footer-canvas');
  if (hasThree && footerCanvas && !reducedMotion) {
    makeNibiruSystem(footerCanvas, {
      withStarfield: true,
      labels: false,
      autoRotate: true,
      cameraDistance: 9,
      modules: [
        { name: '', angle: 0,   radius: 2.2, color: 0xb86bff, size: 0.10 },
        { name: '', angle: 1.4, radius: 2.6, color: 0xffb574, size: 0.10 },
        { name: '', angle: 2.7, radius: 2.0, color: 0x5b8dff, size: 0.08 },
        { name: '', angle: 4.0, radius: 2.8, color: 0x7ad6a3, size: 0.08 },
      ]
    });
  }

  /* ====================== HELPERS ====================== */
  function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

  function makeRadialTexture(stops) {
    const c = document.createElement('canvas');
    c.width = c.height = 128;
    const ctx = c.getContext('2d');
    const g = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);
    stops.forEach((s, i) => g.addColorStop(i / (stops.length - 1), s));
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, 128, 128);
    const tex = new THREE.CanvasTexture(c);
    tex.needsUpdate = true;
    return tex;
  }

  /* ====================== GALAXY (hero) ====================== */
  function makeGalaxy(canvas) {
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 500);
    const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
    renderer.setPixelRatio(Math.min(2.5, devicePixelRatio));
    renderer.setClearColor(0x000000, 0);

    // Camera spherical coords
    const cam = {
      radius: 11,
      minRadius: 5.5,
      maxRadius: 28,
      theta: Math.PI * 0.25,
      phi: 1.05,
      target: new THREE.Vector3(0, 0, 0),
      // Visual offset: shift the rendered scene UP so the galaxy sits in the
      // upper portion of the hero, above the headline.
      yOffset: 1.2,
    };
    function applyCamera() {
      const r = cam.radius;
      const sp = Math.sin(cam.phi);
      camera.position.set(
        r * sp * Math.sin(cam.theta),
        r * Math.cos(cam.phi) + cam.yOffset,
        r * sp * Math.cos(cam.theta)
      );
      const look = cam.target.clone();
      look.y += cam.yOffset;
      camera.lookAt(look);
    }
    applyCamera();

    const resize = () => {
      const parent = canvas.parentElement;
      const rect = (parent || canvas).getBoundingClientRect();
      const w = Math.max(2, rect.width), h = Math.max(2, rect.height);
      camera.aspect = w / h;
      camera.updateProjectionMatrix();
      renderer.setSize(w, h, true);
    };
    resize();
    window.addEventListener('resize', resize);
    if (typeof ResizeObserver !== 'undefined') {
      new ResizeObserver(resize).observe(canvas.parentElement || canvas);
    }

    // ---------- Galaxy particles ----------
    const PARTICLES = 18000;
    const ARMS = 4;
    const positions = new Float32Array(PARTICLES * 3);
    const colors = new Float32Array(PARTICLES * 3);
    const sizes = new Float32Array(PARTICLES);
    const baseAngle = new Float32Array(PARTICLES);
    const radii = new Float32Array(PARTICLES);
    const heights = new Float32Array(PARTICLES);

    const COLOR_CORE = new THREE.Color('#ffe6b8');
    const COLOR_HOT  = new THREE.Color('#ff9d6c');
    const COLOR_MID  = new THREE.Color('#b86bff');
    const COLOR_OUT  = new THREE.Color('#6d9bff');
    const COLOR_DUST = new THREE.Color('#3a1f5a');

    for (let i = 0; i < PARTICLES; i++) {
      const r = Math.pow(Math.random(), 1.8) * 6.4 + 0.05;
      const arm = Math.floor(Math.random() * ARMS);
      const armOffset = (arm / ARMS) * Math.PI * 2;
      const curl = r * 0.9;
      // Tighter arm spread near outer
      const armSpread = (Math.random() - 0.5) * 0.55 * Math.exp(-r * 0.12);
      // Background "haze" stars between arms
      const isArmStar = Math.random() < 0.78;
      const angleNoise = isArmStar ? armSpread : (Math.random() - 0.5) * Math.PI * 0.9;
      const angle = armOffset + curl + angleNoise;

      const verticalScale = Math.exp(-r * 0.45) * 0.45 + 0.06;
      const yJitter = (Math.random() - 0.5) * verticalScale;

      const x = Math.cos(angle) * r;
      const z = Math.sin(angle) * r;
      const y = yJitter;

      positions[i * 3]     = x;
      positions[i * 3 + 1] = y;
      positions[i * 3 + 2] = z;

      baseAngle[i] = angle;
      radii[i] = r;
      heights[i] = y;

      // Color blend: core warm → hot orange → magenta → outer blue
      let c;
      if (r < 1.0) {
        c = COLOR_CORE.clone().lerp(COLOR_HOT, r);
      } else if (r < 3.0) {
        c = COLOR_HOT.clone().lerp(COLOR_MID, (r - 1.0) / 2.0);
      } else {
        c = COLOR_MID.clone().lerp(COLOR_OUT, Math.min(1, (r - 3.0) / 3.0));
      }
      // Dust-lane darkening — slight chance to be a "dust" particle
      if (!isArmStar && Math.random() < 0.35 && r > 1.2 && r < 5.0) {
        c.lerp(COLOR_DUST, 0.55);
      }
      const v = 0.6 + Math.random() * 0.7;
      colors[i * 3]     = c.r * v;
      colors[i * 3 + 1] = c.g * v;
      colors[i * 3 + 2] = c.b * v;

      // Size: bigger near core, occasional bright stars
      const coreBoost = (1 - Math.min(1, r / 6)) * 1.4;
      const bright = Math.random() < 0.025 ? 2.2 : (Math.random() < 0.08 ? 1.4 : 1);
      sizes[i] = (0.4 + Math.random() * 0.5 + coreBoost) * bright;
    }

    const geo = new THREE.BufferGeometry();
    geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
    geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
    geo.setAttribute('aSize', new THREE.BufferAttribute(sizes, 1));

    // Soft star sprite (radial gradient on a canvas) — gives points a real glow
    const sprite = makeRadialTexture([
      'rgba(255,255,255,1)',
      'rgba(255,255,255,0.55)',
      'rgba(255,255,255,0.12)',
      'rgba(255,255,255,0)'
    ]);

    // Custom shader so we can drive per-particle size + soft sprite
    const mat = new THREE.ShaderMaterial({
      uniforms: {
        uTex: { value: sprite },
        uPixelRatio: { value: renderer.getPixelRatio() },
        uBaseSize: { value: 18.0 },
      },
      vertexShader: `
        attribute float aSize;
        varying vec3 vColor;
        uniform float uBaseSize;
        uniform float uPixelRatio;
        void main() {
          vColor = color;
          vec4 mv = modelViewMatrix * vec4(position, 1.0);
          gl_Position = projectionMatrix * mv;
          // perspective-attenuated size
          gl_PointSize = aSize * uBaseSize * uPixelRatio * (1.0 / -mv.z);
        }
      `,
      fragmentShader: `
        uniform sampler2D uTex;
        varying vec3 vColor;
        void main() {
          vec4 t = texture2D(uTex, gl_PointCoord);
          if (t.a < 0.02) discard;
          gl_FragColor = vec4(vColor, 1.0) * t;
        }
      `,
      transparent: true,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
      vertexColors: true,
    });
    const points = new THREE.Points(geo, mat);
    scene.add(points);

    // ---------- Bright galactic core ----------
    const coreSprite = new THREE.Sprite(new THREE.SpriteMaterial({
      map: makeRadialTexture(['rgba(255,240,200,1)','rgba(255,170,100,0.7)','rgba(184,107,255,0.15)','rgba(0,0,0,0)']),
      transparent: true, depthWrite: false, blending: THREE.AdditiveBlending
    }));
    coreSprite.scale.set(3.4, 3.4, 1);
    scene.add(coreSprite);

    const coreOuter = new THREE.Sprite(new THREE.SpriteMaterial({
      map: makeRadialTexture(['rgba(184,107,255,0.45)','rgba(91,141,255,0.12)','rgba(0,0,0,0)']),
      transparent: true, depthWrite: false, blending: THREE.AdditiveBlending
    }));
    coreOuter.scale.set(9, 9, 1);
    scene.add(coreOuter);

    // ---------- Far starfield (parallax background) ----------
    const bgCount = 1200;
    const bgPos = new Float32Array(bgCount * 3);
    for (let i = 0; i < bgCount; i++) {
      const r = 60 + Math.random() * 80;
      const theta = Math.random() * Math.PI * 2;
      const phi = Math.acos(2 * Math.random() - 1);
      bgPos[i*3]   = r * Math.sin(phi) * Math.cos(theta);
      bgPos[i*3+1] = r * Math.sin(phi) * Math.sin(theta);
      bgPos[i*3+2] = r * Math.cos(phi);
    }
    const bgGeo = new THREE.BufferGeometry();
    bgGeo.setAttribute('position', new THREE.BufferAttribute(bgPos, 3));
    const bgStars = new THREE.Points(bgGeo, new THREE.PointsMaterial({
      color: 0xf4eedb, size: 0.18, transparent: true, opacity: 0.7, depthWrite: false,
    }));
    scene.add(bgStars);

    // ---------- Interaction: drag rotate, wheel zoom ----------
    let dragging = false, lastX = 0, lastY = 0;
    let userInteracted = false;
    let interactTimeout = null;
    const markInteract = () => {
      userInteracted = true;
      clearTimeout(interactTimeout);
      interactTimeout = setTimeout(() => { userInteracted = false; }, 3000);
    };
    canvas.addEventListener('pointerdown', (e) => {
      dragging = true; lastX = e.clientX; lastY = e.clientY;
      canvas.setPointerCapture(e.pointerId);
      canvas.style.cursor = 'grabbing';
      markInteract();
    });
    canvas.addEventListener('pointermove', (e) => {
      if (!dragging) return;
      const dx = e.clientX - lastX; const dy = e.clientY - lastY;
      lastX = e.clientX; lastY = e.clientY;
      cam.theta -= dx * 0.005;
      cam.phi   = Math.max(0.25, Math.min(Math.PI - 0.25, cam.phi - dy * 0.005));
      markInteract();
    });
    const endDrag = () => { dragging = false; canvas.style.cursor = 'grab'; };
    canvas.addEventListener('pointerup', endDrag);
    canvas.addEventListener('pointercancel', endDrag);
    canvas.addEventListener('pointerleave', endDrag);
    canvas.style.cursor = 'grab';

    canvas.addEventListener('wheel', (e) => {
      // Only zoom when hero canvas is in foreground (top of page)
      e.preventDefault();
      const factor = Math.exp(e.deltaY * 0.0015);
      cam.radius = Math.max(cam.minRadius, Math.min(cam.maxRadius, cam.radius * factor));
      markInteract();
    }, { passive: false });

    // ---------- Animate ----------
    let last = performance.now();
    function frame() {
      const now = performance.now();
      const dt = Math.min(0.05, (now - last) / 1000);
      last = now;

      // Differential rotation: inner faster, outer slower
      const pos = geo.attributes.position.array;
      for (let i = 0; i < PARTICLES; i++) {
        const r = radii[i];
        const speed = 0.08 / (0.4 + r * 0.18);
        baseAngle[i] += speed * dt;
        const a = baseAngle[i];
        pos[i*3]     = Math.cos(a) * r;
        pos[i*3 + 1] = heights[i];
        pos[i*3 + 2] = Math.sin(a) * r;
      }
      geo.attributes.position.needsUpdate = true;

      // Auto-orbit camera if user idle
      if (!userInteracted && !dragging) {
        cam.theta += dt * 0.04;
      }
      applyCamera();

      // Core breathing
      const breath = 1 + Math.sin(now / 1400) * 0.04;
      coreSprite.scale.set(3.4 * breath, 3.4 * breath, 1);
      // Position the core sprites at the visual offset so they ride with the galaxy
      coreSprite.position.set(0, cam.yOffset, 0);
      coreOuter.position.set(0, cam.yOffset, 0);
      // Move the points cloud up too
      points.position.y = cam.yOffset;
      bgStars.position.y = cam.yOffset;

      renderer.render(scene, camera);
      requestAnimationFrame(frame);
    }
    requestAnimationFrame(frame);

    return { resize };
  }

  /* ====================== NIBIRU SYSTEM (Three.js) ====================== */
  function makeNibiruSystem(canvas, opts) {
    const o = Object.assign({
      withStarfield: true,
      labels: false,
      autoRotate: true,
      cameraDistance: 6.5,
      modules: []
    }, opts || {});

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 200);
    camera.position.set(0, 1.8, o.cameraDistance);
    camera.lookAt(0, 0, 0);

    const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
    renderer.setPixelRatio(Math.min(2, devicePixelRatio));
    renderer.setClearColor(0x000000, 0);

    // Sizing
    const resize = () => {
      const r = canvas.getBoundingClientRect();
      const w = Math.max(2, r.width);
      const h = Math.max(2, r.height);
      camera.aspect = w / h;
      camera.updateProjectionMatrix();
      renderer.setSize(w, h, false);
    };
    resize();

    // Starfield
    if (o.withStarfield) {
      const starGeo = new THREE.BufferGeometry();
      const starCount = 1200;
      const positions = new Float32Array(starCount * 3);
      const sizes = new Float32Array(starCount);
      for (let i = 0; i < starCount; i++) {
        const r = 30 + Math.random() * 50;
        const theta = Math.random() * Math.PI * 2;
        const phi = Math.acos(2 * Math.random() - 1);
        positions[i*3]   = r * Math.sin(phi) * Math.cos(theta);
        positions[i*3+1] = r * Math.sin(phi) * Math.sin(theta);
        positions[i*3+2] = r * Math.cos(phi);
        sizes[i] = Math.random() < 0.05 ? 0.06 : 0.02 + Math.random() * 0.02;
      }
      starGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
      starGeo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
      const starMat = new THREE.PointsMaterial({
        color: 0xf4eedb, size: 0.04, sizeAttenuation: true,
        transparent: true, opacity: 0.85, depthWrite: false
      });
      const stars = new THREE.Points(starGeo, starMat);
      scene.add(stars);
    }

    // === Center: the Model star ===
    // Layered glow: bright core + soft halo
    const coreGeo = new THREE.SphereGeometry(0.32, 48, 48);
    const coreMat = new THREE.MeshBasicMaterial({ color: 0xfff2d6 });
    const core = new THREE.Mesh(coreGeo, coreMat);
    scene.add(core);

    // Halo (sprite)
    const haloTex = makeRadialTexture(['rgba(255,228,170,1)','rgba(255,150,90,0.6)','rgba(184,107,255,0.0)']);
    const haloMat = new THREE.SpriteMaterial({ map: haloTex, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending });
    const halo = new THREE.Sprite(haloMat);
    halo.scale.set(2.2, 2.2, 1);
    scene.add(halo);

    // Outer corona
    const corona = new THREE.Sprite(new THREE.SpriteMaterial({
      map: makeRadialTexture(['rgba(184,107,255,0.5)','rgba(91,141,255,0.15)','rgba(0,0,0,0)']),
      transparent: true, depthWrite: false, blending: THREE.AdditiveBlending
    }));
    corona.scale.set(5.2, 5.2, 1);
    scene.add(corona);

    // Orbits group
    const orbits = new THREE.Group();
    scene.add(orbits);

    // Modules
    const moduleObjs = [];
    function buildOrbit(radius) {
      const seg = 128;
      const pts = [];
      for (let i = 0; i <= seg; i++) {
        const a = (i / seg) * Math.PI * 2;
        pts.push(new THREE.Vector3(Math.cos(a) * radius, 0, Math.sin(a) * radius));
      }
      const g = new THREE.BufferGeometry().setFromPoints(pts);
      const m = new THREE.LineBasicMaterial({ color: 0xb86bff, transparent: true, opacity: 0.18 });
      return new THREE.Line(g, m);
    }
    function addModule(spec) {
      const orbit = buildOrbit(spec.radius);
      orbits.add(orbit);

      const mGeo = new THREE.SphereGeometry(spec.size || 0.14, 24, 24);
      const mMat = new THREE.MeshBasicMaterial({ color: spec.color });
      const mesh = new THREE.Mesh(mGeo, mMat);

      // glow sprite
      const glow = new THREE.Sprite(new THREE.SpriteMaterial({
        map: makeRadialTexture([
          `rgba(${rgb(spec.color)},0.8)`,
          `rgba(${rgb(spec.color)},0.2)`,
          `rgba(0,0,0,0)`
        ]),
        transparent: true, depthWrite: false, blending: THREE.AdditiveBlending
      }));
      glow.scale.set(spec.size * 5, spec.size * 5, 1);

      const node = new THREE.Group();
      node.add(mesh); node.add(glow);
      // initial position (animated to orbit on add)
      node.position.set(Math.cos(spec.angle) * spec.radius, 0, Math.sin(spec.angle) * spec.radius);
      // arrival animation
      node.scale.setScalar(0.001);
      scene.add(node);

      const obj = {
        spec, node, mesh, glow, orbit,
        angle: spec.angle, radius: spec.radius,
        speed: 0.12 + Math.random() * 0.08,
        arriveStart: performance.now(),
        arriveDur: 700,
      };
      moduleObjs.push(obj);
      return obj;
    }
    o.modules.forEach(addModule);

    // Gravity lines (controller "field")
    const linesGroup = new THREE.Group();
    scene.add(linesGroup);
    function rebuildLines() {
      while (linesGroup.children.length) linesGroup.remove(linesGroup.children[0]);
      moduleObjs.forEach(m => {
        const g = new THREE.BufferGeometry().setFromPoints([
          new THREE.Vector3(0,0,0),
          m.node.position.clone()
        ]);
        const mat = new THREE.LineBasicMaterial({ color: 0xb86bff, transparent: true, opacity: 0.0 });
        const line = new THREE.Line(g, mat);
        line._target = m;
        linesGroup.add(line);
      });
    }
    rebuildLines();

    // Tooltip
    const tip = document.getElementById('moduleTip');
    const raycaster = hasThree ? new THREE.Raycaster() : null;
    const pointer = new THREE.Vector2();
    let hovered = null;
    function onMove(e) {
      if (!o.labels || !tip) return;
      const r = canvas.getBoundingClientRect();
      pointer.x = ((e.clientX - r.left) / r.width) * 2 - 1;
      pointer.y = -((e.clientY - r.top) / r.height) * 2 + 1;
    }
    canvas.addEventListener('mousemove', onMove);
    canvas.addEventListener('mouseleave', () => {
      hovered = null;
      if (tip) tip.classList.remove('visible');
    });

    // === Focus / camera presets ===
    let cameraTarget = { pos: camera.position.clone(), look: new THREE.Vector3(0,0,0) };
    let cameraEase = 0.06;
    function focus(step) {
      // step: 0=Model, 1=AI, 2=Module, 3=Controller (lines on), 4=View (pull back)
      switch (step) {
        case 0:
          // MODEL — close on the centre
          cameraTarget = { pos: new THREE.Vector3(0, 1.0, 3.5), look: new THREE.Vector3(0,0,0) };
          linesVisible = 0;
          break;
        case 1:
          // AI — slight orbit, slightly elevated, gravity lines start to flicker on
          cameraTarget = { pos: new THREE.Vector3(0.8, 1.6, 3.8), look: new THREE.Vector3(0, 0.2, 0) };
          linesVisible = 0.25;
          break;
        case 2:
          // MODULE — offset right, looking at orbiting module nodes
          cameraTarget = { pos: new THREE.Vector3(2.6, 1.2, 3.6), look: new THREE.Vector3(1.6, 0, 0) };
          linesVisible = 0;
          break;
        case 3:
          // CONTROLLER — high up, looking down, all gravity lines visible
          cameraTarget = { pos: new THREE.Vector3(0, 4.8, 6.0), look: new THREE.Vector3(0,0,0) };
          linesVisible = 1;
          break;
        case 4:
        default:
          // VIEW — pulled back, full system in frame
          cameraTarget = { pos: new THREE.Vector3(0, 1.8, o.cameraDistance), look: new THREE.Vector3(0,0,0) };
          linesVisible = 0.6;
          break;
      }
    }
    let linesVisible = 0;

    function pulseGravity() {
      linesGroup.children.forEach(l => {
        l.material.opacity = Math.min(1, l.material.opacity + 0.4);
      });
    }
    function flashCenter() {
      core.scale.setScalar(1.4);
      halo.scale.set(3.0, 3.0, 1);
    }
    function reset() {
      while (moduleObjs.length) {
        const m = moduleObjs.pop();
        scene.remove(m.node);
        orbits.remove(m.orbit);
      }
      rebuildLines();
    }

    // Animate
    let t0 = performance.now();
    function frame() {
      const t = performance.now();
      const dt = Math.min(0.05, (t - t0) / 1000);
      t0 = t;

      // Module orbit + arrival animation
      moduleObjs.forEach(m => {
        m.angle += m.speed * dt;
        const x = Math.cos(m.angle) * m.radius;
        const z = Math.sin(m.angle) * m.radius;
        m.node.position.set(x, 0, z);
        // arrival ease
        const p = Math.min(1, (t - m.arriveStart) / m.arriveDur);
        const e = 1 - Math.pow(1 - p, 3);
        m.node.scale.setScalar(e);
      });

      // Core breathing
      const breath = 1 + Math.sin(t / 700) * 0.04;
      core.scale.lerp(new THREE.Vector3(breath, breath, breath), 0.1);
      halo.scale.lerp(new THREE.Vector3(2.2 * breath, 2.2 * breath, 1), 0.1);

      // Camera ease
      camera.position.lerp(cameraTarget.pos, cameraEase);
      const lookCurrent = new THREE.Vector3();
      camera.getWorldDirection(lookCurrent);
      const desiredDir = cameraTarget.look.clone().sub(camera.position).normalize();
      // Smooth lookAt by easing the camera's quaternion toward a temp lookAt camera
      const tmp = new THREE.Object3D();
      tmp.position.copy(camera.position);
      tmp.lookAt(cameraTarget.look);
      camera.quaternion.slerp(tmp.quaternion, cameraEase);

      // Auto rotate around Y by orbiting target slightly
      if (o.autoRotate) {
        const ang = t / 12000;
        const offset = new THREE.Vector3(Math.sin(ang) * 0.4, 0, 0);
        camera.position.add(offset.multiplyScalar(0.001));
      }

      // Gravity lines opacity
      linesGroup.children.forEach(l => {
        const target = linesVisible;
        l.material.opacity += (target - l.material.opacity) * 0.05;
        // refresh geometry to current module pos
        if (l._target) {
          const pts = [new THREE.Vector3(0,0,0), l._target.node.position.clone()];
          l.geometry.setFromPoints(pts);
        }
      });

      // Hover detection
      if (o.labels && tip && raycaster) {
        raycaster.setFromCamera(pointer, camera);
        const meshes = moduleObjs.map(m => m.mesh);
        const hits = raycaster.intersectObjects(meshes, false);
        if (hits.length) {
          const m = moduleObjs.find(m => m.mesh === hits[0].object);
          if (m && m !== hovered) {
            hovered = m;
            tip.querySelector('.name').textContent = m.spec.name;
            tip.querySelector('.sig').textContent = m.spec.sig || '';
            tip.classList.add('visible');
          }
          if (hovered) {
            // project module position to screen
            const v = hovered.node.position.clone().project(camera);
            const r = canvas.getBoundingClientRect();
            const sx = (v.x * 0.5 + 0.5) * r.width + r.left;
            const sy = (-v.y * 0.5 + 0.5) * r.height + r.top;
            tip.style.left = sx + 'px';
            tip.style.top = sy + 'px';
            canvas.style.cursor = 'pointer';
          }
        } else if (hovered) {
          hovered = null;
          tip.classList.remove('visible');
          canvas.style.cursor = '';
        }
      }

      renderer.render(scene, camera);
      requestAnimationFrame(frame);
    }
    requestAnimationFrame(frame);

    window.addEventListener('resize', resize);

    return { focus, addModule, reset, resize, pulseGravity, flashCenter };
  }

  function rgb(hex) {
    const r = (hex >> 16) & 255;
    const g = (hex >> 8) & 255;
    const b = hex & 255;
    return `${r},${g},${b}`;
  }
  function makeRadialTexture(stops) {
    const c = document.createElement('canvas');
    c.width = c.height = 128;
    const ctx = c.getContext('2d');
    const g = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);
    stops.forEach((s, i) => g.addColorStop(i / (stops.length - 1), s));
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, 128, 128);
    const tex = new THREE.CanvasTexture(c);
    tex.needsUpdate = true;
    return tex;
  }

})();
