function storedNumber(key, fallback, min, max) {
  try {
    const raw = localStorage.getItem(key);
    const value = raw === null ? fallback : Number(raw);
    if (!Number.isFinite(value)) return fallback;
    return Math.max(min, Math.min(max, value));
  } catch {
    return fallback;
  }
}


// ---------- Detail/zoom view ----------
function PlanetDetail({
  planet,
  favoritePlanetId,
  onClose,
  onPrev,
  onNext,
  onAstronautWave,
  onFavoriteToggle,
}) {
  const [vp, setVp] = useState({ w: window.innerWidth, h: window.innerHeight });
  const [showLifecycle, setShowLifecycle] = useState(false);
  const [planetBoop, setPlanetBoop] = useState(0);
  const [earthWeight, setEarthWeight] = useState(() =>
    storedNumber("planets_earth_weight_lb", 60, 20, 300),
  );
  const [earthAge, setEarthAge] = useState(() =>
    storedNumber("planets_earth_age_years", 8, 1, 120),
  );
  const [linkReady, setLinkReady] = useState(false);
  useEffect(() => {
    const onR = () => setVp({ w: window.innerWidth, h: window.innerHeight });
    window.addEventListener("resize", onR);
    return () => window.removeEventListener("resize", onR);
  }, []);
  // Pause planet narration while the lifecycle flipbook is open so the two
  // audio sources don't fight each other.
  useEffect(() => {
    if (showLifecycle && window.__narration) window.__narration.stop();
  }, [showLifecycle]);
  useEffect(() => {
    if (!planetBoop) return;
    const t = setTimeout(() => setPlanetBoop(0), 460);
    return () => clearTimeout(t);
  }, [planetBoop]);
  // Keep planet explainers quiet until the child asks to hear them.
  useEffect(() => {
    return () => {
      if (window.__narration) window.__narration.stop();
    };
  }, [planet.id]);
  useEffect(() => {
    if (!linkReady) return undefined;
    const timer = window.setTimeout(() => setLinkReady(false), 1800);
    return () => window.clearTimeout(timer);
  }, [linkReady]);
  useEffect(() => {
    try {
      localStorage.setItem("planets_earth_weight_lb", String(earthWeight));
    } catch {}
  }, [earthWeight]);
  useEffect(() => {
    try {
      localStorage.setItem("planets_earth_age_years", String(earthAge));
    } catch {}
  }, [earthAge]);
  const infoReserve = vp.w > 900 ? 460 : 0;
  const maxByW = (vp.w - infoReserve - 80) * 0.55;
  const maxByH = vp.h * 0.42;
  const radius = Math.max(120, Math.min(planet.detailRadius, maxByW, maxByH));
  const gravityRatio = planetGravityRatio(planet);
  const planetWeight =
    gravityRatio === null ? null : Math.max(0, Math.round(earthWeight * gravityRatio));
  const jumpHeight =
    gravityRatio === null ? null : Math.max(0.1, 1 / gravityRatio).toFixed(1);
  const yearDays = planetYearInEarthDays(planet.year);
  const worldAge =
    yearDays === null ? null : Math.max(0, (earthAge * 365.25) / yearDays);
  const isFavorite = favoritePlanetId === planet.id;

  return (
    <div
      className={`detail-view ${planet.isStar ? "sun-detail-view" : ""}`}
      data-narrow={vp.w <= 900 ? "1" : "0"}
    >
      <div
        className="detail-bg"
        style={{
          background: planet.isStar
            ? "transparent"
            : `radial-gradient(circle at 30% 50%, ${planet.glowColor || planet.color}22 0%, transparent 60%)`,
        }}
      />
      <div
        className={`detail-planet-wrap ${planet.isStar ? "sun-wrap" : ""} ${planetBoop ? "boop" : ""}`}
        style={{ width: radius * 2.4, height: radius * 2.4 }}
        onClick={() => {
          setPlanetBoop((n) => n + 1);
          playKidSound("boop");
        }}
      >
        <Planet3D
          planet={planet}
          detail={true}
          allowOrbit={true}
          className="planet-canvas-detail"
        />
        {planet.isStar ? (
          <button
            className="lifecycle-launch"
            onClick={() => setShowLifecycle(true)}
            title="See how the Sun ages"
          >
            ☀ Sun's life
          </button>
        ) : null}
      </div>

      <div className="detail-info">
        <div className="detail-eyebrow">{planet.type.toUpperCase()}</div>
        <h1 className="detail-name">{planet.name}</h1>
        {isFavorite ? (
          <div className="detail-favorite-badge">★ Favorite world</div>
        ) : null}
        {KID_I_SPY[planet.id] ? (
          <div className="kid-i-spy">{KID_I_SPY[planet.id]}</div>
        ) : null}
        <p className="detail-blurb">{planet.blurb}</p>
        <div className="stat-grid">
          <Stat label="Distance from Sun" value={planet.distance} />
          <Stat label="Day length" value={planet.day} />
          <Stat label="Year length" value={planet.year} />
          <Stat label="Moons" value={planet.moons.toString()} />
          <Stat label="Surface temp" value={planet.temp} />
          <Stat label="Gravity" value={planet.gravity} />
          <Stat label="Sunlight trip" value={sunlightTravelTime(planet.distance)} />
        </div>
        <div className="detail-composition">
          <div className="comp-label">COMPOSITION</div>
          <div className="comp-value">{planet.composition}</div>
        </div>
        <div className="gravity-play-card" style={{ "--planet-color": planet.color }}>
          <div className="gravity-play-copy">
            <div className="gravity-play-label">GRAVITY FEEL</div>
            <strong>
              {planetWeight === null
                ? "Try another world"
                : `${earthWeight} lb on Earth feels like ${planetWeight} lb on ${planet.name}.`}
            </strong>
            <span>{gravityFeelingCopy(gravityRatio)}</span>
          </div>
          <label className="gravity-weight-control">
            <span>Earth weight</span>
            <input
              type="number"
              min="20"
              max="300"
              step="5"
              value={earthWeight}
              onChange={(event) => {
                const next = Number(event.target.value);
                if (!Number.isFinite(next)) return;
                setEarthWeight(Math.max(20, Math.min(300, next)));
              }}
            />
            <small>lb</small>
          </label>
          {jumpHeight ? (
            <div className="gravity-jump-meter" aria-label={`Jump feels ${jumpHeight} times Earth height`}>
              <span style={{ height: `${Math.min(100, 18 + jumpHeight * 16)}%` }} />
              <b>{jumpHeight}x jump</b>
            </div>
          ) : null}
        </div>
        <div className="age-play-card">
          <div className="age-play-copy">
            <div className="gravity-play-label">PLANET AGE</div>
            <strong>
              {worldAge === null
                ? `${planet.name} does not have a planet birthday.`
                : `${earthAge} Earth years is ${worldAge.toFixed(worldAge < 10 ? 1 : 0)} ${planet.name} years.`}
            </strong>
          </div>
          <label className="gravity-weight-control age-control">
            <span>Earth age</span>
            <input
              type="number"
              min="1"
              max="120"
              step="1"
              value={earthAge}
              onChange={(event) => {
                const next = Number(event.target.value);
                if (!Number.isFinite(next)) return;
                setEarthAge(Math.max(1, Math.min(120, next)));
              }}
            />
            <small>yr</small>
          </label>
        </div>
        <div className="detail-actions">
          <button
            className="btn-tour"
            aria-label={`Play ${planet.name} narration`}
            onClick={() =>
              window.__narration && window.__narration.play(planet.id + ".mp3")
            }
          >
            🔊 Play narration
          </button>
          <button
            className="btn-tour"
            aria-label={`Say ${planet.name}`}
            onClick={() => speakWorldName(planet.name)}
          >
            Say name
          </button>
          <button
            className={`btn-tour ${linkReady ? "on" : ""}`}
            aria-label={linkReady ? `${planet.name} link ready` : `Copy ${planet.name} link`}
            onClick={() => {
              copyPlanetLink(planet);
              setLinkReady(true);
            }}
          >
            {linkReady ? "Link ready" : "Copy link"}
          </button>
          <button
            className="btn-tour"
            aria-label="Make the astronaut wave"
            onClick={() => {
              playKidSound("giggle");
              onAstronautWave();
            }}
          >
            👋 Astronaut wave
          </button>
          <button
            className={`btn-tour ${isFavorite ? "on" : ""}`}
            aria-pressed={isFavorite ? "true" : "false"}
            aria-label={
              isFavorite
                ? `Remove ${planet.name} as favorite`
                : `Set ${planet.name} as favorite`
            }
            onClick={() => {
              onFavoriteToggle(isFavorite ? "" : planet.id);
              playKidSound("chime");
            }}
          >
            {isFavorite ? "★ Favorite set" : "☆ Set favorite"}
          </button>
        </div>
        <div className="detail-hint">
          tap to boop · drag to rotate · scroll to zoom
        </div>
      </div>

      <button className="detail-close" onClick={onClose}>
        <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
          <path
            d="M11 3L3 11M3 3l8 8"
            stroke="currentColor"
            strokeWidth="1.6"
            strokeLinecap="round"
          />
        </svg>
        <span>Back to system</span>
      </button>
      <button className="detail-nav prev" onClick={onPrev}>
        <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
          <path
            d="M12 4L6 10l6 6"
            stroke="currentColor"
            strokeWidth="1.6"
            strokeLinecap="round"
            strokeLinejoin="round"
          />
        </svg>
      </button>
      <button className="detail-nav next" onClick={onNext}>
        <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
          <path
            d="M8 4l6 6-6 6"
            stroke="currentColor"
            strokeWidth="1.6"
            strokeLinecap="round"
            strokeLinejoin="round"
          />
        </svg>
      </button>
      {showLifecycle ? (
        <SunLifecycleFlipbook onClose={() => setShowLifecycle(false)} />
      ) : null}
    </div>
  );
}

function Stat({ label, value }) {
  return (
    <div className="stat">
      <div className="stat-label">{label}</div>
      <div className="stat-value">{value}</div>
    </div>
  );
}

// ---------- App ----------
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/ {
  tilt: 18,
  orbitOpacity: 0.7,
  showLabels: true,
  autoOrbit: true,
  starDensity: 400,
}; /*EDITMODE-END*/

function detailCenterFor() {
  const narrow = window.innerWidth <= 900;
  return {
    x: window.innerWidth * (narrow ? 0.5 : 0.32),
    y: window.innerHeight * (narrow ? 0.38 : 0.5),
    radius: Math.min(window.innerWidth, window.innerHeight) * 0.28,
  };
}
