/* eslint-disable */
/* Globe — Vecteezy "Tech Earth Globalization" style:
   - Continents rendered as dot clusters (sampled from blue-marble land mask)
   - Faint wireframe latitude/longitude lines
   - Slow auto-rotation + drag-to-rotate
   - Persistent trail curves drawn behind packets, removed when rotated out of view
   - Cyan-blue tech palette
   - Cursor-bridge: 0/1 digits get pulled into the nearest visible city/packet
*/
const Globe = () => {
  const mountRef = React.useRef(null);

  React.useEffect(() => {
    const mount = mountRef.current;
    if (!mount) return;

    let alive = true;
    let cleanup = () => {};

    (async () => {
      const waitForLib = () => new Promise((resolve) => {
        if (window.THREE_LIB) return resolve(window.THREE_LIB);
        const t = setInterval(() => {
          if (window.THREE_LIB) {clearInterval(t);resolve(window.THREE_LIB);}
        }, 30);
        window.addEventListener("three-lib-ready", () => {
          clearInterval(t);resolve(window.THREE_LIB);
        }, { once: true });
      });
      const { THREE, EffectComposer, RenderPass, UnrealBloomPass } = await waitForLib();
      if (!alive || !mount) return;

      // ── Scene setup ──
      const W = mount.clientWidth;
      const H = mount.clientHeight;
      const scene = new THREE.Scene();

      const camera = new THREE.PerspectiveCamera(42, W / H, 0.1, 1000);
      camera.position.set(0, 0, 120);

      const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" });
      renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
      renderer.setSize(W, H);
      renderer.setClearColor(0x000000, 0);
      mount.appendChild(renderer.domElement);

      // ── Tech palette ──
      const COLOR_OCEAN = new THREE.Color(0x081a2a); // deep blue solid core
      const COLOR_WIRE = new THREE.Color(0x1c4d6e); // grid lines
      const COLOR_DOT = new THREE.Color(0x6ae3ff); // continent dots
      const COLOR_DOT_BRIGHT = new THREE.Color(0xc4f0ff); // sparkle dots
      const COLOR_ATMO = new THREE.Color(0x2aa0ff); // rim glow
      const COLOR_PACKET = new THREE.Color(0xffffff); // packet head
      const COLOR_TRAIL = new THREE.Color(0x4dd5ff); // trail line color
      const COLOR_CITY = new THREE.Color(0xffd166); // city node

      // Read accent from CSS so cursor bridge highlights match brand color
      const css = getComputedStyle(document.documentElement);
      const accentRaw = css.getPropertyValue("--accent").trim() || "#FF4B1F";
      const accentColor = new THREE.Color(accentRaw);

      const R = 36;
      const globeGroup = new THREE.Group();
      scene.add(globeGroup);

      // ── Star field ──
      const starGeom = new THREE.BufferGeometry();
      const STAR_COUNT = 1500;
      const starPos = new Float32Array(STAR_COUNT * 3);
      const starSize = new Float32Array(STAR_COUNT);
      for (let i = 0; i < STAR_COUNT; i++) {
        const u = Math.random(),v = Math.random();
        const theta = 2 * Math.PI * u;
        const phi = Math.acos(2 * v - 1);
        const r = 220 + Math.random() * 80;
        starPos[i * 3 + 0] = r * Math.sin(phi) * Math.cos(theta);
        starPos[i * 3 + 1] = r * Math.cos(phi);
        starPos[i * 3 + 2] = r * Math.sin(phi) * Math.sin(theta);
        starSize[i] = Math.random() < 0.08 ? 1.6 + Math.random() * 1.2 : 0.4 + Math.random() * 0.6;
      }
      starGeom.setAttribute("position", new THREE.BufferAttribute(starPos, 3));
      starGeom.setAttribute("aSize", new THREE.BufferAttribute(starSize, 1));
      const starMat = new THREE.ShaderMaterial({
        transparent: true, depthWrite: false,
        uniforms: { uPixelRatio: { value: renderer.getPixelRatio() } },
        vertexShader: `
          attribute float aSize;
          uniform float uPixelRatio;
          varying float vSize;
          void main() {
            vSize = aSize;
            vec4 mv = modelViewMatrix * vec4(position, 1.0);
            gl_PointSize = aSize * uPixelRatio * 1.4;
            gl_Position = projectionMatrix * mv;
          }
        `,
        fragmentShader: `
          varying float vSize;
          void main() {
            vec2 c = gl_PointCoord - 0.5;
            float d = length(c);
            if (d > 0.5) discard;
            float a = smoothstep(0.5, 0.0, d) * (vSize > 1.4 ? 0.85 : 0.45);
            gl_FragColor = vec4(0.6, 0.78, 0.95, a);
          }
        `
      });
      const stars = new THREE.Points(starGeom, starMat);
      scene.add(stars);

      // ── Ocean core ──
      // Solid dark sphere — NOT transparent, so transparent-sort doesn't reorder
      // it relative to the dots. depthWrite:true (default) means it occludes
      // dots on the back hemisphere correctly.
      const oceanMat = new THREE.MeshBasicMaterial({
        color: COLOR_OCEAN
      });
      const oceanMesh = new THREE.Mesh(new THREE.SphereGeometry(R * 0.99, 64, 48), oceanMat);
      globeGroup.add(oceanMesh);

      // ── Wireframe lat/lon grid ──
      const wireGroup = new THREE.Group();
      globeGroup.add(wireGroup);
      const wireMat = new THREE.LineBasicMaterial({
        color: COLOR_WIRE,
        transparent: true,
        opacity: 0.32
      });
      // Latitudes (rings)
      const LAT_COUNT = 12; // every 15°
      for (let i = 1; i < LAT_COUNT; i++) {
        const lat = -90 + i * 180 / LAT_COUNT;
        const phi = (90 - lat) * Math.PI / 180;
        const r = R * Math.sin(phi);
        const y = R * Math.cos(phi);
        const segs = 96;
        const pts = [];
        for (let s = 0; s <= segs; s++) {
          const a = s / segs * Math.PI * 2;
          pts.push(new THREE.Vector3(Math.cos(a) * r, y, Math.sin(a) * r));
        }
        const g = new THREE.BufferGeometry().setFromPoints(pts);
        wireGroup.add(new THREE.Line(g, wireMat));
      }
      // Longitudes (meridians)
      const LON_COUNT = 18; // every 20°
      for (let i = 0; i < LON_COUNT; i++) {
        const lon = i / LON_COUNT * 360;
        const theta = (lon + 180) * Math.PI / 180;
        const segs = 64;
        const pts = [];
        for (let s = 0; s <= segs; s++) {
          const phi = s / segs * Math.PI;
          const x = -R * Math.sin(phi) * Math.cos(theta);
          const y = R * Math.cos(phi);
          const z = R * Math.sin(phi) * Math.sin(theta);
          pts.push(new THREE.Vector3(x, y, z));
        }
        const g = new THREE.BufferGeometry().setFromPoints(pts);
        wireGroup.add(new THREE.Line(g, wireMat));
      }

      // ── Continent dots (sampled from a blue-marble land mask) ──
      // We allocate a Points object with a max dot count, hide them off-screen,
      // and fill them once the image has decoded + we've sampled the canvas.
      const MAX_DOTS = 9000;
      const dotPos = new Float32Array(MAX_DOTS * 3);
      const dotPhase = new Float32Array(MAX_DOTS); // for twinkle
      // Park all off-screen
      for (let i = 0; i < MAX_DOTS; i++) {
        dotPos[i * 3 + 0] = 9999;dotPos[i * 3 + 1] = 9999;dotPos[i * 3 + 2] = 9999;
        dotPhase[i] = Math.random();
      }
      const dotGeom = new THREE.BufferGeometry();
      dotGeom.setAttribute("position", new THREE.BufferAttribute(dotPos, 3));
      dotGeom.setAttribute("aPhase", new THREE.BufferAttribute(dotPhase, 1));
      const dotMat = new THREE.ShaderMaterial({
        transparent: true,
        depthWrite: false,
        depthTest: true,
        blending: THREE.AdditiveBlending,
        uniforms: {
          uPixelRatio: { value: renderer.getPixelRatio() },
          uTime: { value: 0 },
          uColor: { value: COLOR_DOT },
          uColorBright: { value: COLOR_DOT_BRIGHT }
        },
        vertexShader: `
          attribute float aPhase;
          uniform float uPixelRatio;
          uniform float uTime;
          varying float vTwinkle;
          void main() {
            float tw = 0.5 + 0.5 * sin(uTime * 1.4 + aPhase * 6.2831);
            vTwinkle = tw;
            vec4 mv = modelViewMatrix * vec4(position, 1.0);
            float size = 4.5 + tw * 1.5;
            gl_PointSize = size * uPixelRatio;
            gl_Position = projectionMatrix * mv;
          }
        `,
        fragmentShader: `
          uniform vec3 uColor;
          uniform vec3 uColorBright;
          varying float vTwinkle;
          void main() {
            vec2 c = gl_PointCoord - 0.5;
            float d = length(c);
            if (d > 0.5) discard;
            // Strong core → soft halo (additive blends OK with this profile)
            float core = smoothstep(0.5, 0.0, d);
            float halo = pow(core, 0.4) * 0.5;
            vec3 col = mix(uColor, uColorBright, vTwinkle * 0.7);
            // For additive blending: final = dst + col * (alpha as multiplier).
            // Multiply color by intensity rather than rely on alpha clamp.
            float intensity = (core * 0.85 + halo) * 0.6;
            gl_FragColor = vec4(col * intensity, 1.0);
          }
        `
      });
      const dotPoints = new THREE.Points(dotGeom, dotMat);
      dotPoints.frustumCulled = false;
      globeGroup.add(dotPoints);
      window.__globeDotPoints = dotPoints; // debug
      // Render dots BEFORE the wireframe so wireframe doesn't dim them
      dotPoints.renderOrder = 1;
      oceanMesh.renderOrder = 0;

      // Sample land mask. Use blue-marble (continents = bright land), threshold
      // R+G+B above some limit → place a dot. Reject ocean pixels.
      const sampleLandMask = (img) => {
        try {
          const SAMPLE_W = 720;
          const SAMPLE_H = 360;
          const cv = document.createElement("canvas");
          cv.width = SAMPLE_W;cv.height = SAMPLE_H;
          const ctx = cv.getContext("2d");
          ctx.drawImage(img, 0, 0, SAMPLE_W, SAMPLE_H);
          const data = ctx.getImageData(0, 0, SAMPLE_W, SAMPLE_H).data;

          // Walk a near-uniform grid of lat/lon and only keep land pixels.
          // Use latitude-banded longitudinal step so dots stay roughly equidistant.
          let written = 0;
          const LAT_STEPS = 130; // resolution in latitude
          for (let li = 0; li < LAT_STEPS && written < MAX_DOTS; li++) {
            const lat = -85 + li / (LAT_STEPS - 1) * 170; // -85..+85
            const phiRad = (90 - lat) * Math.PI / 180;
            const ringCircumference = Math.cos(lat * Math.PI / 180);
            // ~density-equalised columns per ring
            const colsThisRing = Math.max(8, Math.round(LAT_STEPS * 2 * Math.abs(ringCircumference)));
            for (let k = 0; k < colsThisRing && written < MAX_DOTS; k++) {
              // Slight per-ring jitter to break visible grid pattern
              const lon = -180 + (k + (li % 2 ? 0.5 : 0)) / colsThisRing * 360;
              // Sample pixel
              const u = (lon + 180) / 360; // 0..1
              const v = (90 - lat) / 180; // 0..1
              const px = Math.min(SAMPLE_W - 1, Math.floor(u * SAMPLE_W));
              const py = Math.min(SAMPLE_H - 1, Math.floor(v * SAMPLE_H));
              const o = (py * SAMPLE_W + px) * 4;
              const r = data[o],g = data[o + 1],b = data[o + 2];
              // Land discrimination: blue-marble oceans are blue-dominant
              // (B > R + 10) while land pixels are generally not. Also reject
              // very dark pixels (deep oceans) just to be safe.
              const isOcean = b > r + 8 && b > 50;
              const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
              if (isOcean) continue;
              if (luminance < 24) continue; // black background / shadows
              // Convert lat/lon to xyz on sphere
              const theta = (lon + 180) * Math.PI / 180;
              // Dots far enough outside the ocean shell to never z-fight
              const radius = R * 1.012;
              const x = -radius * Math.sin(phiRad) * Math.cos(theta);
              const y = radius * Math.cos(phiRad);
              const z = radius * Math.sin(phiRad) * Math.sin(theta);
              dotPos[written * 3 + 0] = x;
              dotPos[written * 3 + 1] = y;
              dotPos[written * 3 + 2] = z;
              dotPhase[written] = Math.random();
              written++;
            }
          }
          dotGeom.attributes.position.needsUpdate = true;
          dotGeom.attributes.aPhase.needsUpdate = true;
          // Limit drawing to actually-filled dots
          dotGeom.setDrawRange(0, written);
          // CRITICAL: explicitly set a bounding sphere covering the globe.
          // Without this, the auto-computed bounds reflect the parked (9999,9999,9999)
          // positions and the Points get culled. We avoid computeBoundingSphere()
          // because it loops all 9000 verts (including parked) and is slow.
          dotGeom.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), R * 1.05);
          dotGeom.boundingBox = new THREE.Box3(
            new THREE.Vector3(-R * 1.05, -R * 1.05, -R * 1.05),
            new THREE.Vector3(R * 1.05, R * 1.05, R * 1.05)
          );
          console.log("[Globe] land mask sampled — dots:", written);
        } catch (err) {
          console.warn("[Globe] land mask sampling failed:", err);
        }
      };

      // Image fetch chain (CDN fallbacks)
      const earthCandidates = [
      "https://unpkg.com/three-globe@2.31.1/example/img/earth-blue-marble.jpg",
      "https://raw.githubusercontent.com/vasturiano/three-globe/master/example/img/earth-blue-marble.jpg",
      "https://cdn.jsdelivr.net/gh/vasturiano/three-globe@master/example/img/earth-blue-marble.jpg",
      "https://cdn.jsdelivr.net/npm/three-globe@2.31.1/example/img/earth-blue-marble.jpg"];

      let earthIdx = 0;
      const earthImg = new Image();
      earthImg.crossOrigin = "anonymous";
      earthImg.onload = () => {
        sampleLandMask(earthImg);
      };
      earthImg.onerror = () => {
        console.warn("[Globe] earth land-mask URL failed:", earthImg.src);
        if (earthIdx < earthCandidates.length) {
          earthImg.src = earthCandidates[earthIdx++];
        }
      };
      earthImg.src = earthCandidates[earthIdx++];

      // ── Atmospheric rim — soft cyan halo ──
      const atmoMat = new THREE.ShaderMaterial({
        transparent: true,
        side: THREE.BackSide,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
        uniforms: {
          uColor: { value: COLOR_ATMO },
          uIntensity: { value: 0.35 }
        },
        vertexShader: `
          varying vec3 vNormal;
          varying vec3 vEyeDir;
          void main() {
            vNormal = normalize(normalMatrix * normal);
            vec4 mv = modelViewMatrix * vec4(position, 1.0);
            vEyeDir = normalize(-mv.xyz);
            gl_Position = projectionMatrix * mv;
          }
        `,
        fragmentShader: `
          uniform vec3 uColor;
          uniform float uIntensity;
          varying vec3 vNormal;
          varying vec3 vEyeDir;
          void main() {
            float fres = pow(1.0 - max(dot(vNormal, vEyeDir), 0.0), 4.5);
            gl_FragColor = vec4(uColor, fres * uIntensity);
          }
        `
      });
      const atmo = new THREE.Mesh(new THREE.SphereGeometry(R * 1.04, 64, 40), atmoMat);
      globeGroup.add(atmo);

      // ── City nodes ──
      const llToVec = (lat, lon, radius = R + 0.05) => {
        const phi = (90 - lat) * (Math.PI / 180);
        const theta = (lon + 180) * (Math.PI / 180);
        return new THREE.Vector3(
          -radius * Math.sin(phi) * Math.cos(theta),
          radius * Math.cos(phi),
          radius * Math.sin(phi) * Math.sin(theta)
        );
      };
      const CITIES = [
      { name: "Berlin", lat: 52.52, lon: 13.40 },
      { name: "London", lat: 51.51, lon: -0.13 },
      { name: "New York", lat: 40.71, lon: -74.01 },
      { name: "Sao Paulo", lat: -23.55, lon: -46.63 },
      { name: "Tokyo", lat: 35.68, lon: 139.69 },
      { name: "Shanghai", lat: 31.23, lon: 121.47 },
      { name: "Singapore", lat: 1.35, lon: 103.82 },
      { name: "Sydney", lat: -33.87, lon: 151.21 },
      { name: "Mumbai", lat: 19.08, lon: 72.88 },
      { name: "Cairo", lat: 30.04, lon: 31.24 },
      { name: "Dubai", lat: 25.20, lon: 55.27 },
      { name: "Moscow", lat: 55.76, lon: 37.62 },
      { name: "Lagos", lat: 6.52, lon: 3.38 },
      { name: "Mexico City", lat: 19.43, lon: -99.13 }];

      const cityNodes = CITIES.map((c) => ({
        ...c,
        pos: llToVec(c.lat, c.lon),
        pulse: 0
      }));
      const cityGeom = new THREE.SphereGeometry(0.4, 12, 12);
      const cityMat = new THREE.MeshBasicMaterial({ color: COLOR_CITY });
      const cityMesh = new THREE.InstancedMesh(cityGeom, cityMat, cityNodes.length);
      cityNodes.forEach((c, i) => {
        cityMesh.setMatrixAt(i, new THREE.Matrix4().makeTranslation(c.pos.x, c.pos.y, c.pos.z));
      });
      globeGroup.add(cityMesh);

      // City pulse rings
      const ringGeom = new THREE.RingGeometry(0.5, 0.55, 24);
      const cityRings = cityNodes.map((c) => {
        const m = new THREE.Mesh(
          ringGeom,
          new THREE.MeshBasicMaterial({ color: COLOR_CITY, transparent: true, opacity: 0, side: THREE.DoubleSide })
        );
        m.position.copy(c.pos);
        m.lookAt(0, 0, 0);
        m.scale.setScalar(0.001);
        globeGroup.add(m);
        return m;
      });

      // ── Packet system ──
      const slerp = (a, b, t) => {
        const va = a.clone().normalize();
        const vb = b.clone().normalize();
        const dot = THREE.MathUtils.clamp(va.dot(vb), -1, 1);
        const omega = Math.acos(dot);
        if (omega < 1e-4) return va.clone();
        const sinO = Math.sin(omega);
        return va.multiplyScalar(Math.sin((1 - t) * omega) / sinO).
        add(vb.multiplyScalar(Math.sin(t * omega) / sinO));
      };
      const arcPoint = (from, to, t, lift) => {
        const dir = slerp(from, to, t).normalize();
        const arc = Math.sin(t * Math.PI);
        return dir.multiplyScalar(R + lift * arc);
      };

      const MAX_PACKETS = 90;
      // Packet head as a single Points object (one bright dot per packet)
      const packetGeom = new THREE.BufferGeometry();
      const packetPos = new Float32Array(MAX_PACKETS * 3);
      packetGeom.setAttribute("position", new THREE.BufferAttribute(packetPos, 3));
      const packetMat = new THREE.ShaderMaterial({
        transparent: true,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
        uniforms: { uPixelRatio: { value: renderer.getPixelRatio() } },
        vertexShader: `
          uniform float uPixelRatio;
          void main() {
            vec4 mv = modelViewMatrix * vec4(position, 1.0);
            gl_PointSize = 5.0 * uPixelRatio;
            gl_Position = projectionMatrix * mv;
          }
        `,
        fragmentShader: `
          void main() {
            vec2 c = gl_PointCoord - 0.5;
            float d = length(c);
            if (d > 0.5) discard;
            float core = smoothstep(0.5, 0.0, d);
            float halo = pow(core, 0.5);
            vec3 col = vec3(0.95, 0.98, 1.0);
            float a = halo * 0.95;
            gl_FragColor = vec4(col, a);
          }
        `
      });
      const packetPoints = new THREE.Points(packetGeom, packetMat);
      // Park
      for (let i = 0; i < MAX_PACKETS; i++) {
        packetPos[i * 3 + 0] = 9999;packetPos[i * 3 + 1] = 9999;packetPos[i * 3 + 2] = 9999;
      }
      globeGroup.add(packetPoints);

      // Each packet
      const packets = new Array(MAX_PACKETS).fill(null).map(() => ({ alive: false }));

      // ── Persistent trail lines ──
      // Each active packet owns one Line whose vertex array grows as the packet
      // travels. When the packet finishes, the trail stays as a curve along the
      // arc; it's removed when ALL of its vertices have rotated to the back
      // of the globe (out of camera sight).
      const trailGroup = new THREE.Group();
      globeGroup.add(trailGroup);
      const TRAIL_MAX_VERTS = 256; // per trail (sub-sampled along arc)
      const trails = []; // {line, geom, positions, count, full, packetIdx}

      const makeTrail = () => {
        const positions = new Float32Array(TRAIL_MAX_VERTS * 3);
        const geom = new THREE.BufferGeometry();
        geom.setAttribute("position", new THREE.BufferAttribute(positions, 3));
        geom.setDrawRange(0, 0);
        // Pre-set a bounding sphere covering the whole globe so the line is
        // never frustum-culled while its vertices are still being filled in.
        geom.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), R * 1.4);
        const mat = new THREE.LineBasicMaterial({
          color: COLOR_TRAIL,
          transparent: true,
          opacity: 0.9
        });
        const line = new THREE.Line(geom, mat);
        line.frustumCulled = false;
        trailGroup.add(line);
        return { line, geom, positions, count: 0, full: false, mat, age: 0 };
      };

      const spawnPacket = (fromIdx, toIdx) => {
        const slot = packets.findIndex((p) => !p.alive);
        if (slot < 0) return;
        const from = cityNodes[fromIdx];
        const to = cityNodes[toIdx];
        const dist = from.pos.distanceTo(to.pos);
        const trail = makeTrail();
        packets[slot] = {
          alive: true,
          from: from.pos.clone(),
          to: to.pos.clone(),
          fromIdx, toIdx,
          t: 0,
          speed: 0.0028 + Math.random() * 0.0024,
          baseSpeed: 0,
          boost: 0,
          arcHeight: Math.min(R * 0.55, dist * 0.35 + 4),
          trail
        };
        packets[slot].baseSpeed = packets[slot].speed;
      };

      // Seed
      for (let i = 0; i < 18; i++) {
        const a = Math.floor(Math.random() * cityNodes.length);
        let b = Math.floor(Math.random() * cityNodes.length);
        if (b === a) b = (b + 1) % cityNodes.length;
        spawnPacket(a, b);
      }

      // ── Postprocessing ──
      const composer = new EffectComposer(renderer);
      composer.addPass(new RenderPass(scene, camera));
      const bloom = new UnrealBloomPass(new THREE.Vector2(W, H), 0.18, 0.5, 0.85);
      composer.addPass(bloom);

      // ── Mouse / drag ──
      const mouse = { x: 0, y: 0, ndcX: 0, ndcY: 0, inside: false };
      const raycaster = new THREE.Raycaster();
      const tmp = new THREE.Vector3();
      const onMove = (e) => {
        const rect = mount.getBoundingClientRect();
        mouse.x = e.clientX - rect.left;
        mouse.y = e.clientY - rect.top;
        mouse.ndcX = mouse.x / rect.width * 2 - 1;
        mouse.ndcY = -(mouse.y / rect.height) * 2 + 1;
        mouse.inside = mouse.x >= 0 && mouse.y >= 0 && mouse.x <= rect.width && mouse.y <= rect.height;
      };
      const onLeave = () => {mouse.inside = false;};
      window.addEventListener("mousemove", onMove);
      mount.addEventListener("mouseleave", onLeave);

      let dragging = false;
      let lastDrag = { x: 0, y: 0 };
      let rotVel = { x: 0, y: 0 };
      const onDown = (e) => {
        const rect = mount.getBoundingClientRect();
        if (e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom) return;
        dragging = true;
        lastDrag = { x: e.clientX, y: e.clientY };
      };
      const onUp = () => {dragging = false;};
      const onDrag = (e) => {
        if (!dragging) return;
        const dx = e.clientX - lastDrag.x;
        const dy = e.clientY - lastDrag.y;
        rotVel.y = dx * 0.005;
        rotVel.x = dy * 0.005;
        lastDrag = { x: e.clientX, y: e.clientY };
      };
      window.addEventListener("mousedown", onDown);
      window.addEventListener("mouseup", onUp);
      window.addEventListener("mousemove", onDrag);

      // ── Cursor bridge ──
      const livePacketsScreen = [];
      const findNearestCityOnScreen = (sx, sy) => {
        const rect = mount.getBoundingClientRect();
        const localX = sx - rect.left;
        const localY = sy - rect.top;
        let best = -1,bestD = Infinity;
        const camDir = camera.position.clone().normalize();
        for (let i = 0; i < cityNodes.length; i++) {
          const worldPos = cityNodes[i].pos.clone().applyMatrix4(globeGroup.matrixWorld);
          if (worldPos.clone().normalize().dot(camDir) < -0.05) continue; // back side
          const v = worldPos.clone().project(camera);
          const px = (v.x * 0.5 + 0.5) * rect.width;
          const py = (-v.y * 0.5 + 0.5) * rect.height;
          const d = Math.hypot(px - localX, py - localY);
          if (d < bestD) {bestD = d;best = i;}
        }
        return { idx: best, dist: bestD };
      };
      window.__globeBridge = {
        captureDigit: (clientX, clientY) => {
          const rect = mount.getBoundingClientRect();
          if (clientX < rect.left || clientX > rect.right || clientY < rect.top || clientY > rect.bottom) {
            return false;
          }
          const PACKET_RADIUS = 90;
          let bestPkt = -1,bestPktD = PACKET_RADIUS;
          for (const lp of livePacketsScreen) {
            const dx = lp.sx - clientX,dy = lp.sy - clientY;
            const d = Math.hypot(dx, dy);
            if (d < bestPktD) {bestPktD = d;bestPkt = lp.slot;}
          }
          if (bestPkt >= 0) {
            const lp = livePacketsScreen.find((x) => x.slot === bestPkt);
            if (lp) {
              const p = packets[bestPkt];
              if (p && p.alive) p.boost = Math.min(2.5, p.boost + 0.7);
              return { captured: true, screenX: lp.sx, screenY: lp.sy };
            }
          }
          const { idx } = findNearestCityOnScreen(clientX, clientY);
          if (idx < 0) return false;
          const v = cityNodes[idx].pos.clone().applyMatrix4(globeGroup.matrixWorld);
          v.project(camera);
          const screenX = rect.left + (v.x * 0.5 + 0.5) * rect.width;
          const screenY = rect.top + (-v.y * 0.5 + 0.5) * rect.height;
          let other = Math.floor(Math.random() * cityNodes.length);
          if (other === idx) other = (other + 1) % cityNodes.length;
          spawnPacket(idx, other);
          spawnPacket(other, idx);
          cityRings[idx].material.opacity = 1.0;
          cityRings[idx].scale.setScalar(0.001);
          cityNodes[idx].pulse = 1.0;
          return { captured: true, screenX, screenY };
        }
      };

      // ── Resize ──
      const onResize = () => {
        const W2 = mount.clientWidth;
        const H2 = mount.clientHeight;
        camera.aspect = W2 / H2;
        camera.updateProjectionMatrix();
        renderer.setSize(W2, H2);
        composer.setSize(W2, H2);
        bloom.setSize(W2, H2);
      };
      window.addEventListener("resize", onResize);

      // ── Animation loop ──
      const clock = new THREE.Clock();
      let raf;
      const tmpV = new THREE.Vector3();

      const animate = () => {
        if (!alive) return;
        raf = requestAnimationFrame(animate);
        const dt = Math.min(clock.getDelta(), 0.05);
        const t = clock.elapsedTime;

        // Auto-rotation (slow & continuous) + drag override
        if (!dragging) {
          globeGroup.rotation.y += dt * 0.05;
        } else {
          globeGroup.rotation.y += rotVel.y;
          globeGroup.rotation.x += rotVel.x;
          rotVel.x *= 0.9;rotVel.y *= 0.9;
        }
        // Damp residual flick velocity into the auto-rotation
        if (!dragging) {
          globeGroup.rotation.y += rotVel.y;
          globeGroup.rotation.x += rotVel.x;
          rotVel.x *= 0.94;rotVel.y *= 0.94;
        }

        // Update dot shader uniforms
        dotMat.uniforms.uTime.value = t;

        // Mouse hit on sphere (for proximity boost)
        let mouseHit = null;
        if (mouse.inside) {
          raycaster.setFromCamera({ x: mouse.ndcX, y: mouse.ndcY }, camera);
          const hit = raycaster.ray.intersectSphere(new THREE.Sphere(new THREE.Vector3(0, 0, 0), R), tmp);
          if (hit) mouseHit = tmp.clone();
        }

        // Steady baseline traffic
        if (Math.random() < 0.32) {
          const a = Math.floor(Math.random() * cityNodes.length);
          let b = Math.floor(Math.random() * cityNodes.length);
          if (b === a) b = (b + 1) % cityNodes.length;
          spawnPacket(a, b);
        }

        // Update packets + their growing trail lines
        let writeI = 0;
        const livePacketsForBridge = [];
        for (let i = 0; i < MAX_PACKETS; i++) {
          const p = packets[i];
          if (!p.alive) continue;
          p.boost *= 0.94;

          // Mouse-proximity boost (in local space)
          if (mouseHit) {
            const localMouse = mouseHit.clone();
            globeGroup.worldToLocal(localMouse);
            const probe = arcPoint(p.from, p.to, p.t, p.arcHeight);
            const d = probe.distanceTo(localMouse);
            const PROX = 12;
            if (d < PROX) {
              const k = 1 - d / PROX;
              p.boost = Math.min(2.5, p.boost + k * 0.18);
            }
          }
          p.speed = p.baseSpeed * (1 + p.boost);
          p.t += p.speed;

          if (p.t >= 1) {
            // Arrived → ping the destination, then kill packet AND its trail.
            // The user wants the trail to disappear when the packet arrives.
            cityNodes[p.toIdx].pulse = Math.min(1, cityNodes[p.toIdx].pulse + 0.6);
            p.alive = false;
            if (p.trail) {
              // Fade out fast, no orphan retention
              orphanTrails.push({ ...p.trail, age: 0, fadeFrom: 0.9, fadeDuration: 0.25 });
            }
            continue;
          }

          // Current head position (local)
          const head = arcPoint(p.from, p.to, p.t, p.arcHeight);
          packetPos[writeI * 3 + 0] = head.x;
          packetPos[writeI * 3 + 1] = head.y;
          packetPos[writeI * 3 + 2] = head.z;
          writeI++;

          // Append head to its trail. To prevent gaps when speed/boost causes
          // big per-frame jumps, sub-sample between the last trail vertex and
          // the new head and insert intermediate points.
          const tr = p.trail;
          if (tr && !tr.full) {
            const writeVert = (x, y, z) => {
              if (tr.count >= TRAIL_MAX_VERTS) {tr.full = true;return;}
              tr.positions[tr.count * 3 + 0] = x;
              tr.positions[tr.count * 3 + 1] = y;
              tr.positions[tr.count * 3 + 2] = z;
              tr.count++;
            };
            if (tr.count === 0) {
              writeVert(head.x, head.y, head.z);
            } else {
              const lx = tr.positions[(tr.count - 1) * 3 + 0];
              const ly = tr.positions[(tr.count - 1) * 3 + 1];
              const lz = tr.positions[(tr.count - 1) * 3 + 2];
              const dx = head.x - lx,dy = head.y - ly,dz = head.z - lz;
              const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
              // Insert a sub-sample for every ~0.6 units of arc travel so the
              // line never visibly breaks even with high boost.
              const STEP = 0.6;
              const subs = Math.min(8, Math.max(1, Math.floor(dist / STEP)));
              for (let s = 1; s <= subs; s++) {
                const k = s / subs;
                writeVert(lx + dx * k, ly + dy * k, lz + dz * k);
                if (tr.full) break;
              }
            }
            tr.geom.setDrawRange(0, tr.count);
            tr.geom.attributes.position.needsUpdate = true;
            // Refresh bounding sphere occasionally so the line doesn't get
            // frustum-culled when its parked verts dominate.
            if ((tr.count & 31) === 0) {
              tr.geom.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), R * 1.4);
            }
          }

          // World position for cursor bridge
          const worldHead = head.clone();
          globeGroup.localToWorld(worldHead);
          livePacketsForBridge.push({ slot: i, world: worldHead });
        }
        // Hide unused packet heads
        for (let i = writeI; i < MAX_PACKETS; i++) {
          packetPos[i * 3 + 0] = 9999;packetPos[i * 3 + 1] = 9999;packetPos[i * 3 + 2] = 9999;
        }
        packetGeom.attributes.position.needsUpdate = true;

        // Project live packets to screen for cursor bridge
        livePacketsScreen.length = 0;
        const rect = mount.getBoundingClientRect();
        for (const lp of livePacketsForBridge) {
          tmpV.copy(lp.world).project(camera);
          if (tmpV.z > 1) continue;
          const sx = rect.left + (tmpV.x * 0.5 + 0.5) * rect.width;
          const sy = rect.top + (-tmpV.y * 0.5 + 0.5) * rect.height;
          livePacketsScreen.push({ slot: lp.slot, sx, sy });
        }

        // ── Update trails ──
        // 1) For LIVE packets: trail stays bright, but fade if any vertex has
        //    rotated to the back of the globe (cull-aware opacity).
        // 2) For ORPHAN trails (packet finished): age them, then remove when
        //    fully rotated out of view OR after max age.
        const camWorldDir = camera.position.clone().normalize();
        const checkVisibility = (tr) => {
          // Sample a few vertices, see how many are front-facing
          if (tr.count === 0) return 0;
          const stride = Math.max(1, Math.floor(tr.count / 5));
          let frontCount = 0,total = 0;
          for (let v = 0; v < tr.count; v += stride) {
            tmpV.set(
              tr.positions[v * 3 + 0],
              tr.positions[v * 3 + 1],
              tr.positions[v * 3 + 2]
            );
            tmpV.applyMatrix4(globeGroup.matrixWorld);
            // Normal at this point ~ direction from origin
            const facing = tmpV.clone().normalize().dot(camWorldDir);
            if (facing > -0.1) frontCount++;
            total++;
          }
          return total > 0 ? frontCount / total : 0;
        };

        // Dim live trails when their tail goes behind globe
        for (let i = 0; i < MAX_PACKETS; i++) {
          const p = packets[i];
          if (!p.alive || !p.trail) continue;
          const visFrac = checkVisibility(p.trail);
          p.trail.mat.opacity = 0.35 + 0.55 * visFrac;
        }

        // Process orphan trails — quick fade-out after packet arrival
        for (let i = orphanTrails.length - 1; i >= 0; i--) {
          const tr = orphanTrails[i];
          tr.age += dt;
          const dur = tr.fadeDuration || 0.25;
          const k = Math.max(0, 1 - tr.age / dur);
          tr.mat.opacity = (tr.fadeFrom || 0.85) * k;
          if (tr.age >= dur) {
            trailGroup.remove(tr.line);
            tr.geom.dispose();
            tr.mat.dispose();
            orphanTrails.splice(i, 1);
          }
        }

        // Update city pulse rings
        for (let i = 0; i < cityNodes.length; i++) {
          const c = cityNodes[i];
          const ring = cityRings[i];
          if (c.pulse > 0.01) {
            const s = (1 - c.pulse) * 6 + 0.3;
            ring.scale.setScalar(s);
            ring.material.opacity = c.pulse * 0.7;
            c.pulse *= 0.94;
          } else {
            ring.material.opacity = 0;
          }
          ring.lookAt(0, 0, 0);
        }

        composer.render();
      };
      const orphanTrails = [];
      animate();

      cleanup = () => {
        cancelAnimationFrame(raf);
        window.removeEventListener("mousemove", onMove);
        window.removeEventListener("mousemove", onDrag);
        window.removeEventListener("mousedown", onDown);
        window.removeEventListener("mouseup", onUp);
        window.removeEventListener("resize", onResize);
        mount.removeEventListener("mouseleave", onLeave);
        if (window.__globeBridge) delete window.__globeBridge;
        if (renderer.domElement.parentNode === mount) mount.removeChild(renderer.domElement);
        renderer.dispose();
        oceanMesh.geometry.dispose();
        oceanMat.dispose();
        atmo.geometry.dispose();
        atmoMat.dispose();
        starGeom.dispose();
        starMat.dispose();
        cityGeom.dispose();
        ringGeom.dispose();
        dotGeom.dispose();
        dotMat.dispose();
        packetGeom.dispose();
        packetMat.dispose();
        wireGroup.children.forEach((l) => l.geometry.dispose());
        wireMat.dispose();
        for (const tr of orphanTrails) {tr.geom.dispose();tr.mat.dispose();}
        for (const p of packets) {if (p.trail) {p.trail.geom.dispose();p.trail.mat.dispose();}}
      };
    })();

    return () => {
      alive = false;
      cleanup();
    };
  }, []);

  return (
    <section id="network" style={{
      background: "var(--bg-inverse)",
      color: "var(--fg-on-dark)",
      padding: "var(--s-10) 0 var(--s-10)",
      position: "relative",
      overflow: "hidden"
    }}>
      <div className="grid" style={{ marginBottom: "var(--s-7)", position: "relative", zIndex: 2 }}>
        <div className="mono" style={{ gridColumn: "1 / span 4", color: "var(--fg-muted-on-dark)" }}>
          (·) — Live Datenfluss
        </div>
        <div style={{ gridColumn: "5 / span 8" }}>
          <h2 style={{
            fontFamily: "var(--font-display)",
            fontStyle: "italic",
            fontSize: "clamp(48px, 6vw, 104px)",
            lineHeight: 0.96,
            letterSpacing: "-0.035em",
            fontWeight: 400,
            maxWidth: "20ch"
          }}>
            Daten kennen<br />keine Grenzen<span style={{ color: "var(--accent)" }}>.</span>
          </h2>
        </div>
      </div>

      <div
        ref={mountRef}
        style={{
          position: "relative",
          width: "100%",
          height: "min(72vh, 720px)",
          minHeight: 480,
          cursor: "none"
        }} />
      

      <div className="grid" style={{ marginTop: "var(--s-9)", marginBottom: "var(--s-4)", position: "relative", zIndex: 2 }}>
        <div style={{
          gridColumn: "1 / -1",
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          textAlign: "center",
          gap: "var(--s-3)",
        }}>
          <div className="mono" style={{ color: "var(--fg-muted-on-dark)" }}>
            (·) — Lass uns starten
          </div>
          <h2 style={{
            fontFamily: "var(--font-display)",
            fontStyle: "italic",
            fontSize: "clamp(40px, 5vw, 80px)",
            lineHeight: 1.05,
            letterSpacing: "-0.035em",
            fontWeight: 400,
            maxWidth: "22ch",
            textWrap: "balance",
            margin: 0,
          }}>
            Genauso haben deine Ideen<br />
            keine Grenzen<span style={{ color: "var(--accent)" }}>.</span>
          </h2>
          <p style={{
            fontFamily: "var(--font-display)",
            fontStyle: "italic",
            fontSize: "clamp(20px, 2vw, 28px)",
            lineHeight: 1.3,
            letterSpacing: "-0.01em",
            color: "var(--fg-muted-on-dark)",
            maxWidth: "32ch",
            margin: 0,
          }}>
            Setzen wir sie gemeinsam um.
          </p>
        </div>
      </div>
    </section>);

};

window.Globe = Globe;