/* global React, ReactDOM, Globe */
const { useState, useEffect, useMemo, useRef, useCallback } = React;

const TWEAK_DEFAULTS = {
  accentColor: '#7ec8ff',
  globeStyle: 'hex',
  atmosphere: true,
  starfield: true,
  autoRotate: false,
};

const ACCENT_PRESETS = [
  { name: 'cyan',    color: '#7ec8ff' },
  { name: 'amber',   color: '#ffc978' },
  { name: 'magenta', color: '#f08cc5' },
  { name: 'lime',    color: '#9de08a' },
  { name: 'violet',  color: '#b99cf0' }
];

function formatCoord(value, pos, neg) {
  const abs = Math.abs(value);
  const d = Math.floor(abs);
  const m = Math.floor((abs - d) * 60);
  const s = ((abs - d) * 60 - m) * 60;
  return `${d}°${m.toString().padStart(2,'0')}′${s.toFixed(0).padStart(2,'0')}″${value >= 0 ? pos : neg}`;
}

function App() {
  const [languages, setLanguages] = useState(null);
  const [countries, setCountries] = useState(null);
  const [selectedIdx, setSelectedIdx] = useState(null);
  // when the user clicks a secondary pin (a country sharing the language but
  // not the language's origin), this holds { lat, lng, name } so the camera,
  // ring, and panel header all reflect the clicked country instead of the
  // language's origin country. null = use the language's origin.
  const [selectedLocation, setSelectedLocation] = useState(null);
  const [rollIdx, setRollIdx] = useState(0);
  const [discovered, setDiscovered] = useState(() => {
    try { return new Set(JSON.parse(localStorage.getItem('glob.discovered') || '[]')); }
    catch { return new Set(); }
  });
  const [searchOpen, setSearchOpen] = useState(false);
  const [searchText, setSearchText] = useState('');
  const [showTweaks, setShowTweaks] = useState(false);
  const [tweaks, setTweaks] = useState(() => {
    try {
      const saved = JSON.parse(localStorage.getItem('glob.tweaks') || 'null');
      const merged = { ...TWEAK_DEFAULTS, ...(saved || {}) };
      // migrate old oklch colors to hex presets
      if (typeof merged.accentColor !== 'string' || !merged.accentColor.startsWith('#')) {
        merged.accentColor = TWEAK_DEFAULTS.accentColor;
      }
      return merged;
    } catch { return TWEAK_DEFAULTS; }
  });

  const globeRef = useRef(null);
  const globeInstRef = useRef(null);
  const containerRef = useRef(null);
  const tweaksRef = useRef(null);
  const hoveredFeatRef = useRef(null);
  const selectedCountryNamesRef = useRef(new Set());
  const [hoveredCountry, setHoveredCountry] = useState(null);
  const [hoverScreenPos, setHoverScreenPos] = useState(null);
  const mousePosRef = useRef({ x: 0, y: 0 });

  // Re-applies hex polygon coloring based on the current refs (hover + selection
  // + style). Called from every event that changes any of those.
  const applyHexColor = useCallback((world) => {
    if (!world) return;
    const accent = tweaksRef.current?.accentColor || '#ffd388';
    const styleHex = (tweaksRef.current?.globeStyle || 'hex') === 'hex';
    const baseColor = styleHex
      ? 'rgba(170, 200, 255, 0.75)'
      : 'rgba(120, 160, 220, 0.35)';
    const selSet = selectedCountryNamesRef.current;
    const hoveredFeat = hoveredFeatRef.current;
    world.hexPolygonColor(f => {
      if (f === hoveredFeat) return accent;
      const name = getCountryName(f);
      if (selSet.has(name)) return accent;
      return baseColor;
    });
  }, []);

  // keep tweaks in a ref for access inside globe init closure
  useEffect(() => { tweaksRef.current = tweaks; }, [tweaks]);

  // track mouse for hover button placement
  useEffect(() => {
    const onMove = (e) => {
      mousePosRef.current = { x: e.clientX, y: e.clientY };
      // update button position live if hovering
      setHoverScreenPos(prev => prev ? { x: e.clientX, y: e.clientY } : prev);
    };
    window.addEventListener('pointermove', onMove);
    return () => window.removeEventListener('pointermove', onMove);
  }, []);

  // Country containing each language's primary pin (point-in-polygon).
  // Computed once per data load and reused by both the hover-lookup map and
  // the secondary-pin placement effect.
  const primaryCountryByLang = useMemo(() => {
    if (!languages || !countries) return {};
    const out = {};
    for (let i = 0; i < languages.length; i++) {
      const { lat, lng } = languages[i];
      for (const feat of countries.features) {
        if (pointInFeature([lng, lat], feat)) {
          out[i] = getCountryName(feat);
          break;
        }
      }
    }
    return out;
  }, [languages, countries]);

  // map country name -> language index
  // Uses two sources:
  //  1. point-in-polygon of the primary pin lat/lng
  //  2. explicit multi-country mapping in window.LANGUAGE_COUNTRIES
  //     (countries sharing the same language e.g. English, Spanish)
  const countryToLangIdx = useMemo(() => {
    if (!languages) return {};
    const map = {};
    for (let i = 0; i < languages.length; i++) {
      const name = primaryCountryByLang[i];
      if (name && !(name in map)) map[name] = i;
    }
    const langMap = window.LANGUAGE_COUNTRIES || {};
    for (let i = 0; i < languages.length; i++) {
      const extras = langMap[languages[i].language];
      if (!extras) continue;
      for (const cname of extras) {
        if (!(cname in map)) map[cname] = i;
      }
    }
    return map;
  }, [languages, primaryCountryByLang]);

  // compute country centroids (bbox midpoint — fast, good enough for pin placement)
  const countryCentroids = useMemo(() => {
    if (!countries) return {};
    const out = {};
    for (const feat of countries.features) {
      const name = getCountryName(feat);
      const bbox = computeBBox(feat);
      if (bbox) out[name] = { lat: (bbox.minLat + bbox.maxLat) / 2, lng: (bbox.minLng + bbox.maxLng) / 2 };
    }
    return out;
  }, [countries]);

  const setTweakValue = (k, v) => {
    setTweaks(prev => {
      const next = { ...prev, [k]: v };
      try { localStorage.setItem('glob.tweaks', JSON.stringify(next)); } catch {}
      return next;
    });
  };

  // load both data files
  useEffect(() => {
    Promise.all([
      fetch('languages.json').then(r => r.json()),
      fetch('countries.geojson').then(r => r.json())
    ]).then(([langs, geo]) => {
      setLanguages(langs);
      setCountries(geo);
    }).catch(err => console.error('data load failed', err));
  }, []);

  // initialize globe.gl (once)
  useEffect(() => {
    if (!languages || !countries || globeInstRef.current) return;
    const el = document.getElementById('globeViz');
    if (!el) return;

    // globe.gl exposes a factory function — call it (not new). It supports both.
    const GlobeFactory = window.Globe;
    const world = GlobeFactory()(el)
      .backgroundColor('rgba(0,0,0,0)')
      .showAtmosphere(tweaks.atmosphere)
      .atmosphereColor(tweaks.accentColor)
      .atmosphereAltitude(0.22)
      .showGlobe(true)
      .globeImageUrl(null)
      .hexPolygonsData(countries.features)
      .hexPolygonResolution(3)
      .hexPolygonMargin(0.12)
      .hexPolygonUseDots(true)
      .hexPolygonAltitude(0.005)
      .hexPolygonColor(() => 'rgba(170, 200, 255, 0.75)')
      .onHexPolygonHover(feat => {
        if (window.__onCountryHover) window.__onCountryHover(feat);
        hoveredFeatRef.current = feat;
        applyHexColor(world);
      })
      .pointsData([])
      .pointAltitude(0.01)
      .pointRadius(0.35)
      .pointColor(() => '#ffd388')
      .onPointClick((p) => handlePick(p.__idx, p.isPrimary ? null : { lat: p.lat, lng: p.lng, name: p.__country }))
      .pointsMerge(false);

    // solid globe material — set color via .set() which accepts hex strings
    const globeMat = world.globeMaterial();
    if (globeMat) {
      if (globeMat.color && globeMat.color.set) globeMat.color.set('#0a1130');
      if (globeMat.emissive && globeMat.emissive.set) globeMat.emissive.set('#030615');
      if ('emissiveIntensity' in globeMat) globeMat.emissiveIntensity = 0.7;
      if ('shininess' in globeMat) globeMat.shininess = 0.2;
    }

    // controls — globe.gl creates them after a tick; defer safely
    let controls = null;
    let idleTimer;
    const onInteract = () => {
      // immediately suppress hover button while dragging
      if (window.__onCountryHover) window.__onCountryHover(null);
      window.__isDraggingGlobe = true;
    };
    const onInteractEnd = () => {
      // short debounce so the release doesn't instantly refire hover
      setTimeout(() => { window.__isDraggingGlobe = false; }, 180);
    };
    const setupControls = () => {
      try {
        const c = typeof world.controls === 'function' ? world.controls() : null;
        if (!c) { setTimeout(setupControls, 80); return; }
        controls = c;
        try { c.autoRotate = !!tweaksRef.current?.autoRotate; } catch {}
        try { c.autoRotateSpeed = 0.6; } catch {}
        try { c.enableDamping = true; } catch {}
        try { c.dampingFactor = 0.08; } catch {}
        try { c.minDistance = 180; c.maxDistance = 600; } catch {}
        try { c.addEventListener && c.addEventListener('start', onInteract); } catch {}
        try { c.addEventListener && c.addEventListener('end', onInteractEnd); } catch {}
      } catch (e) { console.warn('controls setup skipped:', e.message); }
    };
    setTimeout(setupControls, 100);

    // nice starting camera
    world.pointOfView({ lat: 25, lng: 10, altitude: 2.4 }, 0);

    // resize
    const ro = new ResizeObserver(() => {
      world.width(el.clientWidth);
      world.height(el.clientHeight);
    });
    ro.observe(el);
    world.width(el.clientWidth).height(el.clientHeight);

    globeInstRef.current = world;

    // add ambient + directional lights so pins don't render pitch-black
    // on the shadow side of the globe
    try {
      const THREE = window.THREE;
      if (THREE) {
        const scene = world.scene();
        // remove any existing default lights to avoid double-lighting
        const toRemove = [];
        scene.traverse(obj => {
          if (obj.isLight) toRemove.push(obj);
        });
        toRemove.forEach(l => scene.remove(l));
        scene.add(new THREE.AmbientLight(0xffffff, 0.9));
        const dir = new THREE.DirectionalLight(0xffffff, 0.6);
        dir.position.set(1, 1, 1);
        scene.add(dir);
      }
    } catch (e) { console.warn('lights setup skipped', e.message); }

    // wire hover callback to React (dedupe by country name — tolerates
    // repeated events for the same country, and a short grace period on
    // leave so hex-dot gaps don't flicker the popup)
    let lastHoverName = null;
    let leaveTimer = null;
    window.__onCountryHover = (feat) => {
      // suppress during drag
      if (window.__isDraggingGlobe && feat) return;

      if (!feat) {
        // schedule clear, don't do it immediately
        if (leaveTimer) return;
        leaveTimer = setTimeout(() => {
          leaveTimer = null;
          lastHoverName = null;
          setHoveredCountry(null);
          setHoverScreenPos(null);
        }, 140);
        return;
      }

      // entering (or re-entering) a country — cancel any pending leave
      if (leaveTimer) { clearTimeout(leaveTimer); leaveTimer = null; }

      const name = getCountryName(feat);
      if (name === lastHoverName) return; // same country, no state churn
      lastHoverName = name;

      const langIdx = countryToLangIdx[name];
      // show popup for EVERY country; langIdx may be null (no pin → label only)
      setHoveredCountry({ name, langIdx });
      setHoverScreenPos({ x: mousePosRef.current.x, y: mousePosRef.current.y });
    };

    return () => {
      ro.disconnect();
      if (controls) {
        try { controls.removeEventListener('start', onInteract); } catch {}
        try { controls.removeEventListener('end', onInteractEnd); } catch {}
      }
      window.__onCountryHover = null;
    };
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [languages, countries, countryToLangIdx]);

  // update pins when languages / selected changes
  useEffect(() => {
    const w = globeInstRef.current;
    if (!w || !languages) return;

    // A pin is "selected" iff it matches the currently-selected language AND
    // (for primary pins: no secondary override is active; for secondary pins:
    // the override's country name matches this pin's country).
    const isPinSelected = (idx, isPrimary, cname) => {
      if (idx !== selectedIdx) return false;
      if (selectedLocation == null) return isPrimary;
      return !isPrimary && cname === selectedLocation.name;
    };

    // Primary pins (from languages.json)
    const pts = languages.map((l, idx) => ({
      ...l,
      __idx: idx,
      __country: l.location,
      lat: l.lat,
      lng: l.lng,
      isSelected: isPinSelected(idx, true, l.location),
      isPrimary: true,
    }));

    // Secondary pins — for every country in LANGUAGE_COUNTRIES, drop a pin
    // at the country's centroid linking back to the same language index.
    // Skip the country that already contains the primary pin so we don't
    // double-stack.
    const langMap = window.LANGUAGE_COUNTRIES || {};

    for (let i = 0; i < languages.length; i++) {
      const langName = languages[i].language;
      const extras = langMap[langName];
      if (!extras) continue;
      const primaryCountry = primaryCountryByLang[i];
      for (const cname of extras) {
        if (cname === primaryCountry) continue;
        const centroid = countryCentroids[cname];
        if (!centroid) continue;
        pts.push({
          ...languages[i],
          __idx: i,
          __country: cname,
          lat: centroid.lat,
          lng: centroid.lng,
          isSelected: isPinSelected(i, false, cname),
          isPrimary: false,
        });
      }
    }

    w.pointsData(pts)
      .pointColor(p => p.isSelected ? tweaks.accentColor : (p.isPrimary ? '#ffd388' : '#e6b469'))
      .pointRadius(p => p.isSelected ? 0.9 : (p.isPrimary ? 0.55 : 0.42))
      .pointAltitude(p => p.isSelected ? 0.06 : (p.isPrimary ? 0.02 : 0.015));

    // selected pin also gets a ring marker — anchored at the *clicked* spot
    // (origin or secondary country), not always the language origin
    const ringTarget = selectedIdx != null
      ? (selectedLocation || languages[selectedIdx])
      : null;
    w.ringsData(
      ringTarget
        ? [{ lat: ringTarget.lat, lng: ringTarget.lng, color: tweaks.accentColor }]
        : []
    )
      .ringColor(d => t => {
        // t goes 0→1; fade out with distance
        const alpha = 1 - t;
        // convert hex to rgba
        const c = d.color;
        return hexToRgba(c, alpha);
      })
      .ringMaxRadius(4)
      .ringPropagationSpeed(2.4)
      .ringRepeatPeriod(900)
      .ringAltitude(0.012);
  }, [languages, countries, countryCentroids, primaryCountryByLang, selectedIdx, selectedLocation, tweaks.accentColor]);

  // react to tweak changes
  useEffect(() => {
    const w = globeInstRef.current;
    if (!w) return;
    try { w.showAtmosphere(tweaks.atmosphere).atmosphereColor(tweaks.accentColor); } catch {}
    try {
      const c = typeof w.controls === 'function' ? w.controls() : null;
      if (c) c.autoRotate = !!tweaks.autoRotate;
    } catch {}
    try {
      w.hexPolygonUseDots(tweaks.globeStyle === 'hex');
      w.hexPolygonMargin(tweaks.globeStyle === 'hex' ? 0.28 : 0.05);
      applyHexColor(w);
    } catch {}
  }, [tweaks.atmosphere, tweaks.accentColor, tweaks.globeStyle, tweaks.autoRotate, applyHexColor]);

  // selection → highlight the language's origin country AND the clicked country
  useEffect(() => {
    const w = globeInstRef.current;
    const set = new Set();
    if (selectedIdx != null) {
      const origin = primaryCountryByLang[selectedIdx];
      if (origin) set.add(origin);
      if (selectedLocation?.name) set.add(selectedLocation.name);
    }
    selectedCountryNamesRef.current = set;
    applyHexColor(w);
  }, [selectedIdx, selectedLocation, primaryCountryByLang, applyHexColor]);

  const selectedLang = languages && selectedIdx != null ? languages[selectedIdx] : null;
  const panelOpen = selectedIdx != null;

  // toggle globe shift class
  useEffect(() => {
    const el = document.getElementById('globeViz');
    if (!el) return;
    if (panelOpen) el.classList.add('panel-open');
    else el.classList.remove('panel-open');
  }, [panelOpen]);

  const { phrase, fact } = useMemo(() => {
    if (!selectedLang) return { phrase: null, fact: null };
    const pIdx = Math.floor(Math.random() * selectedLang.phrases.length);
    const fIdx = Math.floor(Math.random() * selectedLang.facts.length);
    return { phrase: selectedLang.phrases[pIdx], fact: selectedLang.facts[fIdx] };
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [selectedIdx, rollIdx]);

  const handlePick = useCallback((idx, locationOverride = null) => {
    setSelectedIdx(idx);
    setSelectedLocation(locationOverride);
    setRollIdx(r => r + 1);
    setDiscovered(prev => {
      if (prev.has(idx)) return prev;
      const next = new Set(prev); next.add(idx);
      try { localStorage.setItem('glob.discovered', JSON.stringify([...next])); } catch {}
      return next;
    });
    // fly to — offset longitude eastward so the panel (which covers the right
    // third) doesn't cover the selected pin
    const w = globeInstRef.current;
    if (w && languages) {
      const target = locationOverride || languages[idx];
      const panelW = Math.min(440, window.innerWidth * 0.92);
      const shiftDeg = (panelW / window.innerWidth) * 55;
      w.pointOfView({ lat: target.lat, lng: target.lng + shiftDeg, altitude: 1.9 }, 1000);
    }
  }, [languages]);

  const handleShuffle = () => {
    if (!languages) return;
    let idx = Math.floor(Math.random() * languages.length);
    if (idx === selectedIdx) idx = (idx + 1) % languages.length;
    handlePick(idx);
  };

  const handleReroll = () => setRollIdx(r => r + 1);

  const searchResults = useMemo(() => {
    if (!languages || !searchText.trim()) return [];
    const q = searchText.trim().toLowerCase();
    return languages
      .map((l, idx) => ({ l, idx }))
      .filter(({ l }) =>
        l.language.toLowerCase().includes(q) ||
        l.location.toLowerCase().includes(q))
      .slice(0, 8);
  }, [languages, searchText]);

  const loading = !languages || !countries;

  return (
    <>
      {tweaks.starfield && <Starfield />}

      {/* HOVER SUGGESTION / LABEL */}
      {hoveredCountry && hoverScreenPos && !panelOpen && (() => {
        const hasLang = hoveredCountry.langIdx != null;
        const onClickHover = () => {
          const idx = hoveredCountry.langIdx;
          const cname = hoveredCountry.name;
          // if the hovered country is *not* the language's origin, fly to the
          // hovered country's centroid instead of the origin
          const isOrigin = primaryCountryByLang[idx] === cname;
          const centroid = countryCentroids[cname];
          handlePick(idx, !isOrigin && centroid ? { ...centroid, name: cname } : null);
        };
        return (
          <div
            onClick={hasLang ? onClickHover : undefined}
            style={{
              position: 'fixed',
              left: hoverScreenPos.x + 18,
              top: hoverScreenPos.y - 14,
              zIndex: 20,
              pointerEvents: hasLang ? 'auto' : 'none',
              cursor: hasLang ? 'pointer' : 'default',
              padding: hasLang ? '8px 14px' : '6px 12px',
              background: hasLang ? 'rgba(10, 14, 36, 0.92)' : 'rgba(10, 14, 36, 0.78)',
              border: hasLang
                ? `1px solid ${tweaks.accentColor}`
                : '1px solid rgba(170, 200, 255, 0.22)',
              borderRadius: 999,
              color: hasLang ? tweaks.accentColor : 'rgba(200, 214, 255, 0.82)',
              fontFamily: "'JetBrains Mono', monospace",
              fontSize: hasLang ? 11 : 10,
              letterSpacing: '0.18em',
              textTransform: 'uppercase',
              boxShadow: hasLang
                ? `0 8px 24px rgba(0,0,0,0.4), 0 0 0 4px ${hexToRgba(tweaks.accentColor, 0.08)}`
                : '0 4px 14px rgba(0,0,0,0.35)',
              whiteSpace: 'nowrap',
              userSelect: 'none',
              animation: 'hoverPop 160ms ease-out',
            }}
          >
            {hasLang ? (
              <>
                <span style={{ opacity: 0.7, marginRight: 8 }}>▸</span>
                Discover {hoveredCountry.name}
              </>
            ) : (
              hoveredCountry.name
            )}
          </div>
        );
      })()}

      {loading && (
        <div style={{
          position: 'fixed', inset: 0,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          color: 'var(--ink-2)', zIndex: 50,
          fontFamily: 'JetBrains Mono, monospace', fontSize: 12, letterSpacing: '0.2em',
          pointerEvents: 'none'
        }}>
          ◉ LOADING TRANSMISSIONS…
        </div>
      )}

      {/* TOP BAR */}
      <header style={{
        position: 'fixed', top: 0, left: 0, right: 0, zIndex: 15,
        padding: '22px 30px',
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        pointerEvents: 'none'
      }}>
        <div style={{ pointerEvents: 'auto' }}>
          <div style={{
            fontFamily: 'Space Grotesk, sans-serif', fontWeight: 600,
            fontSize: 22, letterSpacing: '-0.02em', color: 'var(--ink-0)',
            display: 'flex', alignItems: 'baseline', gap: 8
          }}>
            <span style={{
              display: 'inline-block', width: 8, height: 8, borderRadius: '50%',
              background: tweaks.accentColor, boxShadow: `0 0 10px ${tweaks.accentColor}`,
              transform: 'translateY(-2px)'
            }}/>
            glob
          </div>
          <div style={{
            fontFamily: 'JetBrains Mono, monospace',
            fontSize: 10, letterSpacing: '0.18em',
            color: 'var(--ink-3)', marginTop: 2, textTransform: 'uppercase'
          }}>
            a language globe
          </div>
        </div>

        <div style={{ pointerEvents: 'auto', display: 'flex', alignItems: 'center', gap: 12 }}>
          <div style={{ position: 'relative' }}>
            {!searchOpen && (
              <button onClick={() => setSearchOpen(true)} style={topBtnStyle}>
                <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
                  <circle cx="5" cy="5" r="3.5" stroke="currentColor" strokeWidth="1.3"/>
                  <path d="M7.5 7.5 L10.5 10.5" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/>
                </svg>
                Search
              </button>
            )}
            {searchOpen && (
              <div style={{
                position: 'relative',
                background: 'rgba(10,13,31,0.92)',
                border: '1px solid rgba(180,200,255,0.15)',
                borderRadius: 24,
                padding: '0 14px',
                display: 'flex', alignItems: 'center', gap: 10,
                minWidth: 280
              }}>
                <svg width="12" height="12" viewBox="0 0 12 12" fill="none" style={{ color: 'var(--ink-2)' }}>
                  <circle cx="5" cy="5" r="3.5" stroke="currentColor" strokeWidth="1.3"/>
                  <path d="M7.5 7.5 L10.5 10.5" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/>
                </svg>
                <input
                  autoFocus
                  value={searchText}
                  onChange={e => setSearchText(e.target.value)}
                  onKeyDown={e => {
                    if (e.key === 'Escape') { setSearchOpen(false); setSearchText(''); }
                    if (e.key === 'Enter' && searchResults[0]) {
                      handlePick(searchResults[0].idx);
                      setSearchOpen(false); setSearchText('');
                    }
                  }}
                  placeholder="Language or city…"
                  style={{
                    flex: 1, background: 'transparent', border: 'none', outline: 'none',
                    color: 'var(--ink-0)', fontSize: 13, padding: '10px 0',
                    fontFamily: 'Inter, sans-serif'
                  }}
                />
                <button onClick={() => { setSearchOpen(false); setSearchText(''); }}
                  style={{ background: 'none', border: 'none', color: 'var(--ink-2)', cursor: 'pointer', fontSize: 11 }}>
                  esc
                </button>

                {searchResults.length > 0 && (
                  <div style={{
                    position: 'absolute', top: '100%', left: 0, right: 0, marginTop: 6,
                    background: 'rgba(10,13,31,0.96)',
                    border: '1px solid rgba(180,200,255,0.15)',
                    borderRadius: 10, overflow: 'hidden',
                    boxShadow: '0 10px 40px rgba(0,0,0,0.5)'
                  }}>
                    {searchResults.map(({ l, idx }) => (
                      <button key={idx}
                        onClick={() => { handlePick(idx); setSearchOpen(false); setSearchText(''); }}
                        style={{
                          width: '100%', padding: '10px 14px',
                          background: 'transparent', border: 'none',
                          textAlign: 'left', cursor: 'pointer',
                          color: 'var(--ink-1)', fontSize: 13,
                          display: 'flex', justifyContent: 'space-between',
                          borderBottom: '1px solid rgba(180,200,255,0.06)',
                          fontFamily: 'inherit'
                        }}
                        onMouseEnter={e => e.currentTarget.style.background = 'rgba(180,200,255,0.06)'}
                        onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
                      >
                        <span style={{ color: 'var(--ink-0)' }}>{l.language}</span>
                        <span style={{ color: 'var(--ink-3)', fontSize: 11, fontFamily: 'JetBrains Mono, monospace' }}>
                          {l.location.split(',').pop().trim()}
                        </span>
                      </button>
                    ))}
                  </div>
                )}
              </div>
            )}
          </div>

          <button onClick={handleShuffle} style={topBtnStyle}>
            <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
              <path d="M1 3 L3 3 L8 9 L11 9 M1 9 L3 9 L4.5 7 M7.5 5 L8 9 M9 7.5 L11 9 L9 10.5 M9 1.5 L11 3 L9 4.5"
                stroke="currentColor" strokeWidth="1.1" strokeLinecap="round" strokeLinejoin="round"/>
            </svg>
            Shuffle
          </button>

          <button onClick={() => setShowTweaks(v => !v)} style={{ ...topBtnStyle, opacity: showTweaks ? 1 : 0.7 }}>
            <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
              <circle cx="3" cy="3" r="1.5" stroke="currentColor" strokeWidth="1.1"/>
              <circle cx="9" cy="9" r="1.5" stroke="currentColor" strokeWidth="1.1"/>
              <path d="M3 4.5 L3 10.5 M9 1.5 L9 7.5" stroke="currentColor" strokeWidth="1.1" strokeLinecap="round"/>
            </svg>
            Tweaks
          </button>
        </div>
      </header>

      {/* BOTTOM STATUS */}
      <footer style={{
        position: 'fixed', bottom: 22, left: 0, right: 0, zIndex: 10,
        display: 'flex', justifyContent: 'center',
        pointerEvents: 'none'
      }}>
        <div style={{
          pointerEvents: 'auto',
          display: 'flex', alignItems: 'center', gap: 20,
          padding: '10px 20px',
          background: 'rgba(10,13,31,0.75)',
          border: '1px solid rgba(180,200,255,0.12)',
          borderRadius: 999,
          backdropFilter: 'blur(10px)',
          fontFamily: 'JetBrains Mono, monospace',
          fontSize: 11, letterSpacing: '0.08em',
          color: 'var(--ink-2)'
        }}>
          <span>
            <span style={{ color: tweaks.accentColor, fontWeight: 500 }}>
              {discovered.size.toString().padStart(2, '0')}
            </span>
            <span style={{ color: 'var(--ink-3)' }}>/{languages?.length ?? '—'}</span>
            <span style={{ marginLeft: 6, textTransform: 'uppercase', letterSpacing: '0.16em' }}>
              discovered
            </span>
          </span>
          <span style={{ color: 'var(--ink-3)' }}>·</span>
          <span style={{ textTransform: 'uppercase', letterSpacing: '0.16em' }}>
            drag to spin · click a pin
          </span>
        </div>
      </footer>

      <Panel
        open={panelOpen}
        lang={selectedLang}
        locationName={selectedLocation?.name || selectedLang?.location || ''}
        phrase={phrase}
        fact={fact}
        onClose={() => { setSelectedIdx(null); setSelectedLocation(null); }}
        onReroll={handleReroll}
        accentColor={tweaks.accentColor}
        latText={selectedLang ? formatCoord(selectedLocation?.lat ?? selectedLang.lat, 'N', 'S') : ''}
        lngText={selectedLang ? formatCoord(selectedLocation?.lng ?? selectedLang.lng, 'E', 'W') : ''}
      />

      <Tweaks open={showTweaks} values={tweaks} setValue={setTweakValue} presets={ACCENT_PRESETS} />
    </>
  );
}

function hexToRgba(hex, a) {
  const h = hex.replace('#', '');
  const r = parseInt(h.substring(0,2), 16);
  const g = parseInt(h.substring(2,4), 16);
  const b = parseInt(h.substring(4,6), 16);
  return `rgba(${r},${g},${b},${a})`;
}

// Read country name from GeoJSON feature; tries common property keys
function getCountryName(feat) {
  const p = feat.properties || {};
  return (
    p.ADMIN || p.admin ||
    p.NAME_LONG || p.name_long ||
    p.NAME || p.name ||
    p.SOVEREIGNT || p.sovereignt ||
    p.FORMAL_EN || p.formal_en ||
    p.BRK_NAME || p.brk_name ||
    p.GEOUNIT || p.geounit ||
    p.SUBUNIT || p.subunit ||
    'Unknown region'
  );
}

// ray-casting point-in-polygon for GeoJSON Polygon/MultiPolygon
function pointInFeature(point, feat) {
  const g = feat.geometry;
  if (!g) return false;
  if (g.type === 'Polygon') return pointInPolygon(point, g.coordinates);
  if (g.type === 'MultiPolygon') {
    for (const poly of g.coordinates) if (pointInPolygon(point, poly)) return true;
  }
  return false;
}

// compute bounding box of a GeoJSON feature (Polygon / MultiPolygon)
function computeBBox(feat) {
  const g = feat.geometry;
  if (!g) return null;
  let minLng = Infinity, maxLng = -Infinity, minLat = Infinity, maxLat = -Infinity;
  const scan = rings => {
    for (const ring of rings) {
      for (const [lng, lat] of ring) {
        if (lng < minLng) minLng = lng;
        if (lng > maxLng) maxLng = lng;
        if (lat < minLat) minLat = lat;
        if (lat > maxLat) maxLat = lat;
      }
    }
  };
  if (g.type === 'Polygon') scan(g.coordinates);
  else if (g.type === 'MultiPolygon') {
    // pick the largest ring so centroid for e.g. USA lands in mainland, not Alaska
    let best = null, bestArea = -Infinity;
    for (const poly of g.coordinates) {
      const ring = poly[0];
      let minL = Infinity, maxL = -Infinity, minA = Infinity, maxA = -Infinity;
      for (const [lng, lat] of ring) {
        if (lng < minL) minL = lng;
        if (lng > maxL) maxL = lng;
        if (lat < minA) minA = lat;
        if (lat > maxA) maxA = lat;
      }
      const area = (maxL - minL) * (maxA - minA);
      if (area > bestArea) { bestArea = area; best = poly; }
    }
    if (best) scan(best);
    else scan(g.coordinates[0] || []);
  }
  if (!isFinite(minLng)) return null;
  return { minLng, maxLng, minLat, maxLat };
}

function pointInPolygon(point, rings) {
  const [x, y] = point;
  let inside = false;
  for (let r = 0; r < rings.length; r++) {
    const ring = rings[r];
    let rIn = false;
    for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
      const xi = ring[i][0], yi = ring[i][1];
      const xj = ring[j][0], yj = ring[j][1];
      const intersect = ((yi > y) !== (yj > y)) &&
        (x < (xj - xi) * (y - yi) / ((yj - yi) || 1e-12) + xi);
      if (intersect) rIn = !rIn;
    }
    if (r === 0) inside = rIn;
    else if (rIn) inside = !inside; // hole
  }
  return inside;
}

const topBtnStyle = {
  display: 'inline-flex', alignItems: 'center', gap: 7,
  padding: '8px 14px',
  background: 'rgba(10,13,31,0.75)',
  border: '1px solid rgba(180,200,255,0.12)',
  backdropFilter: 'blur(10px)',
  borderRadius: 999,
  color: 'var(--ink-1)',
  fontSize: 12,
  fontFamily: 'Inter, sans-serif',
  letterSpacing: '0.02em',
  transition: 'all 0.2s'
};

class ErrBoundary extends React.Component {
  constructor(p) { super(p); this.state = { err: null }; }
  static getDerivedStateFromError(err) { return { err }; }
  componentDidCatch(err, info) { console.error('REAL ERR:', err?.message, err?.stack, info?.componentStack); }
  render() {
    if (this.state.err) return <div style={{padding:40,color:'#f88',fontFamily:'monospace',fontSize:11}}>{this.state.err.message}<br/><pre>{this.state.err.stack}</pre></div>;
    return this.props.children;
  }
}

ReactDOM.createRoot(document.getElementById('ui')).render(<ErrBoundary><App /></ErrBoundary>);
