// ============================================================
// SHARED EFFECTS — constellation canvas + animated logo
// Used across hero and (optionally) as ambient background
// ============================================================

const { useEffect, useRef, useState } = React;

function Constellation({ density, speed, accent, glow, dark, motion }) {
  const canvasRef = useRef(null);
  const rafRef = useRef(null);
  const stateRef = useRef({ nodes: [], pulses: [], t: 0, w: 0, h: 0, dpr: 1 });

  useEffect(() => {
    const canvas = canvasRef.current;
    const ctx = canvas.getContext("2d");
    const dpr = Math.min(window.devicePixelRatio || 1, 2);

    function resize() {
      const w = canvas.clientWidth;
      const h = canvas.clientHeight;
      canvas.width = w * dpr;
      canvas.height = h * dpr;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      stateRef.current.w = w;
      stateRef.current.h = h;
      stateRef.current.dpr = dpr;
      seed();
    }

    function seed() {
      const { w, h } = stateRef.current;
      const count = Math.round((w * h) / 28000 * density);
      const nodes = [];
      for (let i = 0; i < count; i++) {
        nodes.push({
          x: Math.random() * w,
          y: Math.random() * h,
          vx: (Math.random() - 0.5) * 0.12 * speed,
          vy: (Math.random() - 0.5) * 0.12 * speed,
          r: 1.2 + Math.random() * 1.8,
          phase: Math.random() * Math.PI * 2,
          tone: Math.random() < 0.18 ? 1 : 0,
        });
      }
      stateRef.current.nodes = nodes;
      stateRef.current.pulses = [];
    }

    function spawnPulse() {
      const { nodes } = stateRef.current;
      if (nodes.length < 2) return;
      const a = nodes[(Math.random() * nodes.length) | 0];
      let best = null;
      let bestD = Infinity;
      for (let k = 0; k < 8; k++) {
        const b = nodes[(Math.random() * nodes.length) | 0];
        if (b === a) continue;
        const d = (a.x - b.x) ** 2 + (a.y - b.y) ** 2;
        if (d < bestD && d > 200) {
          bestD = d;
          best = b;
        }
      }
      if (!best) return;
      stateRef.current.pulses.push({ a, b: best, p: 0, life: 1 });
    }

    function tick() {
      const s = stateRef.current;
      const { w, h, nodes, pulses } = s;
      s.t += 1;
      ctx.clearRect(0, 0, w, h);

      const grad = ctx.createRadialGradient(w * 0.5, h * 0.55, 0, w * 0.5, h * 0.55, Math.max(w, h) * 0.7);
      if (dark) {
        grad.addColorStop(0, "rgba(38, 52, 88, 0.35)");
        grad.addColorStop(1, "rgba(10, 16, 32, 0)");
      } else {
        grad.addColorStop(0, "rgba(245, 193, 78, 0.08)");
        grad.addColorStop(1, "rgba(245, 193, 78, 0)");
      }
      ctx.fillStyle = grad;
      ctx.fillRect(0, 0, w, h);

      if (motion) {
        for (const n of nodes) {
          n.x += n.vx;
          n.y += n.vy;
          if (n.x < -20) n.x = w + 20;
          if (n.x > w + 20) n.x = -20;
          if (n.y < -20) n.y = h + 20;
          if (n.y > h + 20) n.y = -20;
        }
      }

      const linkDist = 140;
      for (let i = 0; i < nodes.length; i++) {
        const a = nodes[i];
        for (let j = i + 1; j < nodes.length; j++) {
          const b = nodes[j];
          const dx = a.x - b.x;
          const dy = a.y - b.y;
          const d2 = dx * dx + dy * dy;
          if (d2 < linkDist * linkDist) {
            const alpha = (1 - Math.sqrt(d2) / linkDist) * 0.22;
            ctx.strokeStyle = dark
              ? `rgba(170, 195, 235, ${alpha})`
              : `rgba(60, 80, 130, ${alpha})`;
            ctx.lineWidth = 0.6;
            ctx.beginPath();
            ctx.moveTo(a.x, a.y);
            ctx.lineTo(b.x, b.y);
            ctx.stroke();
          }
        }
      }

      for (const n of nodes) {
        const pulse = motion ? 0.65 + 0.35 * Math.sin(s.t * 0.03 + n.phase) : 0.85;
        const color = n.tone ? accent : (dark ? "#cfd9f0" : "#1a2238");
        if (glow) {
          ctx.shadowBlur = n.tone ? 12 : 6;
          ctx.shadowColor = n.tone ? accent : (dark ? "#7aa3ff" : "#aab8d8");
        }
        ctx.fillStyle = color;
        ctx.globalAlpha = n.tone ? 0.95 * pulse : 0.7 * pulse;
        ctx.beginPath();
        ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.shadowBlur = 0;
      ctx.globalAlpha = 1;

      if (motion) {
        if (s.t % Math.max(8, 30 - density * 4) === 0) spawnPulse();
        for (let i = pulses.length - 1; i >= 0; i--) {
          const p = pulses[i];
          p.p += 0.012 * speed;
          if (p.p >= 1) {
            pulses.splice(i, 1);
            continue;
          }
          const x = p.a.x + (p.b.x - p.a.x) * p.p;
          const y = p.a.y + (p.b.y - p.a.y) * p.p;
          ctx.fillStyle = accent;
          ctx.shadowBlur = 16;
          ctx.shadowColor = accent;
          ctx.globalAlpha = Math.sin(p.p * Math.PI);
          ctx.beginPath();
          ctx.arc(x, y, 2.2, 0, Math.PI * 2);
          ctx.fill();
        }
      }
      ctx.shadowBlur = 0;
      ctx.globalAlpha = 1;

      rafRef.current = requestAnimationFrame(tick);
    }

    resize();
    rafRef.current = requestAnimationFrame(tick);
    window.addEventListener("resize", resize);
    return () => {
      cancelAnimationFrame(rafRef.current);
      window.removeEventListener("resize", resize);
    };
  }, [density, speed, accent, glow, dark, motion]);

  return (
    <canvas
      ref={canvasRef}
      style={{
        position: "absolute",
        inset: 0,
        width: "100%",
        height: "100%",
        display: "block",
        pointerEvents: "none",
      }}
    />
  );
}

function AnimatedLogo({ size, drawIn, breathe, drift, motion, dark = true }) {
  const { isMobile } = window.useViewport();
  const [t, setT] = useState(0);

  useEffect(() => {
    if (!motion) return;
    let raf, start;
    const loop = (ts) => {
      if (!start) start = ts;
      setT((ts - start) / 1000);
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(raf);
  }, [motion]);

  const breatheScale = breathe && motion ? 1 + 0.015 * Math.sin(t * 0.8) : 1;
  const driftX = drift && motion ? Math.sin(t * 0.35) * 5 : 0;
  const driftY = drift && motion ? Math.cos(t * 0.28) * 4 : 0;

  return (
    <div
      style={{
        position: "relative",
        width: size,
        height: size,
        transform: `translate(${driftX}px, ${driftY}px) scale(${breatheScale})`,
        transition: "transform 60ms linear",
      }}
    >
      {!isMobile && (
        <div
          style={{
            position: "absolute",
            inset: "-25%",
            background:
              "radial-gradient(circle at 50% 50%, rgba(245,193,78,0.22), rgba(245,193,78,0.04) 40%, transparent 65%)",
            filter: "blur(12px)",
            pointerEvents: "none",
          }}
        />
      )}
      <img
        src={dark ? "../logos/BRAINS Logo (Midnight).png" : "../logos/BRAINS LOGO (Bone).png"}
        alt="BRAINS Infiniti"
        style={{
          position: "absolute",
          inset: 0,
          width: "100%",
          height: "100%",
          objectFit: "contain",
          filter: isMobile
            ? "none"
            : dark
              ? "drop-shadow(0 0 22px rgba(245,193,78,0.40)) drop-shadow(0 6px 30px rgba(15,22,46,0.55))"
              : "drop-shadow(0 0 22px rgba(226,168,42,0.30)) drop-shadow(0 6px 30px rgba(22,26,43,0.30))",
        }}
      />
      {motion && (
        <div
          style={{
            position: "absolute",
            left: "50%",
            top: "50%",
            width: 6,
            height: 6,
            marginLeft: -3,
            marginTop: -3,
            borderRadius: "50%",
            background: "#f5c14e",
            boxShadow: "0 0 14px 2px rgba(245,193,78,0.9)",
            transform: `translate(${Math.cos(t * 1.1) * size * 0.42}px, ${Math.sin(t * 1.1) * size * 0.42}px)`,
            opacity: 0.85,
            pointerEvents: "none",
          }}
        />
      )}
    </div>
  );
}

// ============================================================
// CONSTELLATION 3D — a true-depth neural field rendered in
// three.js. Same prop API as the 2D canvas (density, speed,
// accent, glow, dark, motion) so heroes swap in without edits.
// Nodes float in a slab of space, hairline edges connect close
// pairs, and gold signal pulses travel the network. The whole
// field tilts gently toward the pointer.
// ============================================================

function Constellation3D({ density = 1, speed = 1, accent = "#f5c14e", glow = true, dark = true, motion = true }) {
  const mountRef = React.useRef(null);
  const [failed, setFailed] = React.useState(false);

  React.useEffect(() => {
    if (!window.THREE || !mountRef.current) {
      setFailed(true);
      return;
    }
    const THREE = window.THREE;
    const mount = mountRef.current;

    let renderer;
    try {
      renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    } catch (e) {
      setFailed(true);
      return;
    }
    const dpr = Math.min(window.devicePixelRatio || 1, 1.75);
    renderer.setPixelRatio(dpr);

    const sizeToMount = () => {
      const w = mount.clientWidth || 1;
      const h = mount.clientHeight || 1;
      renderer.setSize(w, h);
      camera.aspect = w / h;
      camera.updateProjectionMatrix();
    };

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(60, 1, 0.1, 60);
    camera.position.set(0, 0, 9);

    mount.appendChild(renderer.domElement);

    const accentColor = new THREE.Color(accent);
    const baseColor = new THREE.Color(dark ? "#cfd9f0" : "#1a2238");
    const group = new THREE.Group();
    scene.add(group);

    // Deterministic node placement.
    const seeded = (i) => {
      const x = Math.sin(i * 127.1 + 311.7) * 43758.5453;
      return x - Math.floor(x);
    };
    const COUNT = Math.round(110 * density);
    const nodes = [];
    for (let i = 0; i < COUNT; i++) {
      nodes.push(
        new THREE.Vector3(
          (seeded(i * 3 + 1) - 0.5) * 22,
          (seeded(i * 3 + 2) - 0.5) * 11,
          -1 - seeded(i * 3 + 3) * 8
        )
      );
    }

    // Base + accent point clouds.
    const isAccent = nodes.map((_, i) => seeded(i + 777) < 0.18);
    const mkPoints = (filterAccent, color, size, opacity) => {
      const sel = nodes.filter((_, i) => isAccent[i] === filterAccent);
      const arr = new Float32Array(sel.length * 3);
      sel.forEach((v, i) => {
        arr[i * 3] = v.x;
        arr[i * 3 + 1] = v.y;
        arr[i * 3 + 2] = v.z;
      });
      const g = new THREE.BufferGeometry();
      g.setAttribute("position", new THREE.BufferAttribute(arr, 3));
      const m = new THREE.PointsMaterial({
        color,
        size,
        transparent: true,
        opacity,
        sizeAttenuation: true,
      });
      const p = new THREE.Points(g, m);
      group.add(p);
      return { g, m };
    };
    const basePts = mkPoints(false, baseColor, 0.07, dark ? 0.65 : 0.7);
    const accentPts = mkPoints(true, accentColor, glow ? 0.13 : 0.09, 0.95);

    // Edges between close pairs (capped).
    const edges = [];
    const MAXD = 3.4;
    for (let i = 0; i < COUNT && edges.length < 260; i++) {
      for (let j = i + 1; j < COUNT && edges.length < 260; j++) {
        if (nodes[i].distanceTo(nodes[j]) < MAXD) edges.push([i, j]);
      }
    }
    const edgeArr = new Float32Array(edges.length * 6);
    edges.forEach(([a, b], k) => {
      edgeArr[k * 6] = nodes[a].x;
      edgeArr[k * 6 + 1] = nodes[a].y;
      edgeArr[k * 6 + 2] = nodes[a].z;
      edgeArr[k * 6 + 3] = nodes[b].x;
      edgeArr[k * 6 + 4] = nodes[b].y;
      edgeArr[k * 6 + 5] = nodes[b].z;
    });
    const edgeGeo = new THREE.BufferGeometry();
    edgeGeo.setAttribute("position", new THREE.BufferAttribute(edgeArr, 3));
    const edgeMat = new THREE.LineBasicMaterial({
      color: dark ? 0xaac3eb : 0x3c5082,
      transparent: true,
      opacity: dark ? 0.14 : 0.18,
    });
    const edgeLines = new THREE.LineSegments(edgeGeo, edgeMat);
    group.add(edgeLines);

    // Signal pulses — small bright points traveling along random edges.
    const PULSES = Math.max(4, Math.round(7 * density));
    const pulseArr = new Float32Array(PULSES * 3);
    const pulseGeo = new THREE.BufferGeometry();
    pulseGeo.setAttribute("position", new THREE.BufferAttribute(pulseArr, 3));
    const pulseMat = new THREE.PointsMaterial({
      color: accentColor,
      size: glow ? 0.22 : 0.15,
      transparent: true,
      opacity: 0.95,
      sizeAttenuation: true,
    });
    const pulsePts = new THREE.Points(pulseGeo, pulseMat);
    group.add(pulsePts);
    const pulses = Array.from({ length: PULSES }, (_, i) => ({
      edge: edges.length ? (i * 37) % edges.length : 0,
      t: seeded(i + 99),
      v: 0.004 + seeded(i + 55) * 0.006,
    }));

    // Pointer tilt.
    const target = { x: 0, y: 0 };
    const onPointer = (e) => {
      target.y = (e.clientX / window.innerWidth - 0.5) * 0.22;
      target.x = (e.clientY / window.innerHeight - 0.5) * 0.14;
    };
    if (motion) window.addEventListener("pointermove", onPointer);

    sizeToMount();

    let raf = 0;
    let t = 0;
    const eased = { x: 0, y: 0 };
    const v = new THREE.Vector3();

    const frame = () => {
      t += 0.016 * speed;
      eased.x += (target.x - eased.x) * 0.03;
      eased.y += (target.y - eased.y) * 0.03;
      group.rotation.y = Math.sin(t * 0.05) * 0.08 + eased.y;
      group.rotation.x = eased.x;
      group.position.y = Math.sin(t * 0.12) * 0.2;

      if (edges.length) {
        const pos = pulseGeo.attributes.position;
        pulses.forEach((p, i) => {
          p.t += p.v * speed;
          if (p.t >= 1) {
            p.t = 0;
            p.edge = Math.floor(seeded(t * 1000 + i) * edges.length) % edges.length;
          }
          const [a, b] = edges[p.edge];
          v.copy(nodes[a]).lerp(nodes[b], p.t);
          pos.setXYZ(i, v.x, v.y, v.z);
        });
        pos.needsUpdate = true;
      }

      renderer.render(scene, camera);
      raf = requestAnimationFrame(frame);
    };

    if (motion) {
      raf = requestAnimationFrame(frame);
    } else {
      group.rotation.y = 0.04;
      renderer.render(scene, camera);
    }

    let ro;
    if ("ResizeObserver" in window) {
      ro = new ResizeObserver(sizeToMount);
      ro.observe(mount);
    } else {
      window.addEventListener("resize", sizeToMount);
    }

    return () => {
      cancelAnimationFrame(raf);
      if (motion) window.removeEventListener("pointermove", onPointer);
      if (ro) ro.disconnect();
      else window.removeEventListener("resize", sizeToMount);
      basePts.g.dispose();
      basePts.m.dispose();
      accentPts.g.dispose();
      accentPts.m.dispose();
      edgeGeo.dispose();
      edgeMat.dispose();
      pulseGeo.dispose();
      pulseMat.dispose();
      renderer.dispose();
      if (renderer.domElement.parentNode === mount) mount.removeChild(renderer.domElement);
    };
  }, [density, speed, accent, glow, dark, motion]);

  if (failed || !window.THREE) {
    return <Constellation density={density} speed={speed} accent={accent} glow={glow} dark={dark} motion={motion} />;
  }

  return (
    <div
      ref={mountRef}
      aria-hidden
      style={{
        position: "absolute",
        inset: 0,
        pointerEvents: "none",
      }}
    />
  );
}

// ============================================================
// INFINITY MARK 3D — the BRAINS Infiniti logo rebuilt as a
// gold lemniscate in three.js: a metallic infinity ribbon with
// the badge's network nodes along the curve and a signal pulse
// traveling the loop. Rocks gently + tilts toward the pointer.
// Falls back to the PNG AnimatedLogo when WebGL is unavailable.
// ============================================================

function InfinityMark3D({ size = 480, motion = true, dark = true }) {
  const mountRef = React.useRef(null);
  const [failed, setFailed] = React.useState(false);

  React.useEffect(() => {
    if (!window.THREE || !mountRef.current) {
      setFailed(true);
      return;
    }
    const THREE = window.THREE;
    const mount = mountRef.current;

    let renderer;
    try {
      renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    } catch (e) {
      setFailed(true);
      return;
    }
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    renderer.setPixelRatio(dpr);
    renderer.setSize(size, size);
    renderer.outputEncoding = THREE.sRGBEncoding;
    mount.appendChild(renderer.domElement);

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(36, 1, 0.1, 50);
    // Far enough back that the full ~5.8-wide infinity (plus nodes, breathing,
    // and pointer tilt) sits inside the frame with margin on every side.
    camera.position.set(0, 0, 10.6);

    const group = new THREE.Group();
    scene.add(group);

    // Lemniscate of Bernoulli, gently lifted out of plane so the
    // ribbon reads as 3D when it rocks.
    class Lemniscate extends THREE.Curve {
      getPoint(t) {
        const a = 2.05;
        const ang = t * Math.PI * 2;
        const denom = 1 + Math.sin(ang) * Math.sin(ang);
        const x = (a * Math.SQRT2 * Math.cos(ang)) / denom;
        const y = (a * Math.SQRT2 * Math.cos(ang) * Math.sin(ang)) / denom;
        const z = Math.sin(ang) * 0.22;
        return new THREE.Vector3(x, y, z);
      }
    }
    const curve = new Lemniscate();

    // Brand gold — flat #f5c14e like the logo, not brassy metal.
    const GOLD = 0xf5c14e;

    // Slim guide ribbon tracing the infinity path.
    const tubeGeo = new THREE.TubeGeometry(curve, 220, 0.042, 10, true);
    const tubeMat = new THREE.MeshStandardMaterial({
      color: GOLD,
      metalness: 0.25,
      roughness: 0.55,
      emissive: GOLD,
      emissiveIntensity: 0.3,
    });
    const ribbon = new THREE.Mesh(tubeGeo, tubeMat);
    group.add(ribbon);

    // Network nodes along the curve — more of them, varied sizes,
    // echoing the certification-badge motif.
    const seeded = (i) => {
      const x = Math.sin(i * 127.1 + 311.7) * 43758.5453;
      return x - Math.floor(x);
    };
    const NODES = 22;
    const nodeMat = new THREE.MeshStandardMaterial({
      color: GOLD,
      metalness: 0.3,
      roughness: 0.4,
      emissive: GOLD,
      emissiveIntensity: 0.55,
    });
    const nodeMeshes = [];
    const nodePts = [];
    for (let i = 0; i < NODES; i++) {
      const r = 0.05 + seeded(i) * 0.05;
      const g = new THREE.SphereGeometry(r, 12, 12);
      const m = new THREE.Mesh(g, nodeMat);
      const p = curve.getPoint(i / NODES);
      m.position.copy(p);
      group.add(m);
      nodeMeshes.push(m);
      nodePts.push(p);
    }

    // Cross-link chords — straight hairlines between non-adjacent nodes
    // that sit near each other in space. This is what makes it read as a
    // network rather than a solid ring.
    const chordPositions = [];
    for (let i = 0; i < NODES; i++) {
      for (let j = i + 2; j < NODES; j++) {
        const apart = Math.min(j - i, NODES - (j - i)); // curve-distance
        if (apart < 3) continue; // skip near-neighbours (the ribbon covers those)
        if (nodePts[i].distanceTo(nodePts[j]) < 2.3) {
          chordPositions.push(nodePts[i].x, nodePts[i].y, nodePts[i].z, nodePts[j].x, nodePts[j].y, nodePts[j].z);
        }
      }
    }
    const chordGeo = new THREE.BufferGeometry();
    chordGeo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(chordPositions), 3));
    const chordMat = new THREE.LineBasicMaterial({ color: GOLD, transparent: true, opacity: 0.32 });
    const chords = new THREE.LineSegments(chordGeo, chordMat);
    group.add(chords);

    // Three signal pulses traveling the loop at different speeds.
    const pulseGeo = new THREE.SphereGeometry(0.11, 12, 12);
    const pulseMat = new THREE.MeshBasicMaterial({ color: 0xfff3cf, transparent: true, opacity: 0.95 });
    const pulses = Array.from({ length: 3 }, (_, i) => {
      const m = new THREE.Mesh(pulseGeo, pulseMat);
      group.add(m);
      return { mesh: m, t: i / 3, v: 0.0022 + i * 0.0009 };
    });

    // Lighting — white key so the gold keeps its true hue, cool rim.
    scene.add(new THREE.AmbientLight(0xffffff, dark ? 0.5 : 0.7));
    const key = new THREE.PointLight(0xffffff, 1.0, 20);
    key.position.set(3, 2.5, 4);
    scene.add(key);
    const rim = new THREE.PointLight(dark ? 0x7aa3ff : 0xffffff, 0.6, 20);
    rim.position.set(-3.5, -2, 3);
    scene.add(rim);

    const target = { x: 0, y: 0 };
    const onPointer = (e) => {
      target.y = (e.clientX / window.innerWidth - 0.5) * 0.5;
      target.x = (e.clientY / window.innerHeight - 0.5) * 0.3;
    };
    if (motion) window.addEventListener("pointermove", onPointer);

    let raf = 0;
    let t = 0;
    const eased = { x: 0, y: 0 };

    const renderOnce = () => {
      group.rotation.y = 0.3;
      group.rotation.x = 0.12;
      renderer.render(scene, camera);
    };

    const tick = () => {
      t += 0.016;
      eased.x += (target.x - eased.x) * 0.04;
      eased.y += (target.y - eased.y) * 0.04;
      // gentle rock — the mark stays readable, never fully spins
      group.rotation.y = Math.sin(t * 0.32) * 0.38 + eased.y;
      group.rotation.x = Math.sin(t * 0.21) * 0.14 + eased.x;
      const breathe = 1 + Math.sin(t * 0.8) * 0.014;
      group.scale.setScalar(breathe);
      group.position.y = Math.sin(t * 0.35) * 0.1;

      pulses.forEach((p) => {
        p.t = (p.t + p.v) % 1;
        p.mesh.position.copy(curve.getPoint(p.t));
      });
      pulseMat.opacity = 0.65 + Math.sin(t * 4) * 0.3;

      nodeMeshes.forEach((m, i) => {
        const s = 1 + Math.sin(t * 1.6 + i) * 0.18;
        m.scale.setScalar(s);
      });

      key.position.x = Math.cos(t * 0.25) * 3.6;
      key.position.z = Math.sin(t * 0.25) * 2.4 + 2.4;

      renderer.render(scene, camera);
      raf = requestAnimationFrame(tick);
    };

    if (motion) raf = requestAnimationFrame(tick);
    else renderOnce();

    let observer;
    if (motion && "IntersectionObserver" in window) {
      observer = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            if (!raf) raf = requestAnimationFrame(tick);
          } else {
            cancelAnimationFrame(raf);
            raf = 0;
          }
        });
      });
      observer.observe(mount);
    }

    return () => {
      cancelAnimationFrame(raf);
      if (observer) observer.disconnect();
      if (motion) window.removeEventListener("pointermove", onPointer);
      tubeGeo.dispose();
      tubeMat.dispose();
      nodeMeshes.forEach((m) => m.geometry.dispose());
      nodeMat.dispose();
      chordGeo.dispose();
      chordMat.dispose();
      pulseGeo.dispose();
      pulseMat.dispose();
      renderer.dispose();
      if (renderer.domElement.parentNode === mount) mount.removeChild(renderer.domElement);
    };
  }, [size, motion, dark]);

  if (failed || !window.THREE) {
    return (
      <AnimatedLogo size={size} drawIn={motion} breathe={motion} drift={motion} motion={motion} dark={dark} />
    );
  }

  return (
    <div style={{ position: "relative", width: size, height: size }}>
      <div
        aria-hidden
        style={{
          position: "absolute",
          inset: "-15%",
          background:
            "radial-gradient(circle at 50% 50%, rgba(245,193,78,0.20), rgba(245,193,78,0.04) 40%, transparent 65%)",
          filter: "blur(14px)",
          pointerEvents: "none",
        }}
      />
      <div
        ref={mountRef}
        role="img"
        aria-label="BRAINS Infiniti mark — golden infinity loop"
        style={{ position: "relative", width: size, height: size }}
      />
    </div>
  );
}

// Constellation stays 2D (preferred). The 3D variant remains available
// as window.Constellation3D if ever wanted.
window.Constellation = Constellation;
window.Constellation3D = Constellation3D;
window.InfinityMark3D = InfinityMark3D;
window.AnimatedLogo = AnimatedLogo;
