var {useState,useEffect,useRef} = React;

/* ============================================================
   INTERACTIVE 3D GLOBE HERO  (Three.js)
   ============================================================ */

/* --- destination data (India + UAE + route hubs) --- */
window.DESTINATIONS = [
  // ---------- INDIA ----------
  {n:"Kashmir", lat:34.08, lng:74.80, region:"India", feat:true,
   d:"Snow-capped peaks, shikara-dotted lakes and Mughal gardens: the fabled paradise of the north.",
   see:["Dal Lake & shikaras","Gulmarg gondola","Mughal gardens"], best:"Mar–Oct (Apr for tulips)",
   pkg:["Kashmir Valley · 6 days","Honeymoon in Srinagar · 5 days"]},
  {n:"Leh-Ladakh", lat:34.15, lng:77.58, region:"India", feat:true,
   d:"A high-altitude desert of cobalt lakes, monasteries and the world's highest motorable passes.",
   see:["Pangong Lake","Nubra Valley","Thiksey Monastery"], best:"May–Sep",
   pkg:["Ladakh Expedition · 8 days","Leh & Pangong · 6 days"]},
  {n:"Delhi", lat:28.61, lng:77.21, region:"India", feat:false,
   d:"India's capital, where Mughal monuments and colonial avenues meet a buzzing modern metropolis.",
   see:["Red Fort","Qutub Minar","India Gate"], best:"Oct–Mar",
   pkg:["Golden Triangle · 6 days"]},
  {n:"Agra", lat:27.18, lng:78.01, region:"India", feat:false,
   d:"Home of the Taj Mahal: the world's most romantic monument in luminous white marble.",
   see:["Taj Mahal","Agra Fort","Mehtab Bagh"], best:"Oct–Mar",
   pkg:["Golden Triangle · 6 days","Taj day trip"]},
  {n:"Jaipur", lat:26.91, lng:75.79, region:"India", feat:true,
   d:"The Pink City: palaces, hilltop forts and vibrant bazaars steeped in royal Rajput grandeur.",
   see:["Amber Fort","Hawa Mahal","City Palace"], best:"Oct–Mar",
   pkg:["Royal Rajasthan · 9 days","Golden Triangle · 6 days"]},
  {n:"Udaipur", lat:24.58, lng:73.68, region:"India", feat:false,
   d:"The City of Lakes: shimmering palaces, romantic boat rides and Rajasthan's most serene setting.",
   see:["Lake Pichola","City Palace","Jag Mandir"], best:"Sep–Mar",
   pkg:["Royal Rajasthan · 9 days"]},
  {n:"Jodhpur", lat:26.29, lng:73.02, region:"India", feat:false,
   d:"The Blue City crowned by mighty Mehrangarh Fort, rising above a maze of indigo lanes.",
   see:["Mehrangarh Fort","Jaswant Thada","Clock Tower bazaar"], best:"Oct–Mar",
   pkg:["Royal Rajasthan · 9 days"]},
  {n:"Jaisalmer", lat:26.92, lng:70.91, region:"India", feat:false,
   d:"The Golden City of the Thar: a living sandstone fort, camel safaris and desert camps under the stars.",
   see:["Sonar Quila","Sam sand dunes","Patwon ki Haveli"], best:"Oct–Mar",
   pkg:["Royal Rajasthan · 9 days","Desert Safari · 3 days"]},
  {n:"Varanasi", lat:25.32, lng:82.97, region:"India", feat:true,
   d:"The eternal city on the Ganges: ghats, temples and the mesmerising evening Ganga Aarti.",
   see:["Ganga Aarti","Kashi Vishwanath","Sarnath"], best:"Oct–Mar",
   pkg:["Spiritual North India · 6 days"]},
  {n:"Prayagraj", lat:25.44, lng:81.85, region:"India", feat:false,
   d:"The sacred confluence of the Ganga, Yamuna and Saraswati: heart of the Kumbh Mela.",
   see:["Triveni Sangam","Allahabad Fort","Anand Bhavan"], best:"Oct–Mar",
   pkg:["Spiritual North India · 6 days"]},
  {n:"Ayodhya", lat:26.79, lng:82.19, region:"India", feat:false,
   d:"The birthplace of Lord Ram: a revered pilgrimage city on the banks of the Sarayu.",
   see:["Ram Mandir","Hanuman Garhi","Sarayu Aarti"], best:"Oct–Mar",
   pkg:["Spiritual North India · 6 days"]},
  {n:"Kerala", lat:9.93, lng:76.27, region:"India", feat:true,
   d:"God's Own Country: palm-fringed backwaters, misty tea hills, houseboats and golden beaches.",
   see:["Alleppey backwaters","Munnar tea hills","Fort Kochi"], best:"Sep–Mar",
   pkg:["God's Own Country · 7 days","Kerala Honeymoon · 6 days"]},
  {n:"Goa", lat:15.30, lng:74.12, region:"India", feat:true,
   d:"Sun, sand and susegad: golden beaches, Portuguese heritage and India's most laid-back coast.",
   see:["Palolem & Baga beaches","Old Goa churches","Dudhsagar Falls"], best:"Nov–Feb",
   pkg:["Goa Getaway · 4 days","Beach Honeymoon · 5 days"]},
  {n:"Andaman Islands", lat:11.62, lng:92.73, region:"India", feat:true,
   d:"Turquoise lagoons and coral reefs: India's most pristine islands for diving and seclusion.",
   see:["Radhanagar Beach","Cellular Jail","Neil Island"], best:"Oct–May",
   pkg:["Andaman Escape · 6 days","Island Honeymoon · 5 days"]},
  {n:"Somnath", lat:20.89, lng:70.40, region:"India", feat:false,
   d:"The first of the twelve Jyotirlingas: a seaside temple of profound spiritual significance.",
   see:["Somnath Temple","Triveni Sangam","Bhalka Tirth"], best:"Oct–Mar",
   pkg:["Gujarat Heritage & Spiritual · 7 days"]},
  {n:"Dwarka", lat:22.24, lng:68.97, region:"India", feat:false,
   d:"The legendary kingdom of Lord Krishna: one of the holiest Char Dham pilgrimage sites.",
   see:["Dwarkadhish Temple","Bet Dwarka","Nageshwar Jyotirlinga"], best:"Oct–Mar",
   pkg:["Gujarat Heritage & Spiritual · 7 days"]},
  {n:"Rann of Kutch", lat:23.85, lng:70.50, region:"India", feat:true,
   d:"An endless white salt desert that glows under the full moon: culture, craft and the Rann Utsav.",
   see:["White Rann","Rann Utsav","Kutchi handicrafts"], best:"Nov–Feb",
   pkg:["Gujarat Heritage & Spiritual · 7 days"]},
  // ---------- UAE ----------
  {n:"Dubai", lat:25.20, lng:55.27, region:"UAE", feat:true,
   d:"A city of superlatives: record-breaking skylines, golden deserts and world-class luxury.",
   see:["Burj Khalifa","The Dubai Mall","Old Dubai souks"], best:"Nov–Mar",
   pkg:["Dubai & UAE · 6 days","Dubai City Break · 4 days"]},
  {n:"Abu Dhabi", lat:24.45, lng:54.38, region:"UAE", feat:true,
   d:"The UAE's elegant capital: the dazzling Sheikh Zayed Mosque, Louvre and Ferrari World.",
   see:["Sheikh Zayed Mosque","Louvre Abu Dhabi","Ferrari World"], best:"Nov–Mar",
   pkg:["Dubai & UAE · 6 days"]},
  {n:"Sharjah", lat:25.35, lng:55.41, region:"UAE", feat:false,
   d:"The cultural heart of the Emirates: heritage museums, art and traditional souks.",
   see:["Al Noor Mosque","Heritage Area","Blue Souk"], best:"Nov–Mar",
   pkg:["Dubai & UAE · 6 days"]},
  {n:"Desert Safari", lat:24.90, lng:55.60, region:"UAE", feat:false,
   d:"Dune-bashing, camel rides and starlit desert camps with live music and Arabian feasts.",
   see:["Dune bashing","Camel rides","Bedouin camp dinner"], best:"Oct–Apr",
   pkg:["Dubai & UAE · 6 days","Desert Safari add-on"]},
  {n:"Palm Jumeirah", lat:25.11, lng:55.13, region:"UAE", feat:false,
   d:"The iconic man-made island: luxury resorts, Atlantis and the View at The Palm.",
   see:["Atlantis Aquaventure","The View","Beach clubs"], best:"Nov–Mar",
   pkg:["Dubai City Break · 4 days"]},
  {n:"Burj Khalifa", lat:25.197, lng:55.274, region:"UAE", feat:false,
   d:"The world's tallest building: soar to At The Top for unmatched views over the city and desert.",
   see:["At The Top deck","Dubai Fountain","Dubai Mall"], best:"Nov–Mar",
   pkg:["Dubai City Break · 4 days"]},
  {n:"Dubai Marina", lat:25.08, lng:55.14, region:"UAE", feat:false,
   d:"A glittering waterfront of yachts, skyscrapers and the buzzing Marina Walk.",
   see:["Marina Walk","Yacht cruise","JBR Beach"], best:"Nov–Mar",
   pkg:["Dubai City Break · 4 days"]},
  // ---------- ROUTE HUBS (also searchable) ----------
  {n:"Singapore", lat:1.35, lng:103.82, region:"International", feat:true,
   d:"A futuristic garden city: Marina Bay, Gardens by the Bay and Sentosa's island fun.",
   see:["Marina Bay Sands","Gardens by the Bay","Sentosa"], best:"Year-round",
   pkg:["International Getaways · 6 days"]},
  {n:"Kuala Lumpur", lat:3.14, lng:101.69, region:"International", feat:false,
   d:"Malaysia's dynamic capital: the Petronas Towers, street food and Batu Caves.",
   see:["Petronas Towers","Batu Caves","Bukit Bintang"], best:"Year-round",
   pkg:["International Getaways · 6 days"]},
];

/* flight routes between hubs (great-circle arcs with a moving plane) */
window.FLIGHT_ROUTES = [
  ["Delhi","Dubai"],["Dubai","Singapore"],["Singapore","Kuala Lumpur"],
  ["Delhi","Singapore"],["Kerala","Dubai"],["Jaipur","Dubai"],
  ["Delhi","Kerala"],["Dubai","Abu Dhabi"],["Delhi","Kuala Lumpur"],
];

/* quick-access chips */
window.GLOBE_CHIPS = ["Kashmir","Leh-Ladakh","Jaipur","Varanasi","Kerala","Goa","Andaman Islands","Rann of Kutch","Dubai","Abu Dhabi","Singapore"];

window.GlobeHero = function({navigate}){
  const mountRef = useRef(null);
  const tipRef   = useRef(null);
  const flyRef   = useRef(null);
  const apiRef   = useRef({});
  const zoomRef  = useRef(null);
  const selRef   = useRef(null);
  const mapElRef = useRef(null);
  const mapRef   = useRef(null);
  const markersRef = useRef({});
  const boundaryRef = useRef(null);
  const viewRef  = useRef('globe');
  const pendingCenterRef = useRef(null);
  const openZoomRef = useRef(4);
  const goToRef = useRef(null);
  const chipsRef = useRef(null);
  const [selected,setSelected] = useState(null);
  const [query,setQuery] = useState("");
  const [ready,setReady] = useState(false);
  const [failed,setFailed] = useState(false);
  const [view,setView] = useState('globe');

  useEffect(()=>{ selRef.current = selected; },[selected]);
  useEffect(()=>{ viewRef.current = view; },[view]);
  // if the WebGL globe can't run, fall back to the flat map
  useEffect(()=>{ if(failed) setView('map'); },[failed]);

  // ---- Leaflet / OpenStreetMap flat map: built once, eagerly, so tiles are
  //      already loaded before the globe hands off. Leaflet owns its own inner
  //      <div> (mapElRef) whose className React never touches. ----
  useEffect(()=>{
    const L = window.L;
    if(!L || !mapElRef.current || mapRef.current) return;
    const map = L.map(mapElRef.current,{
      center:[20.6,78.9], zoom:4, minZoom:2, maxZoom:18,
      zoomControl:false, worldCopyJump:true, scrollWheelZoom:true
    });
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
      maxZoom:19,
      attribution:'&copy; <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors &middot; boundary &copy; <a href="https://github.com/datameet/maps" target="_blank" rel="noopener">DataMeet</a>'
    }).addTo(map);

    // ---- India's official boundary (Survey-of-India compliant: full Jammu &
    //      Kashmir, Ladakh, Gilgit-Baltistan & Aksai Chin shown as India).
    //      Loaded lazily; the dissolved national outline is drawn on a canvas
    //      renderer over the street tiles so the territory reads correctly. ----
    (function loadIndiaBoundary(){
      const decimate=(coords,keep)=>coords.map(ring=>{
        if(ring.length<=keep*3) return ring;
        const out=ring.filter((_,i)=>i%keep===0);
        if(out[out.length-1]!==ring[ring.length-1]) out.push(ring[ring.length-1]);
        return out;
      });
      const thin=(geom)=>{
        if(geom.type==='Polygon') return {type:'Polygon',coordinates:decimate(geom.coordinates,5)};
        if(geom.type==='MultiPolygon') return {type:'MultiPolygon',coordinates:geom.coordinates.map(p=>decimate(p,5))};
        return geom;
      };
      fetch('https://raw.githubusercontent.com/datameet/maps/master/Country/india-composite.geojson')
        .then(r=>r.ok?r.json():Promise.reject())
        .then(gj=>{
          const feats=(gj.features||[]).map(f=>({...f,geometry:thin(f.geometry)}));
          const layer=L.geoJSON({type:'FeatureCollection',features:feats},{
            interactive:false, renderer:L.canvas(),
            style:{color:'#4f6fc0', weight:2.4, opacity:.95, fill:true, fillColor:'#4f6fc0', fillOpacity:.05}
          });
          layer.addTo(map);
          boundaryRef.current=layer;
        })
        .catch(()=>{/* offline / blocked: street tiles still usable */});
    })();
    window.DESTINATIONS.forEach(dst=>{
      const icon = L.divIcon({className:'map-pin-wrap',
        html:`<span class="map-pin-dot${dst.feat?' feat':''}"></span>`,
        iconSize:[30,30], iconAnchor:[15,15]});
      const m = L.marker([dst.lat,dst.lng],{icon, title:dst.n, riseOnHover:true}).addTo(map);
      m.bindTooltip(dst.n,{direction:'top',offset:[0,-11],className:'map-tip'});
      m.on('click',()=>{ setSelected(dst); map.panTo([dst.lat,dst.lng], {animate:true,duration:.5}); });
      markersRef.current[dst.n]=m;
    });
    mapRef.current = map;
    // single click labels the ACTUAL place clicked (reverse-geocoded via
    // OpenStreetMap Nominatim) and lists nearby places to visit (Overpass).
    // Double-click zooms; a short timer disambiguates single vs double click.
    const placeLabel=async (ll)=>{
      const z=Math.min(16,Math.max(8,Math.round(map.getZoom())));
      // PRIMARY: Google Geocoding (only with a classic AIza… Maps key)
      if(window.GOOGLE_API_KEY && window.GOOGLE_MAPS_OK){
        try{
          const gurl=`https://maps.googleapis.com/maps/api/geocode/json?latlng=${ll.lat},${ll.lng}&result_type=locality|sublocality|administrative_area_level_2|administrative_area_level_1|country&key=${encodeURIComponent(window.GOOGLE_API_KEY)}`;
          const gr=await fetch(gurl);
          const gj=await gr.json();
          if(gj.status==='OK' && gj.results && gj.results.length){
            const get=(t)=>{ for(const res of gj.results){ const c=(res.address_components||[]).find(x=>x.types.includes(t)); if(c) return c.long_name; } return ''; };
            const primary=get('locality')||get('sublocality')||get('administrative_area_level_2')||get('administrative_area_level_1')||get('country')||'Unknown place';
            const state=get('administrative_area_level_1'), country=get('country');
            const secondary=[state,country].filter(Boolean).filter(s=>s!==primary).join(', ');
            return {primary,secondary};
          }
        }catch(_){ /* fall through to OpenStreetMap */ }
      }
      // FALLBACK: OpenStreetMap Nominatim (free, no key)
      const url=`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${ll.lat}&lon=${ll.lng}&zoom=${z}&addressdetails=1`;
      const r=await fetch(url,{headers:{'Accept':'application/json'}});
      const j=await r.json();
      const a=j.address||{};
      const primary=a.city||a.town||a.village||a.suburb||a.hamlet||a.neighbourhood||a.county||j.name||a.state||a.country||'Unknown place';
      const secondary=[a.state,a.country].filter(Boolean).filter(s=>s!==primary).join(', ');
      return {primary,secondary};
    };
    const haversineKm=(la1,lo1,la2,lo2)=>{const toR=x=>x*Math.PI/180;const dLa=toR(la2-la1),dLo=toR(lo2-lo1);
      const s=Math.sin(dLa/2)**2+Math.cos(toR(la1))*Math.cos(toR(la2))*Math.sin(dLo/2)**2;return 6371*2*Math.asin(Math.sqrt(s));};
    const withTimeout=async (url,opts,ms)=>{
      const ctrl=new AbortController();
      const to=setTimeout(()=>ctrl.abort(),ms||9000);
      try{ return await fetch(url,{...(opts||{}),signal:ctrl.signal}); }
      finally{ clearTimeout(to); }
    };
    // Wikidata classes that count as "tourist attractions": temples, forts,
    // museums, monuments, parks, beaches, waterfalls, towers, places of worship,
    // protected areas, gardens, heritage sites… (matched with subclasses too).
    const TOURIST_TYPES=['Q570116','Q839954','Q4989906','Q5003624','Q33506','Q44539',
      'Q16560','Q23413','Q57821','Q22698','Q8502','Q34038','Q40080','Q12518','Q16970',
      'Q32815','Q473972','Q1107656','Q2065736','Q35509','Q24398318']
      .map(q=>'wd:'+q).join(' ');
    // STEP 1: nearby article candidates from Wikipedia GeoSearch (fast, ≤10km).
    const wikiCandidates=async (ll)=>{
      const url=`https://en.wikipedia.org/w/api.php?action=query&list=geosearch`+
        `&gscoord=${ll.lat}%7C${ll.lng}&gsradius=10000&gslimit=40&format=json&origin=*`;
      const j=await (await withTimeout(url)).json();
      return ((j.query&&j.query.geosearch)||[]).map(g=>({name:g.title,pageid:g.pageid,dist:g.dist/1000}));
    };
    // STEP 2/3: resolve those candidates to Wikidata IDs, then keep only the
    // ones whose type is a tourist attraction. Bounded item set ⇒ fast even in
    // dense cities where a radius-based type query would time out.
    const filterTourist=async (cands)=>{
      if(!cands.length) return [];
      const ids=cands.map(c=>c.pageid).join('|');
      const pp=await (await withTimeout(`https://en.wikipedia.org/w/api.php?action=query&prop=pageprops&ppprop=wikibase_item&pageids=${ids}&format=json&origin=*`)).json();
      const qidByPage={};
      Object.values((pp.query&&pp.query.pages)||{}).forEach(p=>{
        if(p.pageprops&&p.pageprops.wikibase_item) qidByPage[p.pageid]=p.pageprops.wikibase_item;
      });
      const qids=Object.values(qidByPage);
      if(!qids.length) return [];
      const q=`SELECT DISTINCT ?place WHERE { VALUES ?place { ${qids.map(x=>'wd:'+x).join(' ')} }`+
        ` ?place wdt:P31/wdt:P279* ?type. VALUES ?type { ${TOURIST_TYPES} } }`;
      const wd=await (await withTimeout('https://query.wikidata.org/sparql?format=json&query='+encodeURIComponent(q),{headers:{'Accept':'application/json'}})).json();
      const keep=new Set((wd.results.bindings||[]).map(b=>b.place.value.split('/').pop()));
      return cands.filter(c=>keep.has(qidByPage[c.pageid]));
    };
    // FALLBACK: sparse/rural clicks where the type-filter leaves <3 results.
    // A wide subclass-of type walk times out near populated regions, so instead
    // we grab the nearest places that HAVE an English Wikipedia article (fast),
    // then drop obvious non-attractions (towns, admin units, stations, rivers).
    const NON_PLACE=/\b(district|taluk|tehsil|mandal|municipality|municipal|constituency|lok sabha|vidhan sabha|assembly|census|gram panchayat|railway station|junction|metro station|airport|bus (stand|station)|river|stream|reservoir|dam\b|canal|airfield|cantonment|block\b|subdivision|division|state highway|national highway|\(state\)|\(city\)|\(town\)|\(village\))\b/i;
    const wikidataWide=async (ll,radius)=>{
      const q=`SELECT DISTINCT ?placeLabel ?dist WHERE {`+
        `SERVICE wikibase:around { ?place wdt:P625 ?loc.`+
        `bd:serviceParam wikibase:center "Point(${ll.lng} ${ll.lat})"^^geo:wktLiteral.`+
        `bd:serviceParam wikibase:radius "${radius}". bd:serviceParam wikibase:distance ?dist. }`+
        `?article schema:about ?place; schema:isPartOf <https://en.wikipedia.org/>.`+
        `SERVICE wikibase:label { bd:serviceParam wikibase:language "en". } }`+
        `ORDER BY ?dist LIMIT 40`;
      const j=await (await withTimeout('https://query.wikidata.org/sparql?format=json&query='+encodeURIComponent(q),{headers:{'Accept':'application/json'}})).json();
      return (j.results.bindings||[]).map(b=>({name:b.placeLabel.value, dist:+b.dist.value}));
    };
    const nearbyPlaces=async (ll)=>{
      let out=[];
      try{ out=await filterTourist(await wikiCandidates(ll)); }
      catch(_){ out=[]; }
      // too few curated tourist spots within 10km → widen the net
      if(out.length<4){
        try{
          let wide=await wikidataWide(ll,150);
          if(wide.length<4){ const w2=await wikidataWide(ll,400); if(w2.length>wide.length) wide=w2; }
          const seen=new Set(out.map(p=>p.name));
          wide.forEach(p=>{
            if(p.name && !/^Q\d+$/.test(p.name) && !NON_PLACE.test(p.name) && !seen.has(p.name)){
              seen.add(p.name); out.push(p);
            }
          });
        }catch(_){/* keep what we have */}
      }
      const seen=new Set(), clean=[];
      out.forEach(p=>{ if(p.name&&!/^Q\d+$/.test(p.name)&&!seen.has(p.name)){ seen.add(p.name); clean.push(p); } });
      clean.sort((a,b)=>a.dist-b.dist);
      return clean.slice(0,6);
    };
    // AI fallback, when the open data sources come up short (common for small
    // AI fallback, when the open data sources come up short (common for small
    // towns/tehsils), ask the AI for the nearest well-known tourist attractions.
    // Uses the site's Gemini key on the live host; falls back to the built-in
    // model when running inside the design preview.
    const aiNearby=async (ll, placeName)=>{
      const where = placeName ? `${placeName} (around ${ll.lat.toFixed(3)}, ${ll.lng.toFixed(3)})`
                              : `${ll.lat.toFixed(3)}, ${ll.lng.toFixed(3)}`;
      const prompt =
`List up to 6 real, notable tourist attractions nearest to ${where}. `+
`Prefer temples, forts, palaces, monuments, parks, lakes, viewpoints, museums, `+
`wildlife sanctuaries, waterfalls and famous landmarks people actually travel to see. `+
`Order by distance, closest first. Do NOT include the place itself or generic towns/districts. `+
`Reply with ONLY a JSON array and nothing else, e.g. `+
`[{"name":"Sanchi Stupa","km":46}], where km is the approximate straight-line distance `+
`in kilometres (integer) from the given point. Give your best estimate if unsure.`;
      let raw='';
      try{
        if(window.GOOGLE_API_KEY && window.GOOGLE_KEY_OK){
          // Gemini via header auth (works with new AQ.… keys)
          const url="https://generativelanguage.googleapis.com/v1beta/models/"+
            ((window.GEMINI_MODEL)||"gemini-2.5-flash")+":generateContent";
          const r=await fetch(url,{ method:"POST",
            headers:{"Content-Type":"application/json","x-goog-api-key":window.GOOGLE_API_KEY},
            body:JSON.stringify({ contents:[{role:"user",parts:[{text:prompt}]}],
              generationConfig:{ maxOutputTokens:400, temperature:0.4, thinkingConfig:{ thinkingBudget:0 } } }) });
          if(r.ok){
            const j=await r.json();
            const c=j.candidates && j.candidates[0];
            raw=(c && c.content && c.content.parts && c.content.parts.map(p=>p.text).join("")) || '';
          }
        }
        if(!raw && window.claude && window.claude.complete){
          raw = await window.claude.complete(prompt);
        }
      }catch(_){ return []; }
      const m = (raw||'').match(/\[[\s\S]*\]/);
      if(!m) return [];
      let arr; try{ arr = JSON.parse(m[0]); }catch(_){ return []; }
      return (Array.isArray(arr)?arr:[])
        .filter(o=>o && o.name)
        .slice(0,6)
        .map(o=>({ name:String(o.name).trim(), dist:(o.km!=null && isFinite(o.km))?Number(o.km):null, ai:true }));
    };
    const fmtKm=d=> d<1 ? Math.round(d*1000)+' m' : d.toFixed(1)+' km';
    let clickTimer=null;
    map.on('click',(e)=>{
      if(clickTimer) clearTimeout(clickTimer);
      clickTimer=setTimeout(async ()=>{
        clickTimer=null;
        const ll=e.latlng;
        const popup=L.popup({className:'place-popup',closeButton:true,autoPan:true,maxWidth:240,offset:[0,-1]})
          .setLatLng(ll).setContent('<span class="place-name">Locating…</span>').openOn(map);
        let header='';
        let placeName='';
        try{
          const {primary,secondary}=await placeLabel(ll);
          placeName = secondary ? `${primary}, ${secondary}` : primary;
          header=`<strong class="place-name">${primary}</strong>${secondary?`<span class="place-sub">${secondary}</span>`:''}`;
        }catch(_){ header=`<span class="place-name">${ll.lat.toFixed(3)}°, ${ll.lng.toFixed(3)}°</span>`; }
        if(!popup.isOpen()) return;
        popup.setContent(header+'<div class="place-poi"><span class="poi-h">Nearby to visit</span><span class="poi-load">Finding places…</span></div>');
        try{
          let pois=await nearbyPlaces(ll);
          let aiUsed=false;
          // not enough from open data → ask the AI assistant
          if(pois.length<3){
            if(popup.isOpen())
              popup.setContent(header+'<div class="place-poi"><span class="poi-h">Nearby to visit</span><span class="poi-load">Asking AI for ideas…</span></div>');
            const ai=await aiNearby(ll, placeName);
            const seen=new Set(pois.map(p=>p.name.toLowerCase()));
            ai.forEach(p=>{ if(p.name && !seen.has(p.name.toLowerCase())){ seen.add(p.name.toLowerCase()); pois.push(p); aiUsed=true; } });
            pois.sort((a,b)=>(a.dist==null?1e9:a.dist)-(b.dist==null?1e9:b.dist));
            pois=pois.slice(0,6);
          }
          if(!popup.isOpen()) return;
          const li=p=>{
            const km = p.dist!=null ? (p.ai?'~':'')+fmtKm(p.dist) : '';
            return `<li><span class="poi-n">${p.name}</span><span class="poi-km">${km}</span></li>`;
          };
          const body = pois.length
            ? `<ul class="poi-list">${pois.map(li).join('')}</ul>${aiUsed?'<span class="poi-ai">✦ Suggested by AI</span>':''}`
            : '<span class="poi-empty">No notable spots found nearby</span>';
          popup.setContent(header+`<div class="place-poi"><span class="poi-h">Nearby to visit</span>${body}</div>`);
        }catch(_){
          if(popup.isOpen()) popup.setContent(header+'<div class="place-poi"><span class="poi-empty">Couldn’t load nearby places</span></div>');
        }
      },280);
    });
    map.on('dblclick',()=>{ if(clickTimer){ clearTimeout(clickTimer); clickTimer=null; } map.closePopup(); });
    // zooming out below the level the map opened at returns to the globe,
    // centred on the same place: no separate button needed
    map.on('zoomend',()=>{
      if(viewRef.current!=='map') return;
      if(map.getZoom() < 4 && apiRef.current.toGlobe){
        const c=map.getCenter();
        apiRef.current.toGlobe(c.lat,c.lng);
        setView('globe');
      }
    });
    return ()=>{ map.remove(); mapRef.current=null; };
  },[]);

  // when the flat map becomes visible, recalc its size and drop it onto exactly
  // the spot the user zoomed the globe into
  useEffect(()=>{
    if(view!=='map' || !mapRef.current) return;
    const map = mapRef.current;
    const apply=()=>{
      map.invalidateSize();
      const c = pendingCenterRef.current;
      if(c){ openZoomRef.current=c.zoom; map.setView([c.lat,c.lng], c.zoom, {animate:false}); pendingCenterRef.current=null; }
    };
    requestAnimationFrame(()=>requestAnimationFrame(apply));
    const t=setTimeout(apply,320);
    return ()=>clearTimeout(t);
  },[view]);

  // reflect the selected destination on the map markers
  useEffect(()=>{
    Object.entries(markersRef.current).forEach(([n,m])=>{
      const el = m.getElement && m.getElement();
      const dot = el && el.querySelector('.map-pin-dot');
      if(dot) dot.classList.toggle('sel', !!selected && selected.n===n);
    });
  },[selected, view]);

  // ---- build the scene once ----
  useEffect(()=>{
    const THREE = window.THREE;
    if(!THREE || !THREE.OrbitControls){ setFailed(true); return; }
    const mount = mountRef.current;
    let W = mount.clientWidth, H = mount.clientHeight || 480;

    const R = 1;                                   // globe radius
    const latLngToVec3 = (lat,lng,r=R)=>{
      const phi=(90-lat)*Math.PI/180, theta=(lng+180)*Math.PI/180;
      return new THREE.Vector3(
        -r*Math.sin(phi)*Math.cos(theta),
         r*Math.cos(phi),
         r*Math.sin(phi)*Math.sin(theta));
    };
    // inverse: which lat/lng is the camera looking at
    const camToLatLng = ()=>{
      const v=camera.position.clone().normalize();
      const lat=90 - Math.acos(Math.max(-1,Math.min(1,v.y)))*180/Math.PI;
      let lng=Math.atan2(v.z, -v.x)*180/Math.PI - 180;
      while(lng<-180) lng+=360; while(lng>180) lng-=360;
      return [lat,lng];
    };

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(38, W/H, 0.1, 100);
    const startDir = latLngToVec3(22,74).normalize();
    camera.position.copy(startDir.multiplyScalar(3.4));

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

    // ---- lights (bright, light-theme earth) ----
    const sun = new THREE.DirectionalLight(0xffffff, 0.85);
    sun.position.set(-1.4, 0.8, 2.6);
    scene.add(sun);
    scene.add(new THREE.AmbientLight(0xffffff, 0.92));

    // ---- textures ----
    const loader = new THREE.TextureLoader();
    loader.setCrossOrigin("anonymous");
    const T = (u)=>loader.load(u);
    const CDN = "https://cdn.jsdelivr.net/npm/three-globe/example/img/";
    const dayTex   = T(CDN+"earth-blue-marble.jpg");
    const bumpTex  = T(CDN+"earth-topology.png");
    const specTex  = T(CDN+"earth-water.png");
    const cloudTex = T("https://cdn.jsdelivr.net/gh/turban/webgl-earth@master/images/fair_clouds_4k.png");

    // ---- globe ----
    const globe = new THREE.Mesh(
      new THREE.SphereGeometry(R, 72, 72),
      new THREE.MeshPhongMaterial({
        map:dayTex, bumpMap:bumpTex, bumpScale:0.014,
        specularMap:specTex, specular:new THREE.Color(0x2a3b66), shininess:13
      })
    );
    scene.add(globe);

    // ---- clouds ----
    const clouds = new THREE.Mesh(
      new THREE.SphereGeometry(R*1.012, 64, 64),
      new THREE.MeshPhongMaterial({map:cloudTex, transparent:true, opacity:0.42, depthWrite:false})
    );
    scene.add(clouds);

    // ---- atmosphere (soft rim glow) ----
    const atmMat = new THREE.ShaderMaterial({
      transparent:true, side:THREE.BackSide, depthWrite:false, blending:THREE.AdditiveBlending,
      uniforms:{ c:{value:0.5}, p:{value:5.0}, glow:{value:new THREE.Color(0x6ea0ff)} },
      vertexShader:`varying vec3 vN; void main(){ vN=normalize(normalMatrix*normal); gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`,
      fragmentShader:`uniform vec3 glow; uniform float c; uniform float p; varying vec3 vN;
        void main(){ float i=pow(c-dot(vN,vec3(0.0,0.0,1.0)),p); gl_FragColor=vec4(glow,1.0)*clamp(i,0.0,1.0);}`
    });
    const atmosphere = new THREE.Mesh(new THREE.SphereGeometry(R*1.14,64,64), atmMat);
    scene.add(atmosphere);

    // ---- destination placeholders (HTML name labels) ----
    const labelLayer=document.createElement('div');
    labelLayer.className='globe-labels';
    mount.appendChild(labelLayer);
    const labels=window.DESTINATIONS.map(dst=>{
      const el=document.createElement('button');
      el.className='globe-label'+(dst.feat?' feat':'');
      el.type='button';
      el.innerHTML='<span class="gl-dot"></span><span class="gl-name"></span>';
      el.querySelector('.gl-name').textContent=dst.n;
      el.addEventListener('click',(e)=>{ e.stopPropagation(); if(goToRef.current) goToRef.current(dst); });
      labelLayer.appendChild(el);
      return {el, pos:latLngToVec3(dst.lat,dst.lng,R), dst};
    });

    // ---- controls ----
    const controls=new THREE.OrbitControls(camera,renderer.domElement);
    controls.enableDamping=true; controls.dampingFactor=0.14;
    controls.rotateSpeed=0.28; controls.enablePan=false;
    controls.minDistance=1.45; controls.maxDistance=5.5;
    controls.enableZoom=false;             // we handle zoom ourselves (cursor-anchored)
    controls.autoRotate=true; controls.autoRotateSpeed=0.32;
    let userInteracted=false;
    const stopAuto=()=>{ controls.autoRotate=false; userInteracted=true; };
    controls.addEventListener("start",stopAuto);

    // ---- cursor-anchored zoom: scroll changes distance AND gently rotates the
    //      point under the pointer toward centre, so zoom pivots on the cursor ----
    const raycaster=new THREE.Raycaster();
    const ndc=new THREE.Vector2();
    const dom=renderer.domElement;
    const clampDist=(d)=>Math.min(controls.maxDistance,Math.max(controls.minDistance,d));

    // ---- UNIFIED ZOOM CORE ----
    // Every zoom method: mouse wheel, pinch, double-tap and the +/− buttons: 
    // funnels through this so they stay perfectly in sync. `factor` < 1 zooms in,
    // > 1 zooms out; `anchor` is an optional NDC point ({x,y}) the zoom pivots
    // toward (the cursor / the tapped place) by gently rotating it to centre.
    const computeZoomTarget=(factor,anchor)=>{
      const pos=camera.position.clone();
      const newDist=clampDist(pos.length()*factor);
      if(anchor){
        ndc.set(anchor.x,anchor.y);
        raycaster.setFromCamera(ndc,camera);
        const hit=raycaster.intersectObject(globe)[0];
        if(hit){
          const from=hit.point.clone().normalize();
          const to=pos.clone().normalize();
          const full=new THREE.Quaternion().setFromUnitVectors(from,to);
          const amt=Math.min(0.5,Math.abs(Math.log(factor))*1.4); // proportional to zoom step
          pos.applyQuaternion(new THREE.Quaternion().slerp(full,amt));
        }
      }
      return pos.setLength(newDist);
    };
    // live response (wheel + pinch): applied this frame
    const zoomImmediate=(factor,anchor)=>{ stopAuto(); fly=null; camera.position.copy(computeZoomTarget(factor,anchor)); };
    // smooth glide (double-tap + buttons): tweened in the render loop
    const zoomAnimated=(factor,anchor)=>{ stopAuto(); fly={from:camera.position.clone(),to:computeZoomTarget(factor,anchor),start:performance.now(),dur:480}; };
    zoomRef.current=zoomAnimated;

    // ---- mouse wheel (desktop): cursor-anchored zoom ----
    const onWheel=(e)=>{
      e.preventDefault();
      const rect=dom.getBoundingClientRect();
      const ax=((e.clientX-rect.left)/rect.width)*2-1;
      const ay=-((e.clientY-rect.top)/rect.height)*2+1;
      const factor=Math.exp(Math.max(-0.4,Math.min(0.4,e.deltaY*0.0016))); // >1 out, <1 in
      zoomImmediate(factor,{x:ax,y:ay});
    };
    dom.addEventListener('wheel',onWheel,{passive:false});

    // ---- touch: pinch-to-zoom (two fingers) + double-tap-to-zoom (one finger) ----
    const touchDist=(t)=>Math.hypot(t[0].clientX-t[1].clientX,t[0].clientY-t[1].clientY);
    let pinch=null, lastTap=0, lastTapX=0, lastTapY=0;
    const onTouchStart=(e)=>{
      if(e.touches.length>=2){
        // disable OrbitControls so the gesture is a clean scale, not scale+spin
        controls.enabled=false; stopAuto(); fly=null;
        pinch={d0:touchDist(e.touches)||1, camD0:camera.position.length()};
        return;
      }
      // single touch → double-tap detection (zoom toward the tapped point)
      const t=e.touches[0]; if(!t) return;
      const now=performance.now();
      const moved=Math.hypot(t.clientX-lastTapX,t.clientY-lastTapY);
      if(now-lastTap<300 && moved<32){
        const rect=dom.getBoundingClientRect();
        const ax=((t.clientX-rect.left)/rect.width)*2-1;
        const ay=-((t.clientY-rect.top)/rect.height)*2+1;
        zoomAnimated(0.6,{x:ax,y:ay});   // each double-tap zooms in progressively
        lastTap=0;
      }else{ lastTap=now; lastTapX=t.clientX; lastTapY=t.clientY; }
    };
    const onTouchMove=(e)=>{
      if(pinch && e.touches.length>=2){
        e.preventDefault();
        const d=touchDist(e.touches);
        if(d>0) camera.position.setLength(clampDist(pinch.camD0*(pinch.d0/d))); // smooth live scale
      }
    };
    const onTouchEnd=(e)=>{ if(e.touches.length<2){ pinch=null; controls.enabled=true; } };
    dom.addEventListener('touchstart',onTouchStart,{passive:true});
    dom.addEventListener('touchmove',onTouchMove,{passive:false});
    dom.addEventListener('touchend',onTouchEnd,{passive:true});
    dom.addEventListener('touchcancel',onTouchEnd,{passive:true});

    // ---- realistic aircraft flying across invisible great-circle routes ----
    // ---- fly-to animation ----
    let fly=null;   // {from,to,start,dur}
    const flyTo=(dst,dist)=>{
      controls.autoRotate=false;
      const dir=latLngToVec3(dst.lat,dst.lng,1).normalize();
      const target=dir.multiplyScalar(dist|| (dst.region==="UAE"?1.85:2.0));
      fly={from:camera.position.clone(),to:target,start:performance.now(),dur:1100};
    };
    flyRef.current=flyTo;
    // return from the flat map to the globe, centred on the same place and
    // animating outward so a zoom-out reads as one continuous pull-back
    const toGlobe=(lat,lng)=>{
      transitioning=false; controls.autoRotate=false; fly=null;
      const dir=(typeof lat==='number')
        ? latLngToVec3(lat,lng,1).normalize()
        : camera.position.clone().normalize();
      const start=dir.clone().multiplyScalar(1.72);
      const end=dir.clone().multiplyScalar(3.2);
      camera.position.copy(start);
      fly={from:start.clone(),to:end,start:performance.now(),dur:900};
    };
    apiRef.current={flyTo,toGlobe};

    // ---- resize ----
    const onResize=()=>{ W=mount.clientWidth; H=mount.clientHeight||480;
      camera.aspect=W/H; camera.updateProjectionMatrix(); renderer.setSize(W,H); };
    window.addEventListener("resize",onResize);

    // ---- loop ----
    let raf, t0=performance.now(), transitioning=false;
    const tmpV=new THREE.Vector3();
    const animate=()=>{
      raf=requestAnimationFrame(animate);
      const now=performance.now();
      clouds.rotation.y += 0.0006;
      // fly tween
      if(fly){ const p=Math.min(1,(now-fly.start)/fly.dur); const e=1-Math.pow(1-p,3);
        camera.position.lerpVectors(fly.from,fly.to,e); if(p>=1) fly=null; }
      // zoom-in past the threshold seamlessly hands off to the flat OSM map,
      // centred on exactly the point being viewed (zoom scaled by how far in)
      if(viewRef.current==='globe' && !transitioning && !fly){
        const dist=camera.position.length();
        if(dist < 1.62){
          transitioning=true;
          const [lat,lng]=camToLatLng();
          // open the map a little far so the whole country is visible
          const t=Math.max(0,Math.min(1,(1.62-dist)/(1.62-controls.minDistance)));
          pendingCenterRef.current={lat,lng,zoom:Math.round(4+t)};
          setView('map');
        }
      }
      // project destination name labels to screen (front hemisphere only)
      for(let i=0;i<labels.length;i++){
        const L=labels[i];
        const facing=L.pos.clone().normalize().dot(camera.position.clone().normalize());
        if(facing<0.12){ L.el.style.display='none'; continue; }
        const v=L.pos.clone().project(camera);
        L.el.style.display='';
        L.el.classList.toggle('sel', L.dst===selRef.current);
        const x=(v.x*0.5+0.5)*W, y=(-v.y*0.5+0.5)*H;
        L.el.style.transform='translate(-50%,-50%) translate('+x+'px,'+y+'px)';
        L.el.style.opacity=Math.min(1,(facing-0.12)/0.16);
      }
      controls.update();
      renderer.render(scene,camera);
    };
    // wait for the day texture, then reveal
    dayTex.image ? setReady(true) : (dayTex.onUpdate=()=>setReady(true));
    let readyTimer=setTimeout(()=>setReady(true),1800);
    animate();

    return ()=>{
      cancelAnimationFrame(raf); clearTimeout(readyTimer);
      window.removeEventListener("resize",onResize);
      dom.removeEventListener('wheel',onWheel);
      dom.removeEventListener('touchstart',onTouchStart);
      dom.removeEventListener('touchmove',onTouchMove);
      dom.removeEventListener('touchend',onTouchEnd);
      dom.removeEventListener('touchcancel',onTouchEnd);
      if(labelLayer.parentNode) labelLayer.parentNode.removeChild(labelLayer);
      controls.dispose(); renderer.dispose();
      if(renderer.domElement.parentNode) renderer.domElement.parentNode.removeChild(renderer.domElement);
    };
  },[]);

  // ---- UI handlers ---- selecting any place opens the flat street map there
  const goTo=(d)=>{
    setSelected(d);
    const z = (d.region==='UAE'||d.region==='International') ? 10 : 8;
    if(viewRef.current==='map' && mapRef.current){
      mapRef.current.flyTo([d.lat,d.lng], z, {duration:.9});
    } else {
      pendingCenterRef.current={lat:d.lat,lng:d.lng,zoom:z};
      setView('map');
    }
  };
  goToRef.current = goTo;
  const pick=(name)=>{ const d=window.DESTINATIONS.find(x=>x.n===name); if(d) goTo(d); };
  const onSearch=(val)=>{ setQuery(val);
    const d=window.DESTINATIONS.find(x=>x.n.toLowerCase()===val.toLowerCase());
    if(d){ goTo(d); setQuery(""); } };
  // shared by the +/− buttons: drives the globe in 3D view and Leaflet in map view
  const doZoom=(dir)=>{
    if(viewRef.current==='map' && mapRef.current){
      dir==='in' ? mapRef.current.zoomIn(1) : mapRef.current.zoomOut(1);
    }else if(zoomRef.current){
      zoomRef.current(dir==='in' ? 0.7 : 1.42);
    }
  };

  return (
    <div className={`globe-wrap ${view==='map'?'view-map':''}`}>
      <div className={`globe-canvas ${ready?'ready':''}`} ref={mountRef}></div>
      <div className={`globe-map ${view==='map'?'show':''}`}>
        <div className="globe-map-inner" ref={mapElRef}></div>
      </div>

      {view==='globe' && !ready && !failed && (
        <div className="globe-loading"><span className="globe-spin"></span><span>Spinning up the globe…</span></div>
      )}

      {/* search */}
      <div className="globe-search">
        <window.Icon name="search" size={16}/>
        <input list="globe-dests" placeholder="Search a destination…" value={query}
          onChange={e=>onSearch(e.target.value)} aria-label="Search destinations"/>
        <datalist id="globe-dests">
          {window.DESTINATIONS.map(d=><option key={d.n} value={d.n}/>)}
        </datalist>
      </div>

      {/* chips */}
      <div className="globe-chips-wrap">
        <button className="chip-arrow chip-arrow-l" aria-label="Previous destinations"
          onClick={()=>chipsRef.current&&chipsRef.current.scrollBy({left:-240,behavior:'smooth'})}>
          <window.Icon name="chevronLeft" size={18}/>
        </button>
        <div className="globe-chips" ref={chipsRef} tabIndex={0} role="listbox" aria-label="Quick destinations"
          onKeyDown={e=>{
            if(e.key==='ArrowRight'){ e.preventDefault(); chipsRef.current.scrollBy({left:200,behavior:'smooth'}); }
            else if(e.key==='ArrowLeft'){ e.preventDefault(); chipsRef.current.scrollBy({left:-200,behavior:'smooth'}); }
          }}>
          {window.GLOBE_CHIPS.map(c=>(
            <button key={c} className="globe-chip" onClick={()=>pick(c)}>{c}</button>
          ))}
        </div>
        <button className="chip-arrow chip-arrow-r" aria-label="More destinations"
          onClick={()=>chipsRef.current&&chipsRef.current.scrollBy({left:240,behavior:'smooth'})}>
          <window.Icon name="chevronRight" size={18}/>
        </button>
      </div>

      {/* zoom controls: glass pill, bottom-right on mobile, works in both views */}
      <div className="globe-zoom" role="group" aria-label="Zoom controls">
        <button className="gz-btn" type="button" aria-label="Zoom in" onClick={()=>doZoom('in')}><window.Icon name="plus" size={20}/></button>
        <span className="gz-sep" aria-hidden="true"></span>
        <button className="gz-btn" type="button" aria-label="Zoom out" onClick={()=>doZoom('out')}><window.Icon name="minus" size={20}/></button>
      </div>

      {/* info card */}
      {selected && (
        <div className="globe-card" key={selected.n}>
          <button className="gc-close" onClick={()=>setSelected(null)} aria-label="Close"><window.Icon name="close" size={18}/></button>
          <span className="gc-region mono">{selected.region}</span>
          <h3 className="gc-name">{selected.n}</h3>
          <p className="gc-desc">{selected.d}</p>
          <div className="gc-block">
            <span className="gc-h mono">Popular attractions</span>
            <ul className="gc-see">{selected.see.map(s=><li key={s}><window.Icon name="pin" size={12}/> {s}</li>)}</ul>
          </div>
          <div className="gc-block gc-best">
            <span className="gc-h mono">Best time to visit</span>
            <span className="gc-best-v">{selected.best}</span>
          </div>
          <div className="gc-block">
            <span className="gc-h mono">Tour packages</span>
            <div className="gc-pkgs">{selected.pkg.map(p=><span key={p} className="gc-pkg">{p}</span>)}</div>
          </div>
          <button className="gc-cta" onClick={()=>window.openWA(`Hi LaLaLand! I'd like to explore tour packages for ${selected.n}. Could you share the options and pricing?`)}>
            Explore Package <window.Icon name="arrow" size={16} className="arrow"/>
          </button>
        </div>
      )}
    </div>
  );
};

/* ============================================================
   GLOBE STYLES
   ============================================================ */
(function(){
  if(document.getElementById('globe-style'))return;
  const s=document.createElement('style');s.id='globe-style';
  s.textContent=`
.globe-wrap{position:relative;width:100%;height:clamp(440px,62vh,640px);background:transparent;isolation:isolate}
.globe-canvas{position:absolute;inset:0;opacity:0;transition:opacity 1.1s var(--ease-out)}
.globe-canvas.ready{opacity:1}
.globe-canvas canvas{display:block;touch-action:none;cursor:grab}
.globe-canvas canvas:active{cursor:grabbing}

/* destination name placeholders */
.globe-labels{position:absolute;inset:0;z-index:4;pointer-events:none;overflow:hidden}
.globe-label{position:absolute;left:0;top:0;padding:6px;pointer-events:auto;
  cursor:pointer;transition:opacity .2s}
.gl-dot{display:block;width:9px;height:9px;border-radius:50%;background:var(--accent-blue);border:2px solid #fff;
  box-shadow:0 1px 5px rgba(0,0,0,.4);transition:transform .25s var(--ease-out)}
.globe-label.feat .gl-dot{width:11px;height:11px;background:var(--terra)}
.gl-name{position:absolute;left:calc(100% - 2px);top:50%;font-size:12px;font-weight:600;color:#fff;
  background:rgba(16,24,52,.6);backdrop-filter:blur(4px);padding:3px 9px;border-radius:100px;
  text-shadow:0 1px 3px rgba(0,0,0,.5);white-space:nowrap;
  opacity:0;transform:translateY(-50%) translateX(-5px);transition:opacity .25s,transform .25s;pointer-events:none}
.globe-label.sel .gl-name,.globe-label:hover .gl-name{opacity:1;transform:translateY(-50%)}
.globe-label:hover{z-index:6}
.globe-label:hover .gl-dot{transform:scale(1.3)}
.globe-label.sel .gl-dot{transform:scale(1.3);box-shadow:0 0 0 3px rgba(255,255,255,.35),0 1px 5px rgba(0,0,0,.4)}
.globe-loading{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;
  color:var(--moss);font-size:13px;letter-spacing:.04em;font-family:'JetBrains Mono',monospace}
.globe-spin{width:30px;height:30px;border-radius:50%;border:2px solid rgba(53,103,230,.2);border-top-color:var(--accent-blue);
  animation:gspin .9s linear infinite}
@keyframes gspin{to{transform:rotate(360deg)}}
.globe-tooltip{position:absolute;left:0;top:0;pointer-events:none;opacity:0;transition:opacity .2s;
  background:var(--cream);color:var(--ink);border:1px solid var(--line);
  padding:5px 12px;border-radius:100px;font-size:12px;font-weight:600;white-space:nowrap;z-index:6;
  box-shadow:var(--shadow-sm)}

/* search */
.globe-search{position:absolute;top:10px;left:50%;transform:translateX(-50%);width:min(330px,86%);z-index:5;
  display:flex;align-items:center;gap:9px;padding:11px 16px;border-radius:100px;
  background:var(--cream);backdrop-filter:blur(8px);border:1px solid var(--line);box-shadow:var(--shadow-sm);color:var(--ink)}
.globe-search svg{color:var(--accent-blue);flex:0 0 auto}
.globe-search input{flex:1;min-width:0;border:none;background:transparent;outline:none;color:var(--ink);
  font-family:inherit;font-size:14px;font-weight:500}
.globe-search input::placeholder{color:var(--moss)}

/* chips */
.globe-chips-wrap{position:absolute;left:0;right:0;bottom:20px;z-index:5;
  display:flex;align-items:center;justify-content:center;gap:8px;padding:0 12px}
.globe-chips{display:flex;gap:8px;overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch;
  scroll-behavior:smooth;max-width:min(620px,72%);flex-wrap:nowrap;outline:none}
.globe-chips::-webkit-scrollbar{display:none}
.chip-arrow{flex:0 0 auto;width:34px;height:34px;border-radius:50%;display:flex;align-items:center;justify-content:center;
  background:var(--cream);border:1px solid var(--line-strong);color:var(--ink);box-shadow:var(--shadow-sm);
  transition:background .3s,color .3s,transform .12s}
.chip-arrow:hover{background:var(--accent-blue);color:#fff;border-color:transparent}
.chip-arrow:active{transform:scale(.9)}
.globe-chip{flex:0 0 auto;padding:8px 15px;border-radius:100px;font-size:13px;font-weight:600;white-space:nowrap;
  background:var(--cream);border:1px solid var(--line-strong);color:var(--ink);box-shadow:var(--shadow-sm);
  transition:all .35s var(--ease-out)}
.globe-chip:hover{background:var(--accent-blue);color:#fff;border-color:transparent;transform:translateY(-2px)}
@media(max-width:560px){.chip-arrow{display:none}.globe-chips{max-width:92%}}

.globe-hint{display:none}

/* zoom controls: glassmorphism pill (matches search / chips theme) */
.globe-zoom{position:absolute;right:14px;bottom:88px;z-index:7;display:flex;flex-direction:column;
  width:46px;border-radius:15px;overflow:hidden;
  background:rgba(251,249,244,.6);backdrop-filter:blur(14px) saturate(150%);-webkit-backdrop-filter:blur(14px) saturate(150%);
  border:1px solid var(--line);box-shadow:var(--shadow)}
.gz-btn{display:flex;align-items:center;justify-content:center;height:46px;color:var(--ink);
  transition:background .25s,color .25s,transform .12s;-webkit-tap-highlight-color:transparent}
.gz-btn:hover{background:rgba(47,107,240,.1);color:var(--accent-blue)}
.gz-btn:active{transform:scale(.88)}
.gz-sep{height:1px;background:var(--line)}
/* desktop: float at the vertically-centred left edge, clear of the chips row + hint */
@media(min-width:561px){.globe-zoom{left:14px;right:auto;top:50%;bottom:auto;transform:translateY(-50%)}}
@media(max-width:560px){.globe-zoom{right:12px;bottom:104px;top:auto;transform:none;width:48px}.gz-btn{height:48px}}

/* info card (light, compact: bounded so it never overlaps search / chips / hint) */
.globe-card{position:absolute;top:60px;right:10px;bottom:86px;width:268px;z-index:8;
  background:var(--cream);backdrop-filter:blur(8px);border:1px solid var(--line);border-radius:16px;
  padding:18px 18px 16px;color:var(--ink);overflow-y:auto;animation:gcin .45s var(--ease-out);box-shadow:var(--shadow-lg)}
.globe-card::-webkit-scrollbar{width:5px}.globe-card::-webkit-scrollbar-thumb{background:var(--line-strong);border-radius:4px}
@keyframes gcin{from{opacity:0;transform:translateX(16px)}to{opacity:1;transform:none}}
.gc-close{position:absolute;top:12px;right:12px;width:28px;height:28px;border-radius:50%;color:var(--ink);
  border:1px solid var(--line-strong);display:flex;align-items:center;justify-content:center;transition:all .35s;background:var(--cream)}
.gc-close:hover{background:var(--paper-2);transform:rotate(90deg)}
.gc-region{font-size:9.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--terra)}
.gc-name{font-size:21px;font-weight:800;letter-spacing:-.02em;margin:4px 28px 0 0;color:var(--ink);line-height:1.1}
.gc-desc{font-size:12.5px;line-height:1.5;color:var(--ink-soft);margin-top:8px}
.gc-block{margin-top:13px;display:flex;flex-direction:column;gap:7px}
.gc-h{font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--moss)}
.gc-see{list-style:none;display:flex;flex-direction:column;gap:5px}
.gc-see li{display:flex;align-items:center;gap:7px;font-size:12.5px;font-weight:500}
.gc-see li svg{color:var(--terra);flex:0 0 auto}
.gc-best{flex-direction:row;justify-content:space-between;align-items:center;gap:10px;
  background:var(--paper-2);border:1px solid var(--line);border-radius:10px;padding:9px 12px;margin-top:13px}
.gc-best-v{font-size:12.5px;font-weight:700;color:var(--ink)}
.gc-pkgs{display:flex;flex-wrap:wrap;gap:6px}
.gc-pkg{font-size:11px;font-weight:600;padding:5px 10px;border-radius:100px;background:var(--paper-2);
  border:1px solid var(--line);color:var(--ink-soft)}
.gc-cta{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;margin-top:15px;padding:11px;
  border-radius:11px;background:var(--grad-accent);color:#fff;font-weight:700;font-size:13px;
  transition:filter .35s,transform .35s var(--ease-out)}
.gc-cta:hover{filter:saturate(1.3) brightness(1.07);transform:translateY(-2px)}
.gc-cta:hover .arrow{transform:translateX(3px)}
.gc-cta .arrow{transition:transform .35s}
@media(max-width:560px){
  .globe-card{top:auto;right:8px;left:8px;bottom:8px;width:auto;max-height:58%}
  .globe-search{width:80%;top:56px}
}

/* ---- flat map (Leaflet / OpenStreetMap) ---- */
.globe-map{position:absolute;inset:0;z-index:1;opacity:0;pointer-events:none;
  border-radius:inherit;overflow:hidden;transition:opacity .5s var(--ease-out)}
.globe-map.show{opacity:1;pointer-events:auto}
.globe-map-inner{position:absolute;inset:0}
.globe-wrap.view-map .globe-canvas{display:none}
.leaflet-container{background:#a9d6f0;font-family:inherit}
.map-pin-wrap{background:none;border:none;display:flex;align-items:center;justify-content:center;cursor:pointer}
.map-pin-dot{display:block;width:12px;height:12px;border-radius:50%;background:var(--accent-blue);
  border:2px solid #fff;box-shadow:0 1px 5px rgba(0,0,0,.45);transition:transform .2s var(--ease-out)}
.map-pin-dot.feat{width:14px;height:14px;background:var(--terra)}
.leaflet-marker-icon:hover .map-pin-dot{transform:scale(1.3)}
.map-pin-dot.sel{transform:scale(1.45);box-shadow:0 0 0 3px rgba(255,255,255,.55),0 1px 6px rgba(0,0,0,.45)}
.map-tip{background:var(--ink);color:#fff;border:none;border-radius:100px;padding:3px 11px;
  font-size:12px;font-weight:600;box-shadow:var(--shadow-sm);white-space:nowrap}
/* clicked-place label popup */
.place-popup .leaflet-popup-content-wrapper{background:var(--cream);color:var(--ink);
  border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow);padding:2px 4px}
.place-popup .leaflet-popup-content{margin:9px 13px;display:flex;flex-direction:column;gap:2px;line-height:1.25}
.place-popup .leaflet-popup-tip{background:var(--cream);border:1px solid var(--line)}
.place-popup .leaflet-popup-close-button{color:var(--moss);padding:5px 7px 0 0}
.place-name{font-size:13.5px;font-weight:800;letter-spacing:-.01em;color:var(--ink)}
.place-sub{font-size:11px;color:var(--moss);font-weight:500}
.place-poi{margin-top:8px;padding-top:8px;border-top:1px solid var(--line);display:flex;flex-direction:column;gap:6px}
.poi-h{font-size:9px;letter-spacing:.14em;text-transform:uppercase;color:var(--terra);font-weight:700}
.poi-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:5px}
.poi-list li{display:flex;align-items:baseline;justify-content:space-between;gap:10px;font-size:12px}
.poi-n{font-weight:600;color:var(--ink)}
.poi-km{font-size:10.5px;color:var(--moss);font-weight:600;white-space:nowrap;flex:0 0 auto}
.poi-load,.poi-empty{font-size:11px;color:var(--moss);font-style:italic}
.poi-ai{font-size:9.5px;letter-spacing:.04em;color:var(--terra);font-weight:700;margin-top:2px}
.map-tip::before{border-top-color:var(--ink)!important}
.leaflet-control-zoom a{color:var(--ink)!important}
.leaflet-control-attribution{font-size:10px;background:rgba(255,255,255,.82)!important}

/* back-to-globe pill (map view) */
.globe-back{position:absolute;top:10px;left:14px;z-index:7;display:inline-flex;align-items:center;gap:7px;
  padding:8px 16px;border-radius:100px;background:var(--cream);border:1px solid var(--line);box-shadow:var(--shadow-sm);
  font-size:12.5px;font-weight:700;color:var(--ink);transition:background .25s,transform .25s}
.globe-back:hover{background:#fff;transform:translateY(-1px)}
.globe-back .back-arrow{transform:rotate(180deg);color:var(--accent-blue)}
`;
  document.head.appendChild(s);
})();
