// wci26/globe.jsx — Three.js WebGL globe with morph-to-flat-map
// Dependencies (window globals): THREE, topojson, COUNTRY_GEO, ISO3_TO_NUM

// ───────────────────────────────────────────────────────────────
// Geo helpers
// ───────────────────────────────────────────────────────────────
const RADIUS = 100;
const PLANE_W = 400;
const PLANE_H = 200;
const SHOWCASE_ROT_Y = -0.62;
const SHOWCASE_ROT_X = -0.05;
const ROUTE_POINT_COUNT = 72;
const NEON_ROUTE_COLORS = ['#4ed1e6', '#e637a8', '#f5d020', '#5bf870'];

const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
const shortestAngleDelta = (from, to) => {
  let delta = (to - from) % (Math.PI * 2);
  if (delta > Math.PI) delta -= Math.PI * 2;
  if (delta < -Math.PI) delta += Math.PI * 2;
  return delta;
};

const llToSphere = (lat, lng, r = RADIUS) => {
  const phi = (90 - lat) * Math.PI / 180;
  const theta = (lng + 180) * Math.PI / 180;
  return [
    -r * Math.sin(phi) * Math.cos(theta),
    r * Math.cos(phi),
    r * Math.sin(phi) * Math.sin(theta),
  ];
};

const llToPlane = (lat, lng) => [
  (lng / 180) * (PLANE_W / 2),
  (lat / 90) * (PLANE_H / 2),
  0,
];

const getCountryFocusRotation = (code) => {
  const geo = COUNTRY_GEO?.[code];
  if (!geo) return null;
  const [lat, lng] = geo;
  const theta = (lng + 180) * Math.PI / 180;
  return {
    rotY: Math.PI / 2 - theta,
    rotX: clamp((lat * Math.PI / 180) * 0.72, -0.38, 0.38),
  };
};

// ───────────────────────────────────────────────────────────────
// Shader: morphs vertices between spherical and planar positions
// ───────────────────────────────────────────────────────────────
const LINE_VERT = `
  attribute vec3 spherePos;
  attribute vec3 planePos;
  uniform float uMorph;
  varying float vY;
  void main() {
    vec3 pos = mix(spherePos, planePos, uMorph);
    vY = pos.y;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
  }
`;
const LINE_FRAG = `
  precision highp float;
  uniform vec3 uColor;
  uniform float uIntensity;
  varying float vY;
  void main() {
    gl_FragColor = vec4(uColor * uIntensity, 1.0);
  }
`;

const FILL_VERT = `
  attribute vec3 spherePos;
  attribute vec3 planePos;
  uniform float uMorph;
  varying vec3 vWorldPos;
  varying vec3 vNormalApprox;
  void main() {
    vec3 pos = mix(spherePos, planePos, uMorph);
    vWorldPos = pos;
    // Approx normal: on sphere, normal = normalize(pos); on plane, normal = (0,0,1)
    vec3 sn = normalize(spherePos);
    vNormalApprox = mix(sn, vec3(0.0, 0.0, 1.0), uMorph);
    gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
  }
`;
const FILL_FRAG = `
  precision highp float;
  uniform vec3 uColor;
  uniform float uPulse;        // 0..1
  uniform float uActive;       // 0 / 1 (top-3 hot)
  uniform float uHover;        // 0 / 1
  uniform float uMorph;
  varying vec3 vWorldPos;
  varying vec3 vNormalApprox;
  void main() {
    // Soft dim by view-facing (only on sphere)
    float facing = max(0.25, normalize(vNormalApprox).z * 0.7 + 0.3);
    // Flat map gets stronger fills (countries colored solid-ish), sphere stays subtle
    float flatBoost = uMorph * 0.28;
    float projectedLift = uMorph * (0.08 + 0.06 * smoothstep(-92.0, 92.0, vWorldPos.y));
    float a = 0.12 + flatBoost + projectedLift + uPulse * 0.16 + uActive * 0.24 + uHover * 0.45;
    vec3 c = uColor * (0.65 + uMorph * 0.30 + uPulse * 0.25 + uActive * 0.34 + uHover * 0.10);
    c += vec3(0.04, 0.10, 0.13) * uMorph;
    gl_FragColor = vec4(c, a * facing);
  }
`;

const FLAT_BACKDROP_VERT = `
  varying vec2 vUv;
  void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`;

const FLAT_BACKDROP_FRAG = `
  precision highp float;
  uniform float uMorph;
  uniform float uTime;
  varying vec2 vUv;
  void main() {
    vec2 p = vUv - 0.5;
    float radial = 1.0 - smoothstep(0.18, 0.78, length(p * vec2(1.15, 1.8)));
    float edge = smoothstep(0.52, 0.18, length(p * vec2(1.0, 1.55)));
    float sweep = 1.0 - smoothstep(0.0, 0.09, abs(vUv.y - (0.54 + sin(uTime * 0.35) * 0.045)));
    float northGlow = 1.0 - smoothstep(0.0, 0.34, distance(vUv, vec2(0.56, 0.64)));
    float atlanticGlow = 1.0 - smoothstep(0.0, 0.28, distance(vUv, vec2(0.39, 0.56)));
    float diagonal = smoothstep(0.82, 1.0, sin((vUv.x * 9.0 + vUv.y * 6.5 + uTime * 0.18) * 6.28318) * 0.5 + 0.5);
    float microGridX = 1.0 - smoothstep(0.0, 0.018, abs(fract(vUv.x * 18.0) - 0.5));
    float microGridY = 1.0 - smoothstep(0.0, 0.018, abs(fract(vUv.y * 9.0) - 0.5));
    float plasma = sin(vUv.x * 12.0 + uTime * 0.42) * sin(vUv.y * 8.0 - uTime * 0.31);
    vec3 deep = vec3(0.010, 0.012, 0.040);
    vec3 mid = vec3(0.015, 0.075, 0.160);
    vec3 color = mix(deep, mid, smoothstep(0.0, 1.0, vUv.y));
    color += vec3(0.00, 0.66, 0.92) * radial * 0.48;
    color += vec3(0.92, 0.12, 0.82) * northGlow * 0.20;
    color += vec3(1.00, 0.82, 0.12) * atlanticGlow * 0.16;
    color += vec3(0.00, 0.80, 1.00) * sweep * 0.12;
    color += vec3(0.86, 0.08, 1.00) * diagonal * radial * 0.045;
    color += vec3(0.00, 0.74, 1.00) * (microGridX + microGridY) * 0.012;
    color += vec3(0.10, 0.36, 0.60) * plasma * radial * 0.035;
    float alpha = uMorph * (0.72 + radial * 0.30 + edge * 0.15);
    gl_FragColor = vec4(color, alpha);
  }
`;

// ───────────────────────────────────────────────────────────────
// Build country mesh (fill + outline) from a GeoJSON feature
// ───────────────────────────────────────────────────────────────
const buildCountryMeshes = (feature, color, idx) => {
  const polygons = feature.geometry.type === 'Polygon'
    ? [feature.geometry.coordinates]
    : feature.geometry.coordinates;

  // ── FILL: tesselate each polygon ring with earcut (THREE.ShapeUtils.triangulateShape)
  const fillSpherePos = [];
  const fillPlanePos = [];

  polygons.forEach((rings) => {
    if (!rings.length) return;
    const outer = rings[0];
    if (outer.length < 4) return;

    // Use THREE.ShapeUtils.triangulateShape for triangulation
    const outer2D = outer.map(([lng, lat]) => new THREE.Vector2(lng, lat));
    const holes2D = rings.slice(1).map(ring => ring.map(([lng, lat]) => new THREE.Vector2(lng, lat)));
    const tris = THREE.ShapeUtils.triangulateShape(outer2D, holes2D);

    // Build a flat list of all vertices (outer + holes)
    const allVerts = [...outer2D];
    holes2D.forEach(h => allVerts.push(...h));

    tris.forEach((tri) => {
      tri.forEach((i) => {
        const v = allVerts[i];
        if (!v) return;
        const sp = llToSphere(v.y, v.x);
        const pp = llToPlane(v.y, v.x);
        fillSpherePos.push(...sp);
        fillPlanePos.push(...pp);
      });
    });
  });

  let fillMesh = null;
  if (fillSpherePos.length > 0) {
    const fillGeom = new THREE.BufferGeometry();
    fillGeom.setAttribute('spherePos', new THREE.Float32BufferAttribute(fillSpherePos, 3));
    fillGeom.setAttribute('planePos', new THREE.Float32BufferAttribute(fillPlanePos, 3));
    // dummy position (shader overrides via spherePos)
    fillGeom.setAttribute('position', new THREE.Float32BufferAttribute(fillSpherePos, 3));
    // Force a large bounding sphere so frustum culling doesn't drop us in morph state
    fillGeom.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), 250);

    const fillMat = new THREE.ShaderMaterial({
      vertexShader: FILL_VERT,
      fragmentShader: FILL_FRAG,
      uniforms: {
        uMorph: { value: 0 },
        uColor: { value: new THREE.Color(color) },
        uPulse: { value: 0 },
        uActive: { value: 0 },
        uHover: { value: 0 },
      },
      transparent: true,
      depthWrite: false,
      depthTest: false,
      side: THREE.DoubleSide,
      blending: THREE.NormalBlending,
    });
    fillMesh = new THREE.Mesh(fillGeom, fillMat);
    fillMesh.renderOrder = 2;
    fillMesh.frustumCulled = false;
  }

  // ── OUTLINE: line strips along outer ring of each polygon
  const outlineSpherePos = [];
  const outlinePlanePos = [];

  polygons.forEach((rings) => {
    rings.forEach((ring) => {
      // Subdivide long segments for smoother sphere mapping
      const subdivided = [];
      for (let i = 0; i < ring.length - 1; i++) {
        const [aLng, aLat] = ring[i];
        const [bLng, bLat] = ring[i + 1];
        if (Math.abs(bLng - aLng) > 180) continue;
        const steps = Math.max(1, Math.ceil(Math.hypot(bLng - aLng, bLat - aLat) / 2.5));
        for (let s = 0; s < steps; s++) {
          const t = s / steps;
          const lng = aLng + (bLng - aLng) * t;
          const lat = aLat + (bLat - aLat) * t;
          subdivided.push([lng, lat]);
        }
      }
      subdivided.push(ring[ring.length - 1]);

      for (let i = 0; i < subdivided.length - 1; i++) {
        const [aLng, aLat] = subdivided[i];
        const [bLng, bLat] = subdivided[i + 1];
        const sa = llToSphere(aLat, aLng, RADIUS * 1.001);
        const sb = llToSphere(bLat, bLng, RADIUS * 1.001);
        const pa = llToPlane(aLat, aLng);
        const pb = llToPlane(bLat, bLng);
        outlineSpherePos.push(...sa, ...sb);
        outlinePlanePos.push(...pa, ...pb);
      }
    });
  });

  let outlineMesh = null;
  if (outlineSpherePos.length > 0) {
    const lineGeom = new THREE.BufferGeometry();
    lineGeom.setAttribute('spherePos', new THREE.Float32BufferAttribute(outlineSpherePos, 3));
    lineGeom.setAttribute('planePos', new THREE.Float32BufferAttribute(outlinePlanePos, 3));
    lineGeom.setAttribute('position', new THREE.Float32BufferAttribute(outlineSpherePos, 3));
    lineGeom.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), 250);

    const lineMat = new THREE.ShaderMaterial({
      vertexShader: LINE_VERT,
      fragmentShader: LINE_FRAG,
      uniforms: {
        uMorph: { value: 0 },
        uColor: { value: new THREE.Color(color) },
        uIntensity: { value: 1.2 },
      },
      transparent: true,
      blending: THREE.AdditiveBlending,
      depthWrite: false,
    });
    outlineMesh = new THREE.LineSegments(lineGeom, lineMat);
    outlineMesh.renderOrder = 3;
    outlineMesh.frustumCulled = false;
  }

  return { fillMesh, outlineMesh };
};

// ───────────────────────────────────────────────────────────────
// Pin / beam meshes (per country marker)
// ───────────────────────────────────────────────────────────────
const buildPinSprite = (color) => {
  // Glowing dot sprite
  const canvas = document.createElement('canvas');
  canvas.width = 128; canvas.height = 128;
  const ctx = canvas.getContext('2d');
  const grad = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);
  grad.addColorStop(0, '#ffffff');
  grad.addColorStop(0.18, color);
  grad.addColorStop(0.48, color + '55');
  grad.addColorStop(1, color + '00');
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, 128, 128);
  const tex = new THREE.CanvasTexture(canvas);
  tex.minFilter = THREE.LinearFilter;
  return tex;
};

const buildSoftGlowSprite = (color) => {
  const canvas = document.createElement('canvas');
  canvas.width = 192; canvas.height = 192;
  const ctx = canvas.getContext('2d');
  const grad = ctx.createRadialGradient(96, 96, 0, 96, 96, 96);
  grad.addColorStop(0, '#ffffff55');
  grad.addColorStop(0.18, color + '88');
  grad.addColorStop(0.52, color + '28');
  grad.addColorStop(1, color + '00');
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, 192, 192);
  const tex = new THREE.CanvasTexture(canvas);
  tex.minFilter = THREE.LinearFilter;
  return tex;
};

const buildScanBarTexture = (color, vertical = false) => {
  const canvas = document.createElement('canvas');
  canvas.width = vertical ? 48 : 512;
  canvas.height = vertical ? 512 : 48;
  const ctx = canvas.getContext('2d');
  const grad = vertical
    ? ctx.createLinearGradient(0, 0, canvas.width, 0)
    : ctx.createLinearGradient(0, 0, 0, canvas.height);
  grad.addColorStop(0, color + '00');
  grad.addColorStop(0.42, color + '28');
  grad.addColorStop(0.5, '#ffffffcc');
  grad.addColorStop(0.58, color + '28');
  grad.addColorStop(1, color + '00');
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  const tex = new THREE.CanvasTexture(canvas);
  tex.minFilter = THREE.LinearFilter;
  return tex;
};

const getTopCountryRank = (topRanked = [], code) => {
  const index = (topRanked || []).findIndex(country => country?.code === code);
  return index >= 0 && index < 3 ? index + 1 : 0;
};

const getCountryActivityStrength = (country) => {
  if (!country) return 0;
  const recentBuyWeth = Number(country.recentBuyVolumeWETH || 0);
  const totalVolumeWeth = Number(country.totalVolumeWETH || country.volume24hWETH || 0);
  const legacyVolume = Number(country.volume24h || 0);
  const momentum = Number(country.momentum || 0);
  const recentBuys = Number(country.recentBuyCount || country.buys1m || 0);
  const liveWethStrength = clamp(recentBuyWeth * 16 + totalVolumeWeth * 0.025, 0, 1.25);
  const legacyStrength = legacyVolume ? clamp(legacyVolume / 3500000, 0, 1.15) : 0;
  const buyCountStrength = clamp(recentBuys / 12, 0, 0.9);
  return clamp(
    0.16 + momentum * 0.36 + Math.max(liveWethStrength, legacyStrength) * 0.42 + buyCountStrength * 0.22,
    0.16,
    1.6,
  );
};

const buildCountryCandle = (color) => {
  const candleGeom = new THREE.CylinderGeometry(0.66, 0.20, 1, 20, 1, true);
  candleGeom.translate(0, 0.5, 0);
  const candleMat = new THREE.ShaderMaterial({
    vertexShader: `
      varying float vH;
      varying vec2 vUv;
      void main() {
        vH = clamp(position.y, 0.0, 1.0);
        vUv = uv;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    `,
    fragmentShader: `
      precision highp float;
      uniform vec3 uColor;
      uniform vec3 uGold;
      uniform vec3 uCyan;
      uniform float uOpacity;
      uniform float uTime;
      uniform float uRankBoost;
      uniform float uPulseStrength;
      varying float vH;
      varying vec2 vUv;
      void main() {
        float edge = smoothstep(0.0, 0.12, vH) * (1.0 - smoothstep(0.96, 1.0, vH));
        float scan = 0.5 + 0.5 * sin(uTime * 4.4 + vH * 18.0);
        float core = smoothstep(0.42, 0.0, abs(vUv.x - 0.5));
        vec3 rankColor = mix(uColor, uGold, 0.24 + uRankBoost * 0.32);
        vec3 pulseColor = mix(rankColor, uCyan, scan * (0.18 + uPulseStrength * 0.18));
        float brightness = 1.1 + uRankBoost * 0.6 + scan * (0.38 + uPulseStrength * 0.52) + core * 0.32;
        float alpha = edge * uOpacity * (0.48 + core * 0.52 + scan * 0.16);
        gl_FragColor = vec4(pulseColor * brightness, alpha);
      }
    `,
    uniforms: {
      uColor: { value: new THREE.Color(color) },
      uGold: { value: new THREE.Color('#f5d020') },
      uCyan: { value: new THREE.Color('#4ed1e6') },
      uOpacity: { value: 0 },
      uTime: { value: 0 },
      uRankBoost: { value: 0 },
      uPulseStrength: { value: 0 },
    },
    transparent: true,
    depthWrite: false,
    blending: THREE.AdditiveBlending,
    side: THREE.DoubleSide,
  });
  const candle = new THREE.Mesh(candleGeom, candleMat);
  candle.renderOrder = 8;
  candle.frustumCulled = false;
  candle.userData.currentHeight = 0.001;
  return candle;
};

const buildActivityPulseRing = (color) => {
  const group = new THREE.Group();
  for (let i = 0; i < 3; i++) {
    const ringGeom = new THREE.RingGeometry(4.8 + i * 1.15, 5.18 + i * 1.15, 96);
    const ringMat = new THREE.MeshBasicMaterial({
      color: new THREE.Color(i === 0 ? '#f5d020' : color),
      transparent: true,
      opacity: 0,
      depthWrite: false,
      depthTest: true,
      side: THREE.DoubleSide,
      blending: THREE.AdditiveBlending,
    });
    const ring = new THREE.Mesh(ringGeom, ringMat);
    ring.renderOrder = 7;
    ring.userData = {
      phase: i * 0.28,
      speed: 0.46 + i * 0.07,
      baseOpacity: 0.38 - i * 0.07,
      spin: (i % 2 ? -1 : 1) * (0.22 + i * 0.05),
      kind: 'ring',
    };
    group.add(ring);
  }

  const burst = new THREE.Sprite(new THREE.SpriteMaterial({
    map: buildSoftGlowSprite(color),
    color: 0xffffff,
    transparent: true,
    opacity: 0,
    depthWrite: false,
    blending: THREE.AdditiveBlending,
  }));
  burst.renderOrder = 9;
  burst.userData = { kind: 'burst', baseOpacity: 0.32 };
  group.add(burst);
  group.userData.currentStrength = 0;
  return group;
};

const drawRankRoundRect = (ctx, x, y, w, h, r) => {
  const radius = Math.min(r, w / 2, h / 2);
  ctx.beginPath();
  ctx.moveTo(x + radius, y);
  ctx.lineTo(x + w - radius, y);
  ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
  ctx.lineTo(x + w, y + h - radius);
  ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
  ctx.lineTo(x + radius, y + h);
  ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
  ctx.lineTo(x, y + radius);
  ctx.quadraticCurveTo(x, y, x + radius, y);
  ctx.closePath();
};

const buildRankLabelTexture = (rank, color) => {
  const canvas = document.createElement('canvas');
  canvas.width = 176;
  canvas.height = 72;
  const ctx = canvas.getContext('2d');
  const grad = ctx.createLinearGradient(0, 0, 176, 72);
  grad.addColorStop(0, 'rgba(6, 12, 33, 0.76)');
  grad.addColorStop(0.55, 'rgba(15, 18, 42, 0.72)');
  grad.addColorStop(1, 'rgba(7, 12, 24, 0.66)');
  ctx.shadowColor = rank === 1 ? '#f5d020' : color;
  ctx.shadowBlur = 16;
  drawRankRoundRect(ctx, 8, 10, 160, 52, 18);
  ctx.fillStyle = grad;
  ctx.fill();
  ctx.lineWidth = 3;
  ctx.strokeStyle = rank === 1 ? '#f5d020' : (rank === 2 ? '#4ed1e6' : color);
  ctx.stroke();
  ctx.shadowBlur = 0;
  ctx.font = '900 28px JetBrains Mono, monospace';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillStyle = rank === 1 ? '#fff4a6' : '#e9f9ff';
  ctx.fillText(`#${rank}`, 58, 36);
  ctx.font = '800 15px JetBrains Mono, monospace';
  ctx.fillStyle = '#7efcff';
  ctx.fillText('LIVE', 118, 36);
  const tex = new THREE.CanvasTexture(canvas);
  tex.minFilter = THREE.LinearFilter;
  return tex;
};

const buildRankLabelSprite = (color) => {
  const rankTextures = [1, 2, 3].map(rank => buildRankLabelTexture(rank, color));
  const rankLabel = new THREE.Sprite(new THREE.SpriteMaterial({
    map: rankTextures[0],
    color: 0xffffff,
    transparent: true,
    opacity: 0,
    depthWrite: false,
    depthTest: false,
    blending: THREE.AdditiveBlending,
  }));
  rankLabel.renderOrder = 10;
  rankLabel.userData = { rankTextures, currentRank: 0 };
  return rankLabel;
};

const sampleFlatRoutePoint = (fromCode, toCode, t, arcSign = 1) => {
  const from = COUNTRY_GEO[fromCode];
  const to = COUNTRY_GEO[toCode];
  if (!from || !to) return new THREE.Vector3(0, 0, 0);
  const [ax, ay] = llToPlane(from[0], from[1]);
  const [bx, by] = llToPlane(to[0], to[1]);
  const dx = bx - ax;
  const dy = by - ay;
  const len = Math.max(1, Math.hypot(dx, dy));
  const bow = Math.sin(Math.PI * t) * clamp(len * 0.13, 10, 28) * arcSign;
  const nx = -dy / len;
  const ny = dx / len;
  return new THREE.Vector3(
    ax + dx * t + nx * bow,
    ay + dy * t + ny * bow,
    1.35 + Math.sin(Math.PI * t) * 0.45,
  );
};

const writeFlatRouteGeometry = (segment, fromCode, toCode) => {
  const attr = segment.line.geometry.getAttribute('position');
  for (let i = 0; i < ROUTE_POINT_COUNT; i++) {
    const t = i / (ROUTE_POINT_COUNT - 1);
    const p = sampleFlatRoutePoint(fromCode, toCode, t, segment.arcSign);
    attr.setXYZ(i, p.x, p.y, p.z);
  }
  attr.needsUpdate = true;
  segment.line.geometry.setDrawRange(0, ROUTE_POINT_COUNT);
  segment.from = fromCode;
  segment.to = toCode;
};

// ───────────────────────────────────────────────────────────────
// Main globe component
// ───────────────────────────────────────────────────────────────
const Globe = React.forwardRef(({
  countries, topRanked, onCountryHover, onCountryClick,
  unfolded, setUnfolded, rotationSpeed = 0.04, scale = 1.0,
  focusCodes = [],
  selectedCountryCode = null,
}, ref) => {
  const mountRef = React.useRef(null);
  const previousSelectedCodeRef = React.useRef(selectedCountryCode);
  const stateRef = React.useRef({
    initialized: false, scene: null, camera: null, renderer: null,
    pivot: null, countryMeshes: {}, pinGroup: null, beamGroup: null,
    candleGroup: null, activityPulseGroup: null, rankLabelGroup: null,
    worldOutlineMeshes: [], flatMapBackdrop: null, flatGridGroup: null,
    flatLightGroup: null, flatScanGroup: null, flatRouteGroup: null, flatBeaconGroup: null, stageRingGroup: null,
    routeSegments: [],
    pins: {}, beams: {}, activityPulses: {}, rankLabels: {}, raycaster: null, mouse: null,
    targetMorph: 0, morph: 0, targetCamZ: 360, camZ: 360,
    autoRotate: true, autoRotateBlend: 1, rotY: SHOWCASE_ROT_Y, rotX: SHOWCASE_ROT_X, hovered: null, selectedCountryCode: null,
    focusRotY: null, focusRotX: null,
    dragging: false, dragPointerId: null, dragStartX: 0, dragStartY: 0,
    dragLastX: 0, dragLastY: 0, didDrag: false,
    inertiaRotY: 0, inertiaRotX: 0,
    countries: [], topRanked: [], focusCodes: [],
  });
  const [loadingState, setLoadingState] = React.useState('Loading geography…');

  // Keep refs of latest data
  React.useEffect(() => { stateRef.current.countries = countries; }, [countries]);
  React.useEffect(() => { stateRef.current.topRanked = topRanked; }, [topRanked]);
  React.useEffect(() => { stateRef.current.focusCodes = focusCodes || []; }, [focusCodes]);
  React.useEffect(() => {
    stateRef.current.selectedCountryCode = selectedCountryCode;
    const previousCode = previousSelectedCodeRef.current;
    if (selectedCountryCode && previousCode && previousCode !== selectedCountryCode) {
      const focus = getCountryFocusRotation(selectedCountryCode);
      if (focus) {
        stateRef.current.focusRotY = focus.rotY;
        stateRef.current.focusRotX = focus.rotX;
        stateRef.current.inertiaRotY = 0;
        stateRef.current.inertiaRotX = 0;
      }
    }
    previousSelectedCodeRef.current = selectedCountryCode;
  }, [selectedCountryCode]);

  // Animate morph + camera when unfolded toggles
  React.useEffect(() => {
    stateRef.current.targetMorph = unfolded ? 1 : 0;
    stateRef.current.targetCamZ = unfolded ? 440 : 360;
    if (!unfolded) {
      stateRef.current.rotY = SHOWCASE_ROT_Y;
      stateRef.current.rotX = SHOWCASE_ROT_X;
      stateRef.current.inertiaRotY = 0;
      stateRef.current.inertiaRotX = 0;
    }
  }, [unfolded]);

  // ── Init Three.js once
  React.useEffect(() => {
    if (!mountRef.current || !window.THREE || !window.topojson) return;
    const mount = mountRef.current;

    const scene = new THREE.Scene();
    scene.background = null;

    const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 5000);
    camera.position.set(0, 0, 360);

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

    // Pivot for rotation + scale animation
    const pivot = new THREE.Group();
    scene.add(pivot);

    // Ocean / base sphere
    const sphereGeom = new THREE.SphereGeometry(RADIUS * 0.998, 80, 80);
    const sphereMat = new THREE.MeshBasicMaterial({
      color: 0x1d1740,
      transparent: true,
      opacity: 0.55,
    });
    const baseSphere = new THREE.Mesh(sphereGeom, sphereMat);
    baseSphere.renderOrder = 1;
    pivot.add(baseSphere);
    stateRef.current.baseSphere = baseSphere;

    // Flat-map backing surface. It fades in during unfold so the map reads
    // like a deliberate tactical surface instead of loose lines on dark glass.
    const flatMapBackdropMat = new THREE.ShaderMaterial({
      vertexShader: FLAT_BACKDROP_VERT,
      fragmentShader: FLAT_BACKDROP_FRAG,
      uniforms: {
        uMorph: { value: 0 },
        uTime: { value: 0 },
      },
      transparent: true,
      depthWrite: false,
      depthTest: false,
      side: THREE.DoubleSide,
    });
    const flatMapBackdrop = new THREE.Mesh(
      new THREE.PlaneGeometry(430, 218),
      flatMapBackdropMat
    );
    flatMapBackdrop.position.z = -3.5;
    flatMapBackdrop.renderOrder = 0;
    pivot.add(flatMapBackdrop);

    const flatGridGroup = new THREE.Group();
    const flatPoint = (lat, lng, z = -2.2) => {
      const [x, y] = llToPlane(lat, lng);
      return new THREE.Vector3(x, y, z);
    };
    const makeFlatLine = (points, color = 0x4ed1e6) => {
      const mat = new THREE.LineBasicMaterial({
        color,
        transparent: true,
        opacity: 0,
        depthWrite: false,
        depthTest: false,
      });
      const geom = new THREE.BufferGeometry().setFromPoints(points);
      const line = new THREE.Line(geom, mat);
      line.renderOrder = 1;
      flatGridGroup.add(line);
    };
    for (let lng = -180; lng <= 180; lng += 30) {
      makeFlatLine([
        flatPoint(-88, lng),
        flatPoint(88, lng),
      ]);
    }
    for (let lat = -60; lat <= 60; lat += 30) {
      makeFlatLine([
        flatPoint(lat, -180),
        flatPoint(lat, 180),
      ]);
    }
    makeFlatLine([
      new THREE.Vector3(-PLANE_W / 2, -PLANE_H / 2, -2),
      new THREE.Vector3(PLANE_W / 2, -PLANE_H / 2, -2),
      new THREE.Vector3(PLANE_W / 2, PLANE_H / 2, -2),
      new THREE.Vector3(-PLANE_W / 2, PLANE_H / 2, -2),
      new THREE.Vector3(-PLANE_W / 2, -PLANE_H / 2, -2),
    ], 0xf5d020);
    pivot.add(flatGridGroup);

    // Atmosphere glow (back-rim)
    const atmGeom = new THREE.SphereGeometry(RADIUS * 1.08, 64, 64);
    const atmMat = new THREE.ShaderMaterial({
      vertexShader: `
        varying vec3 vNormal;
        void main() {
          vNormal = normalize(normalMatrix * normal);
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
      `,
      fragmentShader: `
        varying vec3 vNormal;
        uniform float uMorph;
        void main() {
          float rim = pow(1.0 - abs(vNormal.z), 3.0);
          vec3 c = mix(vec3(0.31,0.82,0.90), vec3(0.85,0.28,0.65), 0.35);
          gl_FragColor = vec4(c * rim, rim * (1.0 - uMorph) * 0.85);
        }
      `,
      uniforms: { uMorph: { value: 0 } },
      transparent: true,
      side: THREE.BackSide,
      blending: THREE.AdditiveBlending,
      depthWrite: false,
    });
    const atmMesh = new THREE.Mesh(atmGeom, atmMat);
    pivot.add(atmMesh);

    // Lat/lng grid (subtle)
    const gridGroup = new THREE.Group();
    const gridMat = new THREE.LineBasicMaterial({ color: 0x4ed1e6, transparent: true, opacity: 0.08 });
    // longitude meridians every 15°
    for (let lng = -180; lng < 180; lng += 30) {
      const pts = [];
      for (let lat = -85; lat <= 85; lat += 5) {
        pts.push(new THREE.Vector3(...llToSphere(lat, lng, RADIUS * 1.0005)));
      }
      const g = new THREE.BufferGeometry().setFromPoints(pts);
      gridGroup.add(new THREE.Line(g, gridMat));
    }
    // latitude rings
    for (let lat = -60; lat <= 60; lat += 30) {
      const pts = [];
      for (let lng = -180; lng <= 180; lng += 5) {
        pts.push(new THREE.Vector3(...llToSphere(lat, lng, RADIUS * 1.0005)));
      }
      const g = new THREE.BufferGeometry().setFromPoints(pts);
      gridGroup.add(new THREE.Line(g, gridMat));
    }
    pivot.add(gridGroup);

    const flatLightGroup = new THREE.Group();
    const makeFlatGlow = (lat, lng, color, scale, opacity, phase) => {
      const glowMat = new THREE.SpriteMaterial({
        map: buildSoftGlowSprite(color),
        transparent: true,
        opacity: 0,
        depthWrite: false,
        depthTest: false,
        blending: THREE.AdditiveBlending,
      });
      const glow = new THREE.Sprite(glowMat);
      const pos = flatPoint(lat, lng, -2.6);
      glow.position.copy(pos);
      glow.scale.set(scale, scale * 0.58, 1);
      glow.renderOrder = 1;
      glow.userData = { baseOpacity: opacity, phase };
      flatLightGroup.add(glow);
    };
    makeFlatGlow(32, -88, '#4ed1e6', 162, 0.24, 0.0);
    makeFlatGlow(42, 18, '#f5d020', 128, 0.18, 1.6);
    makeFlatGlow(24, 98, '#e637a8', 154, 0.18, 2.7);
    makeFlatGlow(-24, 128, '#4ed1e6', 126, 0.14, 3.8);
    pivot.add(flatLightGroup);

    const flatScanGroup = new THREE.Group();
    const makeScanPlane = (vertical, color, baseOpacity, speed, phase) => {
      const mat = new THREE.MeshBasicMaterial({
        map: buildScanBarTexture(color, vertical),
        transparent: true,
        opacity: 0,
        depthWrite: false,
        depthTest: false,
        blending: THREE.AdditiveBlending,
        side: THREE.DoubleSide,
      });
      const geom = vertical
        ? new THREE.PlaneGeometry(10, PLANE_H * 1.08)
        : new THREE.PlaneGeometry(PLANE_W * 1.08, 10);
      const scan = new THREE.Mesh(geom, mat);
      scan.position.z = -1.05;
      scan.renderOrder = 2;
      scan.userData = { vertical, baseOpacity, speed, phase };
      flatScanGroup.add(scan);
    };
    makeScanPlane(false, '#4ed1e6', 0.22, 0.42, 0.2);
    makeScanPlane(false, '#e637a8', 0.14, 0.31, 2.4);
    makeScanPlane(true, '#f5d020', 0.12, 0.27, 1.3);
    pivot.add(flatScanGroup);

    const flatRouteGroup = new THREE.Group();
    const routeSegments = [];
    for (let i = 0; i < 6; i++) {
      const color = NEON_ROUTE_COLORS[i % NEON_ROUTE_COLORS.length];
      const routeGeom = new THREE.BufferGeometry();
      routeGeom.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(ROUTE_POINT_COUNT * 3), 3));
      routeGeom.setDrawRange(0, 0);
      const routeMat = new THREE.LineBasicMaterial({
        color,
        transparent: true,
        opacity: 0,
        depthWrite: false,
        depthTest: false,
        blending: THREE.AdditiveBlending,
      });
      const line = new THREE.Line(routeGeom, routeMat);
      line.renderOrder = 5;
      flatRouteGroup.add(line);

      const packets = [0, 1].map((packetIndex) => {
        const packetMat = new THREE.SpriteMaterial({
          map: buildSoftGlowSprite(color),
          color: 0xffffff,
          transparent: true,
          opacity: 0,
          depthWrite: false,
          depthTest: false,
          blending: THREE.AdditiveBlending,
        });
        const packet = new THREE.Sprite(packetMat);
        packet.scale.set(9, 9, 1);
        packet.renderOrder = 6;
        packet.userData = { offset: packetIndex * 0.48 };
        flatRouteGroup.add(packet);
        return packet;
      });

      routeSegments.push({ line, packets, from: null, to: null, arcSign: i % 2 === 0 ? 1 : -1 });
    }
    pivot.add(flatRouteGroup);

    const flatBeaconGroup = new THREE.Group();
    [
      { inner: 5.2, outer: 5.8, color: 0x4ed1e6, opacity: 0.62, speed: 1.00 },
      { inner: 9.0, outer: 9.6, color: 0xe637a8, opacity: 0.42, speed: -0.72 },
      { inner: 13.8, outer: 14.4, color: 0xf5d020, opacity: 0.28, speed: 0.44 },
    ].forEach((ringDef) => {
      const ring = new THREE.Mesh(
        new THREE.RingGeometry(ringDef.inner, ringDef.outer, 96),
        new THREE.MeshBasicMaterial({
          color: ringDef.color,
          transparent: true,
          opacity: 0,
          depthWrite: false,
          depthTest: false,
          blending: THREE.AdditiveBlending,
          side: THREE.DoubleSide,
        }),
      );
      ring.renderOrder = 7;
      ring.userData = { baseOpacity: ringDef.opacity, speed: ringDef.speed };
      flatBeaconGroup.add(ring);
    });
    pivot.add(flatBeaconGroup);

    // Stadium-like orbit rings give the sphere a stronger "live arena" read.
    const stageRingGroup = new THREE.Group();
    const makeStageRing = (radius, color, opacity, rotation, spin) => {
      const pts = [];
      for (let i = 0; i <= 220; i++) {
        const a = (i / 220) * Math.PI * 2;
        pts.push(new THREE.Vector3(Math.cos(a) * radius, Math.sin(a) * radius, 0));
      }
      const geom = new THREE.BufferGeometry().setFromPoints(pts);
      const mat = new THREE.LineBasicMaterial({
        color,
        transparent: true,
        opacity,
        depthWrite: false,
        depthTest: true,
        blending: THREE.AdditiveBlending,
      });
      const ring = new THREE.Line(geom, mat);
      ring.rotation.set(rotation[0], rotation[1], rotation[2]);
      ring.userData = { baseOpacity: opacity, spin };
      ring.renderOrder = 0;
      stageRingGroup.add(ring);
    };
    makeStageRing(RADIUS * 1.19, 0x4ed1e6, 0.26, [0.06, 0.12, 0], 0.025);
    makeStageRing(RADIUS * 1.28, 0xf5d020, 0.13, [Math.PI / 2.9, 0.04, 0.2], -0.018);
    makeStageRing(RADIUS * 1.36, 0xe637a8, 0.10, [-Math.PI / 3.2, -0.08, -0.16], 0.014);
    pivot.add(stageRingGroup);

    // Pin, candle, pulse and restrained rank label layers
    const pinGroup = new THREE.Group();
    pivot.add(pinGroup);
    const candleGroup = new THREE.Group();
    candleGroup.name = 'top-country-candleGroup';
    const beamGroup = candleGroup; // legacy state alias used by the old top performer beam updater
    pivot.add(candleGroup);
    const activityPulseGroup = new THREE.Group();
    activityPulseGroup.name = 'top-country-activityPulseGroup';
    pivot.add(activityPulseGroup);
    const rankLabelGroup = new THREE.Group();
    rankLabelGroup.name = 'top-country-rankLabelGroup';
    pivot.add(rankLabelGroup);

    // Raycaster (we'll raycast pins for hover)
    const raycaster = new THREE.Raycaster();
    const mouse = new THREE.Vector2(-2, -2);

    // Build pins for all 48 countries
    const pinsMap = {};
    const beamsMap = {};
    const activityPulsesMap = {};
    const rankLabelsMap = {};
    Object.keys(COUNTRY_GEO).forEach((code) => {
      const [lat, lng, color] = COUNTRY_GEO[code];
      const sp = llToSphere(lat, lng, RADIUS * 1.012);
      const pp = llToPlane(lat, lng);
      pp[2] = 0.5; // slight z so it doesn't z-fight on plane

      // Pin: a small sphere with additive sprite glow
      const pinMat = new THREE.SpriteMaterial({
        map: buildPinSprite(color),
        color: 0xffffff,
        transparent: true,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
      });
      const sprite = new THREE.Sprite(pinMat);
      sprite.scale.set(8, 8, 1);
      sprite.position.set(...sp);
      sprite.userData = { code, color, sp, pp };
      pinGroup.add(sprite);
      pinsMap[code] = sprite;

      const beam = buildCountryCandle(color);
      const normal = new THREE.Vector3(sp[0], sp[1], sp[2]).normalize();
      const quat = new THREE.Quaternion();
      quat.setFromUnitVectors(new THREE.Vector3(0, 1, 0), normal);
      beam.quaternion.copy(quat);
      beam.position.set(...sp);
      beam.userData = { ...beam.userData, sp, pp, code, color };
      candleGroup.add(beam);
      beamsMap[code] = beam;

      const activityPulse = buildActivityPulseRing(color);
      const pulseQuat = new THREE.Quaternion();
      pulseQuat.setFromUnitVectors(new THREE.Vector3(0, 0, 1), normal);
      activityPulse.quaternion.copy(pulseQuat);
      activityPulse.position.set(...sp);
      activityPulse.userData = { ...activityPulse.userData, sp, pp, code, color };
      activityPulseGroup.add(activityPulse);
      activityPulsesMap[code] = activityPulse;

      const rankLabel = buildRankLabelSprite(color);
      rankLabel.scale.set(16, 6.5, 1);
      rankLabel.position.set(sp[0] + normal.x * 46, sp[1] + normal.y * 46, sp[2] + normal.z * 46);
      rankLabel.userData = { ...rankLabel.userData, sp, pp, code, color };
      rankLabelGroup.add(rankLabel);
      rankLabelsMap[code] = rankLabel;
    });

    Object.assign(stateRef.current, {
      scene, camera, renderer, pivot, pinGroup, beamGroup, candleGroup, activityPulseGroup, rankLabelGroup,
      pins: pinsMap, beams: beamsMap, activityPulses: activityPulsesMap, rankLabels: rankLabelsMap, raycaster, mouse, atmMat, gridGroup,
      flatMapBackdrop, flatGridGroup, flatLightGroup, flatScanGroup,
      flatRouteGroup, flatBeaconGroup, stageRingGroup, routeSegments,
      initialized: true,
    });

    // Load topojson and build country meshes
    const TOPO_URL = 'https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json';
    fetch(TOPO_URL)
      .then(r => r.json())
      .then((topo) => {
        const features = window.topojson.feature(topo, topo.objects.countries).features;
        const meshGroup = new THREE.Group();
        const countryMeshes = {};
        const worldOutlineMeshes = [];

        // Build a code→feature map using ISO3_TO_NUM
        const featureById = {};
        features.forEach((f) => { featureById[f.id] = f; });
        const worldCupIds = new Set(Object.values(ISO3_TO_NUM));

        // Default: render ALL countries as faint outlines (cyan)
        features.forEach((f, i) => {
          const isWC = worldCupIds.has(f.id);
          if (isWC) return;
          const color = isWC ? '#4ed1e6' : '#5cd5e8';
          const { outlineMesh } = buildCountryMeshes(f, color, i);
          if (outlineMesh) {
            const baseIntensity = 0.18;
            outlineMesh.material.uniforms.uIntensity.value = baseIntensity;
            worldOutlineMeshes.push({ mesh: outlineMesh, baseIntensity, isWC });
            meshGroup.add(outlineMesh);
          }
        });

        // Overlay WC countries with their flag-colored fill + brighter outline
        Object.entries(ISO3_TO_NUM).forEach(([code, isoId]) => {
          const feature = featureById[isoId];
          if (!feature) return;
          const geoEntry = COUNTRY_GEO[code];
          if (!geoEntry) return;
          const color = geoEntry[2];
          const { fillMesh, outlineMesh } = buildCountryMeshes(feature, color, 0);
          if (fillMesh) {
            meshGroup.add(fillMesh);
            countryMeshes[code] = countryMeshes[code] || {};
            countryMeshes[code].fill = fillMesh;
          }
          if (outlineMesh) {
            outlineMesh.material.uniforms.uIntensity.value = 1.6;
            meshGroup.add(outlineMesh);
            countryMeshes[code] = countryMeshes[code] || {};
            countryMeshes[code].outline = outlineMesh;
          }
        });

        pivot.add(meshGroup);
        stateRef.current.countryMeshes = countryMeshes;
        stateRef.current.worldOutlineMeshes = worldOutlineMeshes;
        stateRef.current.meshGroup = meshGroup;
        setLoadingState(null);
      })
      .catch((err) => {
        console.error('Failed to load topojson', err);
        setLoadingState('Map data unavailable — pins only');
      });

    renderer.domElement.style.touchAction = 'none';
    renderer.domElement.style.cursor = 'grab';

    const updatePointerPosition = (e) => {
      const rect = renderer.domElement.getBoundingClientRect();
      stateRef.current.mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
      stateRef.current.mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
    };

    const pickHoveredCode = () => {
      raycaster.setFromCamera(stateRef.current.mouse, camera);
      const hits = raycaster.intersectObjects(Object.values(stateRef.current.pins), false);
      return hits.length ? hits[0].object.userData.code : null;
    };

    const handlePointerDown = (e) => {
      if (e.isPrimary === false) return;
      updatePointerPosition(e);
      const st = stateRef.current;
      st.dragging = true;
      st.dragPointerId = e.pointerId;
      st.dragStartX = e.clientX;
      st.dragStartY = e.clientY;
      st.dragLastX = e.clientX;
      st.dragLastY = e.clientY;
      st.didDrag = false;
      st.inertiaRotY = 0;
      st.inertiaRotX = 0;
      st.focusRotY = null;
      st.focusRotX = null;
      renderer.domElement.style.cursor = 'grabbing';
      try { renderer.domElement.setPointerCapture?.(e.pointerId); } catch (_) {}
    };

    const handlePointerMove = (e) => {
      if (e.isPrimary === false) return;
      updatePointerPosition(e);
      const st = stateRef.current;
      if (!st.dragging || st.dragPointerId !== e.pointerId) return;

      const dx = e.clientX - st.dragLastX;
      const dy = e.clientY - st.dragLastY;
      const totalDrag = Math.hypot(e.clientX - st.dragStartX, e.clientY - st.dragStartY);
      if (totalDrag > 4) st.didDrag = true;

      const sphereWeight = 1 - Math.min(1, st.morph);
      if (sphereWeight > 0.08) {
        st.rotY += dx * 0.0065 * sphereWeight;
        st.rotX = clamp(st.rotX + dy * 0.0045 * sphereWeight, -0.42, 0.28);
        st.inertiaRotY = dx * 0.004 * sphereWeight;
        st.inertiaRotX = dy * 0.0024 * sphereWeight;
      }

      st.dragLastX = e.clientX;
      st.dragLastY = e.clientY;
    };

    const finishPointer = (e) => {
      const st = stateRef.current;
      if (!st.dragging || st.dragPointerId !== e.pointerId) return;
      updatePointerPosition(e);
      const wasTap = !st.didDrag;
      st.dragging = false;
      st.dragPointerId = null;
      try { renderer.domElement.releasePointerCapture?.(e.pointerId); } catch (_) {}

      const hit = pickHoveredCode() || st.hovered;
      renderer.domElement.style.cursor = hit ? 'pointer' : 'grab';
      if (wasTap && hit && onCountryClick) onCountryClick(hit);
    };
    renderer.domElement.addEventListener('pointerdown', handlePointerDown);
    renderer.domElement.addEventListener('pointermove', handlePointerMove);
    renderer.domElement.addEventListener('pointerup', finishPointer);
    renderer.domElement.addEventListener('pointercancel', finishPointer);

    // Resize
    const resize = () => {
      const w = mount.clientWidth;
      const h = mount.clientHeight;
      renderer.setSize(w, h);
      camera.aspect = w / h;
      camera.updateProjectionMatrix();
    };
    resize();
    const ro = new ResizeObserver(resize);
    ro.observe(mount);

    // Animate
    let raf;
    const clock = new THREE.Clock();
    const tick = () => {
      const dt = clock.getDelta();
      const t = clock.elapsedTime;
      const st = stateRef.current;

      // Smooth morph (snappier)
      st.morph += (st.targetMorph - st.morph) * Math.min(1, dt * 6);
      // Smooth cam z
      st.camZ += (st.targetCamZ - st.camZ) * Math.min(1, dt * 4);
      camera.position.z = st.camZ;

      // Rotate (only when fully on sphere — slow when morphing)
      const sphereInfluence = 1 - st.morph;
      const hasFocusRotation = st.focusRotY !== null && st.focusRotX !== null && !st.dragging && sphereInfluence > 0.08;
      const targetAutoRotate = hasFocusRotation || st.dragging ? 0 : 1;
      st.autoRotateBlend += (targetAutoRotate - st.autoRotateBlend) * Math.min(1, dt * 1.6);
      if (hasFocusRotation) {
        const focusEase = Math.min(1, dt * 3.0);
        st.rotY += shortestAngleDelta(st.rotY, st.focusRotY) * focusEase;
        st.rotX = clamp(st.rotX + (st.focusRotX - st.rotX) * focusEase, -0.42, 0.42);
        st.inertiaRotY = 0;
        st.inertiaRotX = 0;
      } else {
        const rotMul = sphereInfluence * stateRef.current.rotMul * st.autoRotateBlend;
        st.rotY += dt * rotMul;
        if (!st.dragging && sphereInfluence > 0.05) {
          st.rotY += st.inertiaRotY;
          st.rotX = clamp(st.rotX + st.inertiaRotX, -0.42, 0.42);
          const friction = Math.pow(0.90, dt * 60);
          st.inertiaRotY *= friction;
          st.inertiaRotX *= friction;
          if (Math.abs(st.inertiaRotY) < 0.0001) st.inertiaRotY = 0;
          if (Math.abs(st.inertiaRotX) < 0.0001) st.inertiaRotX = 0;
        }
      }
      // As morph increases, lerp rotation back toward identity so the flat plane faces camera
      const targetRotY = 0;
      const blendedRotY = st.rotY * (1 - st.morph) + targetRotY * st.morph;
      pivot.rotation.y = blendedRotY;
      // On flat, pivot rotation should be neutral
      pivot.rotation.x = st.rotX * (1 - st.morph) - st.morph * 0.05;

      // Update country uniforms
      Object.entries(st.countryMeshes).forEach(([code, m]) => {
        const data = (st.countries || []).find(c => c.code === code);
        if (!data) return;
        const isTop = (st.topRanked || []).some(c => c.code === code);
        const hasFocus = (st.focusCodes || []).length > 0;
        const isSelected = st.selectedCountryCode === code;
        const isFocused = !hasFocus || (st.focusCodes || []).includes(code) || st.hovered === code || isSelected;
        const focusDim = isFocused ? 1 : (st.morph > 0.45 ? 0.38 : 0.68);
        const pulse = 0.5 + 0.5 * Math.sin(t * (1.0 + data.momentum) + code.charCodeAt(0));
        if (m.fill) {
          m.fill.material.uniforms.uMorph.value = st.morph;
          m.fill.material.uniforms.uPulse.value = pulse * (0.18 + data.momentum * 0.34) * focusDim;
          m.fill.material.uniforms.uActive.value = (isTop || (hasFocus && isFocused) || isSelected) ? 1.0 : 0.0;
          m.fill.material.uniforms.uHover.value = (st.hovered === code || isSelected) ? 1.0 : 0.0;
        }
        if (m.outline) {
          m.outline.material.uniforms.uMorph.value = st.morph;
          m.outline.material.uniforms.uIntensity.value = (0.78
            + pulse * 0.34
            + (isTop ? 0.42 : 0)
            + (st.hovered === code ? 0.62 : 0)
            + (isSelected ? 0.48 : 0)) * focusDim;
        }
      });

      (st.worldOutlineMeshes || []).forEach(({ mesh, baseIntensity, isWC }) => {
        mesh.material.uniforms.uMorph.value = st.morph;
        mesh.material.uniforms.uIntensity.value =
          baseIntensity + st.morph * (isWC ? 0.55 : 0.24);
      });

      // Update atm + grid morph + base sphere
      if (st.atmMat) st.atmMat.uniforms.uMorph.value = st.morph;
      if (st.baseSphere) st.baseSphere.material.opacity = 0.55 * (1 - st.morph);
      if (st.gridGroup) st.gridGroup.children.forEach((line) => {
        line.material.opacity = 0.08 * (1 - st.morph);
      });
      if (st.stageRingGroup) st.stageRingGroup.children.forEach((ring) => {
        ring.material.opacity = ring.userData.baseOpacity * (1 - st.morph);
        ring.rotation.z += dt * ring.userData.spin * (1 - st.morph);
      });
      if (st.flatMapBackdrop) {
        st.flatMapBackdrop.material.uniforms.uMorph.value = st.morph;
        st.flatMapBackdrop.material.uniforms.uTime.value = t;
      }
      if (st.flatGridGroup) st.flatGridGroup.children.forEach((line, index) => {
        const isBorder = index === st.flatGridGroup.children.length - 1;
        line.material.opacity = (isBorder ? 0.48 : 0.18) * st.morph;
      });
      if (st.flatLightGroup) st.flatLightGroup.children.forEach((light) => {
        const pulse = 0.86 + Math.sin(t * 0.9 + light.userData.phase) * 0.14;
        light.material.opacity = light.userData.baseOpacity * st.morph * pulse;
      });
      if (st.flatScanGroup) st.flatScanGroup.children.forEach((scan) => {
        const travel = Math.sin(t * scan.userData.speed + scan.userData.phase);
        if (scan.userData.vertical) scan.position.x = travel * (PLANE_W * 0.48);
        else scan.position.y = travel * (PLANE_H * 0.46);
        scan.material.opacity = scan.userData.baseOpacity * st.morph * (0.72 + Math.abs(travel) * 0.28);
      });
      if (st.routeSegments?.length) {
        const baseRouteCodes = (st.focusCodes?.length
          ? st.focusCodes
          : (st.topRanked || []).map(c => c.code)
        ).filter(code => COUNTRY_GEO[code]);
        const selectedRouteCode = st.selectedCountryCode;
        const activeCodes = (selectedRouteCode && COUNTRY_GEO[selectedRouteCode]
          ? [selectedRouteCode, ...baseRouteCodes.filter(code => code !== selectedRouteCode)]
          : baseRouteCodes
        ).filter(code => COUNTRY_GEO[code]);
        const routePairs = [];
        for (let i = 0; i < activeCodes.length - 1; i++) routePairs.push([activeCodes[i], activeCodes[i + 1]]);
        if (activeCodes.length > 2) routePairs.push([activeCodes[0], activeCodes[activeCodes.length - 1]]);
        st.routeSegments.forEach((segment, index) => {
          const pair = routePairs[index];
          if (!pair || st.morph < 0.08) {
            segment.line.material.opacity = 0;
            segment.line.geometry.setDrawRange(0, 0);
            segment.packets.forEach(packet => { packet.material.opacity = 0; });
            segment.from = null;
            segment.to = null;
            return;
          }
          const [fromCode, toCode] = pair;
          if (segment.from !== fromCode || segment.to !== toCode) {
            writeFlatRouteGeometry(segment, fromCode, toCode);
          }
          const routePulse = 0.72 + 0.28 * Math.sin(t * 1.45 + index);
          segment.line.material.opacity = st.morph * (0.20 + index * 0.018) * routePulse;
          segment.packets.forEach((packet) => {
            const progress = (t * 0.13 + packet.userData.offset + index * 0.075) % 1;
            const pos = sampleFlatRoutePoint(fromCode, toCode, progress, segment.arcSign);
            packet.position.copy(pos);
            const size = 7.5 + Math.sin(t * 2.4 + index) * 1.2;
            packet.scale.set(size, size, 1);
            packet.material.opacity = st.morph * (0.34 + Math.sin(progress * Math.PI) * 0.28);
          });
        });
      }
      if (st.flatBeaconGroup) {
        const beaconCode = st.hovered || st.selectedCountryCode || (st.focusCodes || [])[0];
        const beaconPin = beaconCode ? st.pins[beaconCode] : null;
        const targetOpacity = beaconPin ? st.morph : 0;
        if (beaconPin) {
          st.flatBeaconGroup.position.copy(beaconPin.position);
          st.flatBeaconGroup.position.z = 1.75;
        }
        st.flatBeaconGroup.children.forEach((ring, index) => {
          const pulse = 1 + Math.sin(t * 1.9 + index * 0.9) * 0.12;
          const hoverBoost = st.hovered ? 1.55 : 0.72;
          ring.scale.setScalar((1 + index * 0.08) * pulse);
          ring.rotation.z += dt * ring.userData.speed;
          ring.material.opacity = ring.userData.baseOpacity * targetOpacity * hoverBoost;
        });
      }

      // Update pin positions (lerp sphere → plane)
      Object.values(st.pins).forEach((pin) => {
        const { sp, pp } = pin.userData;
        pin.position.set(
          sp[0] * (1 - st.morph) + pp[0] * st.morph,
          sp[1] * (1 - st.morph) + pp[1] * st.morph,
          sp[2] * (1 - st.morph) + pp[2] * st.morph,
        );
        // Pulse the pin
        const code = pin.userData.code;
        const data = (st.countries || []).find(c => c.code === code);
        const m = data ? data.momentum : 0.7;
        const isTop = (st.topRanked || []).some(c => c.code === code);
        const hasFocus = (st.focusCodes || []).length > 0;
        const isSelected = st.selectedCountryCode === code;
        const isFocused = !hasFocus || (st.focusCodes || []).includes(code) || st.hovered === code || isSelected;
        const base = 8 + m * 3 + (isTop ? 4 : 0);
        const wobble = 1 + 0.12 * Math.sin(t * (2 + m * 2) + code.charCodeAt(0));
        const hoverBoost = st.hovered === code ? 1.35 : (isSelected ? 1.18 : 1.0);
        const flatScale = 1 - st.morph * 0.62;
        const focusScale = isFocused ? 1 : (st.morph > 0.45 ? 0.48 : 0.76);
        const targetOpacity = isFocused ? (isSelected ? 0.98 : 0.88) : (st.morph > 0.45 ? 0.22 : 0.50);
        pin.material.opacity += (targetOpacity - pin.material.opacity) * Math.min(1, dt * 6);
        const s = base * wobble * hoverBoost * flatScale * focusScale;
        pin.scale.set(s, s, 1);
      });

      // Update beams — only top performers visible
      Object.entries(st.beams).forEach(([code, beam]) => {
        const rank = getTopCountryRank(st.topRanked || [], code);
        const isTop = rank > 0 && rank <= 3;
        const data = (st.countries || []).find(c => c.code === code);
        const activityStrength = getCountryActivityStrength(data);
        const rankBoost = isTop ? (4 - rank) / 3 : 0;
        const targetOp = isTop && data
          ? Math.min(0.92, 0.42 + rankBoost * 0.32 + activityStrength * 0.18) * (1 - st.morph * 0.82)
          : 0;
        beam.material.uniforms.uOpacity.value +=
          (targetOp - beam.material.uniforms.uOpacity.value) * Math.min(1, dt * 5.5);
        beam.material.uniforms.uTime.value = t;
        beam.material.uniforms.uRankBoost.value +=
          (rankBoost - beam.material.uniforms.uRankBoost.value) * Math.min(1, dt * 5);
        beam.material.uniforms.uPulseStrength.value +=
          (activityStrength - beam.material.uniforms.uPulseStrength.value) * Math.min(1, dt * 4);
        const rankHeight = rank === 1 ? 86 : (rank === 2 ? 68 : 54);
        const targetHeight = isTop && data
          ? rankHeight * (0.88 + activityStrength * 0.17)
          : 0.001;
        beam.userData.currentHeight +=
          (targetHeight - beam.userData.currentHeight) * Math.min(1, dt * 4.5);
        const radiusScale = Math.max(0.04, isTop ? 1.08 + rankBoost * 0.42 + activityStrength * 0.10 : 0.12);
        beam.scale.set(radiusScale, Math.max(0.001, beam.userData.currentHeight), radiusScale);
        beam.visible = beam.material.uniforms.uOpacity.value > 0.006 || isTop;
        // Position morph
        const { sp, pp } = beam.userData;
        const np = [
          sp[0] * (1 - st.morph) + pp[0] * st.morph,
          sp[1] * (1 - st.morph) + pp[1] * st.morph,
          sp[2] * (1 - st.morph) + pp[2] * (st.morph),
        ];
        beam.position.set(np[0], np[1], np[2]);

        // Beam orientation: on sphere = radial out; on plane = up Z
        const normalSphere = new THREE.Vector3(sp[0], sp[1], sp[2]).normalize();
        const normalPlane = new THREE.Vector3(0, 0, 1);
        const normal = new THREE.Vector3()
          .lerpVectors(normalSphere, normalPlane, st.morph).normalize();
        const quat = new THREE.Quaternion();
        quat.setFromUnitVectors(new THREE.Vector3(0, 1, 0), normal);
        beam.quaternion.copy(quat);
      });

      Object.entries(st.activityPulses || {}).forEach(([code, activityPulse]) => {
        const rank = getTopCountryRank(st.topRanked || [], code);
        const isTop = rank > 0 && rank <= 3;
        const data = (st.countries || []).find(c => c.code === code);
        const activityStrength = getCountryActivityStrength(data);
        const rankBoost = isTop ? (4 - rank) / 3 : 0;
        const targetStrength = isTop && data
          ? (0.32 + rankBoost * 0.22 + activityStrength * 0.16) * (1 - st.morph * 0.56)
          : 0;
        activityPulse.userData.currentStrength +=
          (targetStrength - activityPulse.userData.currentStrength) * Math.min(1, dt * 5.5);

        const { sp, pp } = activityPulse.userData;
        const np = [
          sp[0] * (1 - st.morph) + pp[0] * st.morph,
          sp[1] * (1 - st.morph) + pp[1] * st.morph,
          sp[2] * (1 - st.morph) + pp[2] * st.morph,
        ];
        activityPulse.position.set(np[0], np[1], np[2]);
        const normalSphere = new THREE.Vector3(sp[0], sp[1], sp[2]).normalize();
        const normalPlane = new THREE.Vector3(0, 0, 1);
        const normal = new THREE.Vector3()
          .lerpVectors(normalSphere, normalPlane, st.morph).normalize();
        const quat = new THREE.Quaternion();
        quat.setFromUnitVectors(new THREE.Vector3(0, 0, 1), normal);
        activityPulse.quaternion.copy(quat);
        activityPulse.visible = activityPulse.userData.currentStrength > 0.01 || isTop;

        activityPulse.children.forEach((node) => {
          if (node.userData.kind === 'burst') {
            const burstPulse = 0.86 + Math.sin(t * 3.1 + code.charCodeAt(1)) * 0.14;
            const burstScale = (20 + rankBoost * 13 + activityStrength * 7) * (1 - st.morph * 0.42) * burstPulse;
            node.scale.set(burstScale, burstScale, 1);
            node.material.opacity = node.userData.baseOpacity * activityPulse.userData.currentStrength * burstPulse;
            return;
          }
          const wave = (t * node.userData.speed + node.userData.phase) % 1;
          const falloff = 1 - wave;
          const ringScale = (0.82 + rankBoost * 0.26 + activityStrength * 0.2 + wave * 1.5) * (1 - st.morph * 0.34);
          node.scale.setScalar(Math.max(0.05, ringScale));
          node.rotation.z += dt * node.userData.spin;
          node.material.opacity = node.userData.baseOpacity * activityPulse.userData.currentStrength * falloff;
        });
      });

      Object.entries(st.rankLabels || {}).forEach(([code, rankLabel]) => {
        const rank = getTopCountryRank(st.topRanked || [], code);
        const isTop = rank > 0 && rank <= 3;
        const data = (st.countries || []).find(c => c.code === code);
        if (isTop && rankLabel.userData.currentRank !== rank) {
          rankLabel.material.map = rankLabel.userData.rankTextures[rank - 1];
          rankLabel.material.needsUpdate = true;
          rankLabel.userData.currentRank = rank;
        }
        const activityStrength = getCountryActivityStrength(data);
        const rankBoost = isTop ? (4 - rank) / 3 : 0;
        const targetOpacity = isTop
          ? (0.90 - (rank - 1) * 0.08 + activityStrength * 0.05) * (1 - st.morph * 0.68)
          : 0;
        rankLabel.material.opacity +=
          (targetOpacity - rankLabel.material.opacity) * Math.min(1, dt * 5);

        const { sp, pp } = rankLabel.userData;
        const base = [
          sp[0] * (1 - st.morph) + pp[0] * st.morph,
          sp[1] * (1 - st.morph) + pp[1] * st.morph,
          sp[2] * (1 - st.morph) + pp[2] * st.morph,
        ];
        const normalSphere = new THREE.Vector3(sp[0], sp[1], sp[2]).normalize();
        const normalPlane = new THREE.Vector3(0, 0, 1);
        const normal = new THREE.Vector3()
          .lerpVectors(normalSphere, normalPlane, st.morph).normalize();
        const candleHeight = st.beams[code]?.userData?.currentHeight || 0;
        const lift = (candleHeight + 14 + rankBoost * 5) * (1 - st.morph) + (9 + rankBoost * 3) * st.morph;
        rankLabel.position.set(
          base[0] + normal.x * lift,
          base[1] + normal.y * lift,
          base[2] + normal.z * lift,
        );
        const labelScale = (rank === 1 ? 27 : 23.5) * (1 - st.morph * 0.24);
        rankLabel.scale.set(labelScale, labelScale * 0.42, 1);
        rankLabel.visible = rankLabel.material.opacity > 0.01 || isTop;
      });

      // Raycast pins for hover
      const hovered = pickHoveredCode();
      if (hovered !== st.hovered) {
        st.hovered = hovered;
        if (onCountryHover) onCountryHover(hovered);
        if (!st.dragging) renderer.domElement.style.cursor = hovered ? 'pointer' : 'grab';
      }

      renderer.render(scene, camera);
      raf = requestAnimationFrame(tick);
    };
    stateRef.current.rotMul = rotationSpeed;
    raf = requestAnimationFrame(tick);

    return () => {
      cancelAnimationFrame(raf);
      ro.disconnect();
      renderer.domElement.removeEventListener('pointerdown', handlePointerDown);
      renderer.domElement.removeEventListener('pointermove', handlePointerMove);
      renderer.domElement.removeEventListener('pointerup', finishPointer);
      renderer.domElement.removeEventListener('pointercancel', finishPointer);
      mount.removeChild(renderer.domElement);
      renderer.dispose();
    };
  }, []);

  // Update rotation speed when prop changes
  React.useEffect(() => {
    stateRef.current.rotMul = rotationSpeed;
  }, [rotationSpeed]);

  // Expose imperative handle (for fold-back, etc.)
  React.useImperativeHandle(ref, () => ({
    fold: () => setUnfolded?.(false),
    unfold: () => setUnfolded?.(true),
  }));

  return (
    <div style={{
      position: 'absolute', inset: 0,
      transform: `scale(${scale})`, transformOrigin: 'center',
      transition: 'transform 600ms var(--ease)',
    }}>
      <div ref={mountRef} style={{ position: 'absolute', inset: 0 }} />
      {loadingState && (
        <div style={{
          position: 'absolute', inset: 0, display: 'grid', placeItems: 'center',
          color: 'var(--t3)', fontFamily: 'JetBrains Mono', fontSize: 12,
          letterSpacing: '0.1em', textTransform: 'uppercase', pointerEvents: 'none',
        }}>{loadingState}</div>
      )}
    </div>
  );
});

window.Globe = Globe;
