// Planet data with real NASA/Solar System Scope texture URLs
const PLANETS = [
  {
    id: "sun",
    name: "Sun",
    type: "Star · G2V",
    blurb:
      "A middle-aged yellow dwarf burning four million tonnes of mass into pure light every second. Source of all life in the system.",
    radius: 56,
    detailRadius: 320,
    orbitRadius: 0,
    period: 0,
    distance: "0 AU",
    day: "25 Earth days",
    year: "—",
    moons: 0,
    temp: "5,500 °C surface · 15M °C core",
    gravity: "274 m/s²",
    composition: "Hydrogen 73% · Helium 25% · trace metals",
    texture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/sunmap.jpg",
    color: "#FFD66B",
    emissive: "#FF7B2A",
    glowColor: "#FFB347",
    isStar: true,
  },
  {
    id: "mercury",
    name: "Mercury",
    type: "Terrestrial planet",
    blurb:
      "A scarred, sun-baked rock with no atmosphere. Days reach 430 °C; nights plunge to -180 °C. Pocked with billions of craters.",
    radius: 9,
    detailRadius: 280,
    orbitRadius: 110,
    period: 8,
    eccentricity: 0.206,
    distance: "0.39 AU",
    day: "59 Earth days",
    year: "88 Earth days",
    moons: 0,
    temp: "-180 to 430 °C",
    gravity: "3.7 m/s²",
    composition: "Iron core, silicate crust",
    texture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/mercurymap.jpg",
    color: "#a89078",
  },
  {
    id: "venus",
    name: "Venus",
    type: "Terrestrial planet",
    blurb:
      "Wrapped in a suffocating shroud of sulfuric clouds. Hottest world in the system — a runaway greenhouse with crushing pressure.",
    radius: 13,
    detailRadius: 300,
    orbitRadius: 160,
    period: 14,
    eccentricity: 0.007,
    distance: "0.72 AU",
    day: "243 Earth days",
    year: "225 Earth days",
    moons: 0,
    temp: "465 °C",
    gravity: "8.87 m/s²",
    composition: "CO₂ atmosphere, sulfuric acid clouds",
    texture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/venusmap.jpg",
    color: "#e0b878",
    glowColor: "#F2C77A",
  },
  {
    id: "earth",
    name: "Earth",
    type: "Terrestrial planet",
    blurb:
      "The pale blue dot. A water world with a thin breathable veil — the only known cradle of life in the universe.",
    radius: 14,
    detailRadius: 310,
    orbitRadius: 215,
    period: 22,
    eccentricity: 0.017,
    distance: "1.00 AU",
    day: "23h 56m",
    year: "365.25 days",
    moons: 1,
    temp: "-88 to 58 °C",
    gravity: "9.81 m/s²",
    composition: "78% N₂, 21% O₂, liquid water oceans",
    texture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/earthmap1k.jpg",
    cloudTexture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/earthcloudmap.jpg",
    color: "#5b8fc4",
    glowColor: "#7AB8FF",
  },
  {
    id: "mars",
    name: "Mars",
    type: "Terrestrial planet",
    blurb:
      "The rust-red desert world. Home to Olympus Mons (largest volcano in the system) and Valles Marineris (a 4,000 km canyon).",
    radius: 11,
    detailRadius: 290,
    orbitRadius: 280,
    period: 32,
    eccentricity: 0.093,
    distance: "1.52 AU",
    day: "24h 37m",
    year: "687 Earth days",
    moons: 2,
    temp: "-140 to 30 °C",
    gravity: "3.72 m/s²",
    composition: "Iron oxide crust, thin CO₂ atmosphere",
    texture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/marsmap1k.jpg",
    color: "#c46838",
    glowColor: "#D87050",
  },
  {
    id: "jupiter",
    name: "Jupiter",
    type: "Gas giant",
    blurb:
      "The king of planets. Its Great Red Spot is a storm two Earths wide that has raged for at least 350 years.",
    radius: 34,
    detailRadius: 360,
    orbitRadius: 360,
    period: 62,
    eccentricity: 0.049,
    distance: "5.20 AU",
    day: "9h 56m",
    year: "11.86 years",
    moons: 95,
    temp: "-145 °C cloud tops",
    gravity: "24.79 m/s²",
    composition: "H₂, He, ammonia & water ice clouds",
    texture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/jupitermap.jpg",
    color: "#c89870",
    glowColor: "#D8B080",
  },
  {
    id: "saturn",
    name: "Saturn",
    type: "Gas giant",
    blurb:
      "The jeweled planet. Its rings span 280,000 km but are only ten metres thick — cosmic vinyl of ice and dust shepherded by moons.",
    radius: 28,
    detailRadius: 300,
    orbitRadius: 440,
    period: 88,
    eccentricity: 0.057,
    distance: "9.58 AU",
    day: "10h 33m",
    year: "29.46 years",
    moons: 146,
    temp: "-178 °C",
    gravity: "10.44 m/s²",
    composition: "H₂, He, ammonia ice; ring system",
    texture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/saturnmap.jpg",
    ringTexture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/saturnringcolor.jpg",
    ringAlpha:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/saturnringpattern.gif",
    color: "#d4b878",
    glowColor: "#D8C088",
    rings: true,
    ringInner: 1.3,
    ringOuter: 2.3,
    tilt: 26.7,
  },
  {
    id: "uranus",
    name: "Uranus",
    type: "Ice giant",
    blurb:
      "The sideways planet. Tilted 98° on its axis, it rolls around the Sun like a wheel through pale cyan methane haze.",
    radius: 20,
    detailRadius: 290,
    orbitRadius: 510,
    period: 124,
    eccentricity: 0.046,
    distance: "19.2 AU",
    day: "17h 14m",
    year: "84 Earth years",
    moons: 27,
    temp: "-224 °C",
    gravity: "8.69 m/s²",
    composition: "Water, methane, ammonia ices",
    texture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/uranusmap.jpg",
    color: "#9ed4ce",
    glowColor: "#8ED4CC",
    tilt: 98,
    rings: "thin",
    ringInner: 1.4,
    ringOuter: 1.7,
  },
  {
    id: "neptune",
    name: "Neptune",
    type: "Ice giant",
    blurb:
      "The deep blue wanderer. Supersonic winds tear across its atmosphere at 2,100 km/h — the fastest in the solar system.",
    radius: 19,
    detailRadius: 290,
    orbitRadius: 580,
    period: 168,
    eccentricity: 0.01,
    distance: "30.1 AU",
    day: "16h 6m",
    year: "165 Earth years",
    moons: 14,
    temp: "-218 °C",
    gravity: "11.15 m/s²",
    composition: "Water, methane, ammonia ices",
    texture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/neptunemap.jpg",
    color: "#3a5fb8",
    glowColor: "#5A8AE0",
  },
];

window.PLANETS = PLANETS;

// ---------- Three.js planet renderer ----------
// Renders into a given container. Returns a dispose() function.
function planetTextureSeed(id) {
  const text = String(id || "world");
  let seed = 2166136261;
  for (let index = 0; index < text.length; index += 1) {
    seed ^= text.charCodeAt(index);
    seed = Math.imul(seed, 16777619);
  }
  return seed >>> 0;
}

function seededTextureRandom(seed) {
  let state = seed >>> 0;
  return () => {
    state = Math.imul(state ^ (state >>> 15), 2246822519);
    state = Math.imul(state ^ (state >>> 13), 3266489917);
    return ((state ^ (state >>> 16)) >>> 0) / 4294967296;
  };
}

function hexToRgb(hex) {
  const value = String(hex || "#888888").replace("#", "");
  const full =
    value.length === 3
      ? value
          .split("")
          .map((part) => part + part)
          .join("")
      : value.padEnd(6, "8").slice(0, 6);
  const number = Number.parseInt(full, 16);
  return {
    r: (number >> 16) & 255,
    g: (number >> 8) & 255,
    b: number & 255,
  };
}

function rgbString(color, alpha = 1) {
  return `rgba(${Math.round(color.r)}, ${Math.round(color.g)}, ${Math.round(color.b)}, ${alpha})`;
}

function mixRgb(a, b, amount) {
  const t = Math.max(0, Math.min(1, amount));
  return {
    r: a.r + (b.r - a.r) * t,
    g: a.g + (b.g - a.g) * t,
    b: a.b + (b.b - a.b) * t,
  };
}

function makeProceduralPlanetTexture(planet, detail = false) {
  const isStar = Boolean(planet && planet.isStar);
  const width = detail ? 1024 : 512;
  const height = detail ? 512 : 256;
  const canvas = document.createElement("canvas");
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext("2d");
  const base = hexToRgb((planet && planet.color) || "#8899bb");
  const rand = seededTextureRandom(planetTextureSeed((planet && planet.id) || "world"));
  const dark = mixRgb(base, { r: 0, g: 0, b: 0 }, isStar ? 0.36 : 0.42);
  const light = mixRgb(base, { r: 255, g: 245, b: 210 }, isStar ? 0.34 : 0.2);
  const cold = mixRgb(base, { r: 125, g: 180, b: 255 }, 0.42);
  const hot = mixRgb(base, { r: 255, g: 118, b: 48 }, 0.28);

  const fillGradient = ctx.createLinearGradient(0, 0, width, height);
  fillGradient.addColorStop(0, rgbString(isStar ? light : hot));
  fillGradient.addColorStop(0.48, rgbString(base));
  fillGradient.addColorStop(1, rgbString(isStar ? dark : cold));
  ctx.fillStyle = fillGradient;
  ctx.fillRect(0, 0, width, height);

  if (isStar) {
    const cells = detail ? 220 : 110;
    for (let index = 0; index < cells; index += 1) {
      const x = rand() * width;
      const y = rand() * height;
      const radius = (detail ? 16 : 9) + rand() * (detail ? 62 : 32);
      const color = rand() > 0.36 ? light : mixRgb(base, { r: 90, g: 20, b: 8 }, 0.45);
      const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
      gradient.addColorStop(0, rgbString(color, 0.42 + rand() * 0.34));
      gradient.addColorStop(0.72, rgbString(color, 0.08));
      gradient.addColorStop(1, "rgba(0,0,0,0)");
      ctx.fillStyle = gradient;
      ctx.beginPath();
      ctx.arc(x, y, radius, 0, Math.PI * 2);
      ctx.fill();
    }
    for (let band = 0; band < 18; band += 1) {
      const y = height * (0.08 + rand() * 0.84);
      const wave = height * (0.018 + rand() * 0.04);
      ctx.strokeStyle = rgbString(rand() > 0.45 ? light : dark, 0.18 + rand() * 0.22);
      ctx.lineWidth = (detail ? 4 : 2) + rand() * (detail ? 12 : 5);
      ctx.beginPath();
      ctx.moveTo(-width * 0.08, y);
      for (let x = -width * 0.08; x < width * 1.12; x += width / 7) {
        ctx.quadraticCurveTo(
          x + width / 14,
          y + Math.sin((x / width) * Math.PI * 2 + rand() * 3) * wave,
          x + width / 7,
          y + Math.cos((x / width) * Math.PI * 2 + rand() * 3) * wave,
        );
      }
      ctx.stroke();
    }
    for (let index = 0; index < 30; index += 1) {
      const x = rand() * width;
      const y = rand() * height;
      const radius = (detail ? 18 : 10) + rand() * (detail ? 52 : 24);
      const spot = mixRgb(base, { r: 42, g: 8, b: 5 }, 0.72);
      const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
      gradient.addColorStop(0, rgbString(spot, 0.76));
      gradient.addColorStop(0.58, rgbString(spot, 0.28));
      gradient.addColorStop(1, "rgba(0,0,0,0)");
      ctx.fillStyle = gradient;
      ctx.beginPath();
      ctx.arc(x, y, radius, 0, Math.PI * 2);
      ctx.fill();
    }
    for (let index = 0; index < (detail ? 95 : 42); index += 1) {
      const x = rand() * width;
      const y = rand() * height;
      ctx.fillStyle = rgbString(rand() > 0.5 ? light : dark, 0.28 + rand() * 0.2);
      ctx.beginPath();
      ctx.arc(x, y, 1 + rand() * (detail ? 5 : 3), 0, Math.PI * 2);
      ctx.fill();
    }
  } else {
    const isGasLike = /neptune|giant|gas|sub-neptune|mini-neptune/i.test(
      `${planet && planet.type} ${planet && planet.blurb}`,
    );
    if (isGasLike) {
      for (let y = -height * 0.1; y < height * 1.1; y += height / 10) {
        const bandHeight = height * (0.055 + rand() * 0.07);
        ctx.fillStyle = rgbString(rand() > 0.45 ? light : dark, 0.22 + rand() * 0.22);
        ctx.beginPath();
        ctx.ellipse(width / 2, y + rand() * 24, width * 0.58, bandHeight, rand() * 0.04, 0, Math.PI * 2);
        ctx.fill();
      }
    } else {
      const patchCount = detail ? 95 : 46;
      for (let index = 0; index < patchCount; index += 1) {
        const x = rand() * width;
        const y = rand() * height;
        const rx = (detail ? 18 : 9) + rand() * (detail ? 80 : 42);
        const ry = (detail ? 10 : 5) + rand() * (detail ? 42 : 22);
        const color = rand() > 0.5 ? light : dark;
        ctx.fillStyle = rgbString(color, 0.18 + rand() * 0.26);
        ctx.beginPath();
        ctx.ellipse(x, y, rx, ry, rand() * Math.PI, 0, Math.PI * 2);
        ctx.fill();
      }
      const craterCount = detail ? 44 : 18;
      for (let index = 0; index < craterCount; index += 1) {
        const x = rand() * width;
        const y = rand() * height;
        const radius = (detail ? 4 : 3) + rand() * (detail ? 18 : 9);
        ctx.strokeStyle = rgbString(mixRgb(base, { r: 255, g: 255, b: 255 }, 0.32), 0.26);
        ctx.lineWidth = Math.max(1, radius * 0.12);
        ctx.beginPath();
        ctx.arc(x, y, radius, 0, Math.PI * 2);
        ctx.stroke();
        ctx.fillStyle = rgbString(mixRgb(base, { r: 0, g: 0, b: 0 }, 0.36), 0.14);
        ctx.beginPath();
        ctx.arc(x + radius * 0.16, y + radius * 0.16, radius * 0.72, 0, Math.PI * 2);
        ctx.fill();
      }
      if (/habitable|possibly icy|outer edge|frozen|earth-like/i.test(`${planet && planet.type} ${planet && planet.blurb}`)) {
        for (let index = 0; index < 18; index += 1) {
          const x = rand() * width;
          const y = rand() * height;
          const rx = (detail ? 20 : 12) + rand() * (detail ? 70 : 34);
          const ry = (detail ? 6 : 4) + rand() * (detail ? 20 : 10);
          ctx.fillStyle = rgbString(mixRgb(base, { r: 190, g: 235, b: 255 }, 0.72), 0.16 + rand() * 0.16);
          ctx.beginPath();
          ctx.ellipse(x, y, rx, ry, rand() * Math.PI, 0, Math.PI * 2);
          ctx.fill();
        }
      }
    }
  }

  const vignette = ctx.createRadialGradient(
    width * 0.35,
    height * 0.35,
    0,
    width * 0.5,
    height * 0.5,
    width * 0.62,
  );
  vignette.addColorStop(0, "rgba(255,255,255,0.2)");
  vignette.addColorStop(0.55, "rgba(255,255,255,0)");
  vignette.addColorStop(1, "rgba(0,0,0,0.38)");
  ctx.fillStyle = vignette;
  ctx.fillRect(0, 0, width, height);

  return canvas;
}

function renderPlanet3D(container, planet, opts = {}) {
  const { detail = false, autoRotate = true, allowOrbit = false, size } = opts;

  const w = container.clientWidth;
  const h = container.clientHeight;

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(40, w / h, 0.1, 1000);
  // Pull the camera back far enough that the ring outer edge fits inside
  // the vertical FOV with a 12% margin. Without this Saturn's rings
  // (outer = 2.3 sphere-radii) clip at the default z = 3.2.
  const _ringOuter = planet.rings ? planet.ringOuter || 2.3 : 1;
  const _fovRad = (40 * Math.PI) / 180;
  const _requiredZ = (Math.max(1, _ringOuter) * 1.12) / Math.tan(_fovRad / 2);
  camera.position.set(0, 0, Math.max(3.2, _requiredZ));

  const renderer = new THREE.WebGLRenderer({
    antialias: true,
    alpha: true,
    preserveDrawingBuffer: true,
  });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.setSize(w, h);
  renderer.setClearColor(0x000000, 0);
  renderer.domElement.style.pointerEvents = allowOrbit ? "auto" : "none";
  container.appendChild(renderer.domElement);

  // Lights
  if (planet.isStar) {
    scene.add(new THREE.AmbientLight(0xffffff, 1.2));
  } else {
    scene.add(new THREE.AmbientLight(0xffffff, 0.18));
    const sunLight = new THREE.DirectionalLight(0xffffff, 1.6);
    sunLight.position.set(5, 2, 4);
    scene.add(sunLight);
    // soft fill from opposite side so dark side isn't pitch black in detail view
    if (detail) {
      const fill = new THREE.DirectionalLight(0x4060a0, 0.18);
      fill.position.set(-4, -1, -3);
      scene.add(fill);
    }
  }

  const loader = new THREE.TextureLoader();
  loader.crossOrigin = "anonymous";
  // Track every texture we successfully load so dispose() can free GPU memory
  // and so async loads that finish after teardown can drop their texture
  // instead of attaching it to a disposed material.
  const loadedTextures = [];
  let disposed = false;

  // Planet sphere
  const geo = new THREE.SphereGeometry(1, detail ? 96 : 48, detail ? 96 : 48);
  let mat;
  if (planet.isStar) {
    mat = new THREE.MeshBasicMaterial({ color: 0xffffff });
  } else {
    mat = new THREE.MeshPhongMaterial({
      color: 0xffffff,
      shininess: planet.id === "earth" ? 12 : 4,
      specular: planet.id === "earth" ? 0x223344 : 0x111111,
    });
  }
  const sphere = new THREE.Mesh(geo, mat);
  sphere.rotation.z = ((planet.tilt || 23.4) * Math.PI) / 180;
  scene.add(sphere);

  // Load texture, or paint a deterministic generated surface for worlds that
  // do not have a source image yet.
  mat.color.set(planet.color);
  const applyTexture = (tex, useBump = false) => {
    if (disposed) {
      tex.dispose();
      return;
    }
    tex.colorSpace = THREE.SRGBColorSpace;
    mat.map = tex;
    if (useBump && !planet.isStar) {
      mat.bumpMap = tex;
      mat.bumpScale = detail ? 0.045 : 0.026;
    }
    mat.color.set(planet.textureTint || 0xffffff);
    mat.needsUpdate = true;
    loadedTextures.push(tex);
  };

  if (planet.texture) {
    loader.load(
      planet.texture,
      (tex) => applyTexture(tex),
      undefined,
      () => {
        if (disposed) return;
        const generated = new THREE.CanvasTexture(makeProceduralPlanetTexture(planet, detail));
        applyTexture(generated, true);
      },
    );
  } else {
    const generated = new THREE.CanvasTexture(makeProceduralPlanetTexture(planet, detail));
    applyTexture(generated, true);
  }

  // Earth clouds
  let clouds = null;
  if (planet.cloudTexture && detail) {
    const cloudMat = new THREE.MeshPhongMaterial({
      transparent: true,
      opacity: 0.6,
      depthWrite: false,
    });
    clouds = new THREE.Mesh(new THREE.SphereGeometry(1.012, 64, 64), cloudMat);
    sphere.add(clouds);
    loader.load(planet.cloudTexture, (tex) => {
      if (disposed) {
        tex.dispose();
        return;
      }
      tex.colorSpace = THREE.SRGBColorSpace;
      cloudMat.map = tex;
      cloudMat.alphaMap = tex;
      cloudMat.needsUpdate = true;
      loadedTextures.push(tex);
    });
  }

  // Atmosphere glow (back-side fresnel-ish via shader)
  let atmosphere = null;
  if (planet.glowColor && detail && !planet.isStar) {
    const atmMat = new THREE.ShaderMaterial({
      uniforms: { glow: { value: new THREE.Color(planet.glowColor) } },
      vertexShader: `
        varying vec3 vNormal; varying vec3 vPos;
        void main() {
          vNormal = normalize(normalMatrix * normal);
          vec4 mv = modelViewMatrix * vec4(position, 1.0);
          vPos = -mv.xyz;
          gl_Position = projectionMatrix * mv;
        }
      `,
      fragmentShader: `
        uniform vec3 glow; varying vec3 vNormal; varying vec3 vPos;
        void main() {
          float intensity = pow(0.85 - dot(vNormal, normalize(vPos)), 3.0);
          gl_FragColor = vec4(glow, 1.0) * intensity;
        }
      `,
      side: THREE.BackSide,
      blending: THREE.AdditiveBlending,
      transparent: true,
      depthWrite: false,
    });
    atmosphere = new THREE.Mesh(new THREE.SphereGeometry(1.1, 48, 48), atmMat);
    scene.add(atmosphere);
  }

  // Sun corona/glow sprite. In detail view the sprite can reveal its square
  // texture bounds when the Sun fills the screen, so detail relies on the
  // sphere plus page-level radial light instead.
  let coronaSprite = null;
  let coronaTex = null;
  if (planet.isStar && !detail) {
    const canvas = document.createElement("canvas");
    canvas.width = 256;
    canvas.height = 256;
    const ctx = canvas.getContext("2d");
    const grad = ctx.createRadialGradient(128, 128, 0, 128, 128, 128);
    grad.addColorStop(0, "rgba(255,224,138,1)");
    grad.addColorStop(0.3, "rgba(255,147,38,0.7)");
    grad.addColorStop(0.62, "rgba(255,91,26,0.18)");
    grad.addColorStop(0.84, "rgba(80,22,0,0.035)");
    grad.addColorStop(1, "rgba(0,0,0,0)");
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, 256, 256);
    coronaTex = new THREE.CanvasTexture(canvas);
    const spriteMat = new THREE.SpriteMaterial({
      map: coronaTex,
      transparent: true,
      blending: THREE.AdditiveBlending,
      depthWrite: false,
    });
    coronaSprite = new THREE.Sprite(spriteMat);
    coronaSprite.scale.set(detail ? 4.2 : 2.6, detail ? 4.2 : 2.6, 1);
    scene.add(coronaSprite);
  }

  // Saturn / Uranus rings
  let ring = null;
  if (planet.rings) {
    const inner = planet.ringInner || 1.3;
    const outer = planet.ringOuter || 2.3;
    const ringGeo = new THREE.RingGeometry(inner, outer, 128, 1);
    // Re-map UVs so a horizontal texture maps radially
    const pos = ringGeo.attributes.position;
    const uv = ringGeo.attributes.uv;
    for (let i = 0; i < pos.count; i++) {
      const x = pos.getX(i),
        y = pos.getY(i);
      const r = Math.sqrt(x * x + y * y);
      const u = (r - inner) / (outer - inner);
      uv.setXY(i, u, 0.5);
    }
    let ringMat;
    if (planet.ringTexture) {
      ringMat = new THREE.MeshBasicMaterial({
        color: planet.glowColor || planet.color,
        transparent: true,
        side: THREE.DoubleSide,
        opacity: 0.95,
        depthWrite: false,
      });
      loader.load(planet.ringTexture, (tex) => {
        if (disposed) {
          tex.dispose();
          return;
        }
        tex.colorSpace = THREE.SRGBColorSpace;
        ringMat.map = tex;
        ringMat.color.set(0xffffff);
        ringMat.needsUpdate = true;
        loadedTextures.push(tex);
      });
      if (planet.ringAlpha) {
        loader.load(planet.ringAlpha, (tex) => {
          if (disposed) {
            tex.dispose();
            return;
          }
          ringMat.alphaMap = tex;
          ringMat.needsUpdate = true;
          loadedTextures.push(tex);
        });
      }
    } else {
      ringMat = new THREE.MeshBasicMaterial({
        color: planet.glowColor || planet.color,
        transparent: true,
        opacity: 0.5,
        side: THREE.DoubleSide,
        depthWrite: false,
      });
    }
    ring = new THREE.Mesh(ringGeo, ringMat);
    ring.rotation.x = Math.PI / 2;
    sphere.add(ring);
  }

  // Pointer interaction (drag to rotate in detail view)
  let dragState = {
    down: false,
    moved: false,
    x: 0,
    y: 0,
    rx: sphere.rotation.x,
    ry: sphere.rotation.y,
  };
  let userInteracted = false;
  if (allowOrbit) {
    const dom = renderer.domElement;
    dom.style.cursor = "grab";
    dom.addEventListener("pointerdown", (e) => {
      e.stopPropagation();
      dragState = {
        down: true,
        moved: false,
        x: e.clientX,
        y: e.clientY,
        rx: sphere.rotation.x,
        ry: sphere.rotation.y,
      };
      userInteracted = true;
      dom.style.cursor = "grabbing";
      dom.setPointerCapture(e.pointerId);
    });
    dom.addEventListener("pointermove", (e) => {
      if (!dragState.down) return;
      const dx = (e.clientX - dragState.x) / 200;
      const dy = (e.clientY - dragState.y) / 200;
      if (Math.hypot(e.clientX - dragState.x, e.clientY - dragState.y) > 4) {
        dragState.moved = true;
      }
      sphere.rotation.y = dragState.ry + dx;
      sphere.rotation.x = Math.max(-1.2, Math.min(1.2, dragState.rx + dy));
    });
    dom.addEventListener("pointerup", (e) => {
      e.stopPropagation();
      dragState.down = false;
      dom.style.cursor = "grab";
      try {
        dom.releasePointerCapture(e.pointerId);
      } catch {}
    });
    dom.addEventListener("pointercancel", (e) => {
      e.stopPropagation();
      dragState.down = false;
      dom.style.cursor = "grab";
    });
    dom.addEventListener("click", (e) => {
      if (!dragState.moved) return;
      e.preventDefault();
      e.stopPropagation();
      dragState.moved = false;
    });
    dom.addEventListener(
      "wheel",
      (e) => {
        e.preventDefault();
        // Don't let the user zoom in past the point that clips the rings.
        const minZ = Math.max(1.6, _requiredZ * 0.7);
        const maxZ = Math.max(6, _requiredZ * 1.8);
        camera.position.z = Math.max(
          minZ,
          Math.min(maxZ, camera.position.z + e.deltaY * 0.002),
        );
      },
      { passive: false },
    );
  }

  // Animation loop
  let raf;
  const speed = planet.isStar ? 0.0015 : 0.002;
  function tick() {
    if (autoRotate && !dragState.down) {
      sphere.rotation.y += speed;
    }
    if (clouds && !dragState.down) clouds.rotation.y += speed * 0.4;
    renderer.render(scene, camera);
    raf = requestAnimationFrame(tick);
  }
  tick();

  // Handle container resize
  const ro = new ResizeObserver(() => {
    const nw = container.clientWidth;
    const nh = container.clientHeight;
    if (nw && nh) {
      renderer.setSize(nw, nh);
      camera.aspect = nw / nh;
      camera.updateProjectionMatrix();
    }
  });
  ro.observe(container);

  return () => {
    disposed = true;
    cancelAnimationFrame(raf);
    ro.disconnect();
    // Walk the scene to free every geometry/material we created. Texture maps
    // are tracked separately because Three.js doesn't auto-dispose maps when a
    // material is disposed.
    scene.traverse((obj) => {
      if (obj.geometry && obj.geometry.dispose) obj.geometry.dispose();
      const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
      mats.forEach((m) => {
        if (!m) return;
        if (m.map && m.map.dispose) m.map.dispose();
        if (m.alphaMap && m.alphaMap.dispose) m.alphaMap.dispose();
        if (m.dispose) m.dispose();
      });
    });
    loadedTextures.forEach((tex) => tex && tex.dispose && tex.dispose());
    if (coronaTex && coronaTex.dispose) coronaTex.dispose();
    renderer.dispose();
    if (renderer.domElement.parentNode)
      renderer.domElement.parentNode.removeChild(renderer.domElement);
  };
}

window.renderPlanet3D = renderPlanet3D;
