/* ===========================================================================
   EcoEsperanza — Mapa de Sensores Ambientales
   A full-screen environmental-sensor explorer inspired by openSenseMap, with
   an original EcoEsperanza identity. React (inline) + Leaflet + Leaflet.marker-
   cluster. Adapted from the requested React/Vite/Tailwind/React-Leaflet stack
   to this project's runtime (inline Babel + design-system CSS tokens).

   DATA: real openSenseMap public API with a 24h localStorage cache + graceful
   fallback to a worldwide seed dataset (so the UI is always complete, and the
   live integration is one toggle away). The cache TTL is a single constant —
   change CACHE_TTL to go hourly, or swap loadStations() for a backend call.
   =========================================================================== */

const { useState, useEffect, useMemo, useRef, useCallback } = React;
const Icon = ({ n, ...p }) => <i data-lucide={n} {...p}></i>;
function drawIcons() { if (window.lucide) window.lucide.createIcons(); }

/* ---- Config -------------------------------------------------------------- */
const OSM_API = 'https://api.opensensemap.org';
const CACHE_KEY = 'eco_sensormap_v1';
const CACHE_TTL = 24 * 60 * 60 * 1000;          // 24h — set to 60*60*1000 for hourly
// EcoEsperanza's own senseBoxes (openSenseMap box ids). These are fetched
// individually and highlighted in gold, even if outside the bulk response.
// Add a station: paste its id from opensensemap.org/explore/<ID>
const ECO_BOX_IDS = [
  '6a05f0a0d46e9100071fef83',   // EPA1
];
const ACTIVE_WINDOW = 24 * 60 * 60 * 1000;      // a station is "activa" if it reported < 24h ago
// Live mode pulls the WHOLE openSenseMap network (no bbox). Clustering renders it.
const LIVE_CAP = 15000;                          // safety cap on rendered live boxes

/* ---- Sensor type normalisation ------------------------------------------- */
const SENSOR_TYPES = [
  { key: 'temp', label: 'Temperatura', unit: '°C', icon: 'thermometer', re: /temp/i },
  { key: 'hum',  label: 'Humedad',     unit: '%',  icon: 'droplets',    re: /hum|feucht/i },
  { key: 'pm25', label: 'PM2.5',       unit: 'µg/m³', icon: 'wind',     re: /pm.?2\.?5|pm25/i },
  { key: 'pm10', label: 'PM10',        unit: 'µg/m³', icon: 'cloud',    re: /pm.?10/i },
  { key: 'pres', label: 'Presión',     unit: 'hPa', icon: 'gauge',      re: /press|druck|presi/i },
];
const typeOf = (title, unit) => {
  const t = `${title || ''} ${unit || ''}`;
  const hit = SENSOR_TYPES.find(s => s.re.test(t));
  return hit ? hit.key : null;
};
const typeMeta = (key) => SENSOR_TYPES.find(s => s.key === key) || { label: key, unit: '', icon: 'activity' };

/* ---- Air-quality colour by PM2.5 (µg/m³) --------------------------------- */
const pmBand = (pm) => pm == null ? { label: 'Sin dato', hex: '#93A399' }
  : pm <= 12 ? { label: 'Buena', hex: '#2BA557' }
  : pm <= 35 ? { label: 'Moderada', hex: '#E9A81C' }
  : pm <= 55 ? { label: 'Dañina (sensibles)', hex: '#C98A00' }
  : { label: 'Dañina', hex: '#C0392B' };
const pmOf = (st) => { const s = st.sensors.find(x => x.type === 'pm25'); return s && s.value != null ? s.value : null; };
const isActive = (st) => st.updatedAt != null && (Date.now() - st.updatedAt) < ACTIVE_WINDOW;

function timeAgo(ts) {
  if (ts == null) return 'sin datos';
  const s = Math.max(0, Math.round((Date.now() - ts) / 1000));
  if (s < 60) return `hace ${s}s`;
  const m = Math.round(s / 60); if (m < 60) return `hace ${m} min`;
  const h = Math.round(m / 60); if (h < 24) return `hace ${h} h`;
  return `hace ${Math.round(h / 24)} d`;
}

/* ---- Coarse country lookup for live boxes (no country field in OSM) ------ */
const COUNTRY_BOXES = [
  ['Colombia',  -79, -4, -67, 13], ['Brasil', -74, -34, -34, 5], ['Argentina', -73, -55, -53, -21],
  ['España',    -10, 36, 4, 44],   ['Alemania', 5.8, 47, 15.1, 55], ['Francia', -5, 41, 9, 51],
  ['Reino Unido', -8, 50, 2, 59],  ['México', -118, 14, -86, 33],  ['Perú', -82, -19, -68, 0],
  ['Chile',     -76, -56, -66, -17], ['Ecuador', -81, -5, -75, 2], ['EE. UU.', -125, 25, -66, 49],
];
const countryAt = (lat, lng) => {
  const hit = COUNTRY_BOXES.find(([, w, s, e, n]) => lng >= w && lng <= e && lat >= s && lat <= n);
  return hit ? hit[0] : 'Otro';
};

/* =========================================================================
   SEED dataset — worldwide, deterministic. Includes EcoEsperanza's own 7.
   ========================================================================= */
function rng(seed) { let s = seed % 2147483647; if (s <= 0) s += 2147483646; return () => (s = (s * 16807) % 2147483647) / 2147483647; }
const SEED_CITIES = [
  ['Bogotá', 'Colombia', 4.61, -74.08, 6], ['Medellín', 'Colombia', 6.25, -75.56, 4], ['Cali', 'Colombia', 3.45, -76.53, 3],
  ['São Paulo', 'Brasil', -23.55, -46.63, 8], ['Rio de Janeiro', 'Brasil', -22.91, -43.20, 5], ['Brasília', 'Brasil', -15.79, -47.88, 3],
  ['Provincia de Buenos Aires', 'Argentina', -34.60, -58.38, 6], ['Córdoba', 'Argentina', -31.42, -64.18, 3],
  ['Santiago', 'Chile', -33.45, -70.65, 5], ['Lima', 'Perú', -12.05, -77.04, 4], ['Quito', 'Ecuador', -0.18, -78.47, 3],
  ['Ciudad de México', 'México', 19.43, -99.13, 8], ['Guadalajara', 'México', 20.67, -103.35, 4],
  ['Nueva York', 'EE. UU.', 40.71, -74.00, 6], ['Los Ángeles', 'EE. UU.', 34.05, -118.24, 5], ['Chicago', 'EE. UU.', 41.88, -87.63, 4],
  ['Toronto', 'Canadá', 43.65, -79.38, 4],
  ['Madrid', 'España', 40.42, -3.70, 6], ['Barcelona', 'España', 41.39, 2.17, 5], ['Valencia', 'España', 39.47, -0.38, 5],
  ['Berlín', 'Alemania', 52.52, 13.40, 10], ['Hamburgo', 'Alemania', 53.55, 10.00, 6], ['Múnich', 'Alemania', 48.14, 11.58, 6],
  ['París', 'Francia', 48.85, 2.35, 6], ['Londres', 'Reino Unido', 51.51, -0.13, 6], ['Ámsterdam', 'Países Bajos', 52.37, 4.90, 4],
  ['Roma', 'Italia', 41.90, 12.50, 4], ['Lisboa', 'Portugal', 38.72, -9.14, 3],
  ['El Cairo', 'Egipto', 30.04, 31.24, 3], ['Lagos', 'Nigeria', 6.52, 3.38, 3], ['Nairobi', 'Kenia', -1.29, 36.82, 3],
  ['Ciudad del Cabo', 'Sudáfrica', -33.92, 18.42, 3],
  ['Delhi', 'India', 28.61, 77.21, 6], ['Bombay', 'India', 19.08, 72.88, 4], ['Bangkok', 'Tailandia', 13.76, 100.50, 4],
  ['Yakarta', 'Indonesia', -6.21, 106.85, 4], ['Tokio', 'Japón', 35.68, 139.69, 6], ['Seúl', 'Corea del Sur', 37.57, 126.98, 4],
  ['Pekín', 'China', 39.90, 116.40, 5], ['Sídney', 'Australia', -33.87, 151.21, 4],
];
function buildSeed() {
  const out = [];
  // EcoEsperanza's own network (highlighted)
  const eco = [
    ['EPA1 · Provincia de Buenos Aires', 'Argentina', -38.821392, -61.373956, 24, 18.5, 64],
    ['Ebog1 · Bogotá', 'Colombia', 4.658722, -74.059544, 20, 14.6, 76],
    ['Ebog2 · Bogotá', 'Colombia', 4.659200, -74.058900, 23, 14.4, 77],
    ['Eval1 · Valencia', 'España', 39.466978, -0.355008, 10, 21.8, 66],
    ['Eval2 · Valencia', 'España', 39.467450, -0.354400, 13, 21.6, 67],
    ['Esao1 · São Paulo', 'Brasil', -23.588842, -46.730317, 17, 26.5, 60],
    ['Esao2 · São Paulo', 'Brasil', -23.588300, -46.729700, 19, 26.7, 59],
    ['Bogotá · Centro', 'Colombia', 4.6097, -74.0817, 22, 14.2, 78],
    ['Bogotá · Kennedy', 'Colombia', 4.6280, -74.1500, 31, 15.1, 74],
    ['São Paulo · Sé', 'Brasil', -23.5505, -46.6333, 18, 26.8, 61],
    ['São Paulo · Pinheiros', 'Brasil', -23.5670, -46.7020, 15, 27.4, 58],
    ['Valencia · Ciutat Vella', 'España', 39.4699, -0.3763, 9, 22.1, 65],
    ['Valencia · Cabanyal', 'España', 39.4720, -0.3290, 12, 22.6, 69],
    ['Provincia de Buenos Aires · Centro', 'Argentina', -34.6037, -58.3816, 29, 19.4, 70],
  ];
  eco.forEach((e, i) => out.push({
    id: 'eco-' + i, name: e[0], country: e[1], lat: e[2], lng: e[3], eco: true,
    updatedAt: Date.now() - (3 + i) * 60 * 1000,
    sensors: [
      { type: 'temp', value: e[5] }, { type: 'hum', value: e[6] },
      { type: 'pm25', value: e[4] }, { type: 'pm10', value: Math.round(e[4] * 1.6) },
      { type: 'pres', value: 1010 + (i % 5) },
    ],
  }));
  // Surrounding community/citizen stations
  SEED_CITIES.forEach((c, ci) => {
    const r = rng(ci + 7);
    for (let i = 0; i < c[4]; i++) {
      const lat = c[2] + (r() - 0.5) * 0.22, lng = c[3] + (r() - 0.5) * 0.22;
      const has = (p) => r() < p;
      const sensors = [];
      sensors.push({ type: 'temp', value: +(8 + r() * 26).toFixed(1) });
      sensors.push({ type: 'hum', value: Math.round(40 + r() * 50) });
      if (has(0.85)) sensors.push({ type: 'pm25', value: Math.round(4 + r() * 60) });
      if (has(0.6)) sensors.push({ type: 'pm10', value: Math.round(8 + r() * 80) });
      if (has(0.5)) sensors.push({ type: 'pres', value: Math.round(995 + r() * 30) });
      const inactive = r() < 0.16;
      out.push({
        id: `seed-${ci}-${i}`, name: `${c[0]} #${i + 1}`, country: c[1], lat, lng, eco: false,
        updatedAt: inactive ? Date.now() - (ACTIVE_WINDOW + r() * 5 * ACTIVE_WINDOW) : Date.now() - r() * ACTIVE_WINDOW * 0.9,
        sensors,
      });
    }
  });
  return out;
}
const SEED = buildSeed();

/* =========================================================================
   openSenseMap service + cache
   ========================================================================= */
function normalizeBox(b) {
  const coords = (b.currentLocation && b.currentLocation.coordinates)
    || (b.loc && b.loc[0] && b.loc[0].geometry && b.loc[0].geometry.coordinates);
  if (!coords) return null;
  const [lng, lat] = coords;
  const sensors = (b.sensors || []).map(s => ({
    type: typeOf(s.title, s.unit), title: s.title, unit: s.unit,
    value: s.lastMeasurement ? +parseFloat(s.lastMeasurement.value).toFixed(1) : null,
  })).filter(s => s.type);
  return {
    id: b._id, name: b.name || 'senseBox', country: countryAt(lat, lng), lat, lng, eco: ECO_BOX_IDS.includes(b._id),
    updatedAt: b.lastMeasurementAt ? Date.parse(b.lastMeasurementAt) : null, sensors,
  };
}
/* Fetch the pinned EcoEsperanza boxes by id (real name + coords, highlighted). */
async function fetchPinned() {
  const results = await Promise.all(ECO_BOX_IDS.map(async (id) => {
    try {
      const res = await fetch(`${OSM_API}/boxes/${id}?format=json`);
      if (!res.ok) return null;
      const b = await res.json();
      const st = normalizeBox(b);
      if (st) st.eco = true;
      return st;
    } catch { return null; }
  }));
  return results.filter(Boolean);
}

async function fetchLive() {
  const ctrl = new AbortController();
  const to = setTimeout(() => ctrl.abort(), 15000);
  try {
    // Whole network, with last measurements. Cached 24h so this runs once a day.
    const res = await fetch(`${OSM_API}/boxes?format=json&full=true`, { signal: ctrl.signal });
    if (!res.ok) throw new Error('osm ' + res.status);
    const boxes = await res.json();
    const norm = boxes.map(normalizeBox).filter(Boolean).filter(s => s.sensors.length).slice(0, LIVE_CAP);
    if (!norm.length) throw new Error('empty');
    // pin EcoEsperanza's own boxes (real data) at the front, de-duplicated
    const pinned = await fetchPinned();
    const pinnedIds = new Set(pinned.map(s => s.id));
    return pinned.concat(norm.filter(s => !pinnedIds.has(s.id)));
  } finally { clearTimeout(to); }
}
const readCache = () => { try { const c = JSON.parse(localStorage.getItem(CACHE_KEY)); return c && Array.isArray(c.data) ? c : null; } catch { return null; } };
const writeCache = (data) => { try { localStorage.setItem(CACHE_KEY, JSON.stringify({ ts: Date.now(), data })); } catch {} };

/* =========================================================================
   Map (Leaflet + clustering) — imperative, synced to props via effects
   ========================================================================= */
function SensorMap({ stations, selectedId, onSelect, registerFocus }) {
  const elRef = useRef(null), mapRef = useRef(null), clusterRef = useRef(null), marksRef = useRef({});

  useEffect(() => {
    if (!window.L || !elRef.current || mapRef.current) return;
    const map = window.L.map(elRef.current, { scrollWheelZoom: true, worldCopyJump: true }).setView([15, -30], 2);
    window.L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18, attribution: '&copy; OpenStreetMap' }).addTo(map);
    const cluster = window.L.markerClusterGroup({ maxClusterRadius: 48, showCoverageOnHover: false, chunkedLoading: true });
    map.addLayer(cluster);
    mapRef.current = map; clusterRef.current = cluster;
    setTimeout(() => map.invalidateSize(), 200);
    return () => { map.remove(); mapRef.current = null; };
  }, []);

  // rebuild markers when the filtered list changes
  useEffect(() => {
    const L = window.L, cluster = clusterRef.current, map = mapRef.current;
    if (!L || !cluster) return;
    cluster.clearLayers(); marksRef.current = {};
    stations.forEach(st => {
      const pm = pmOf(st); const band = pmBand(pm);
      const color = st.eco ? '#E9A81C' : (isActive(st) ? band.hex : '#93A399');
      const m = L.circleMarker([st.lat, st.lng], {
        radius: st.eco ? 9 : 7, color: '#fff', weight: st.eco ? 3 : 2, fillColor: color, fillOpacity: 1,
      });
      m.bindPopup(popupHTML(st));
      m.bindTooltip(st.name + (st.eco ? ' · EcoEsperanza' : ''), { direction: 'top', offset: [0, -6], className: 'sx-tt' });
      m.on('click', () => onSelect(st.id));
      marksRef.current[st.id] = m;
      cluster.addLayer(m);
    });
    if (stations.length && map) {
      const g = L.featureGroup(Object.values(marksRef.current));
      const b = g.getBounds(); if (b.isValid()) map.fitBounds(b, { padding: [40, 40], maxZoom: 6 });
    }
  }, [stations]);

  // expose a focus(id) fn to the parent (list click → fly + open popup)
  useEffect(() => {
    registerFocus((id) => {
      const m = marksRef.current[id], cluster = clusterRef.current, map = mapRef.current;
      if (!m || !cluster || !map) return;
      cluster.zoomToShowLayer(m, () => { map.setView(m.getLatLng(), Math.max(map.getZoom(), 11), { animate: true }); m.openPopup(); });
    });
  }, [registerFocus]);

  return <div ref={elRef} className="sx-map__el"></div>;
}
function popupHTML(st) {
  const rows = st.sensors.map(s => {
    const meta = typeMeta(s.type);
    return `<div class="sx-pop__s"><span class="sx-pop__t">${meta.label}</span><b>${s.value == null ? '—' : s.value} ${s.unit || meta.unit}</b></div>`;
  }).join('');
  return `<div class="sx-pop__name">${st.name}${st.eco ? '<span class="sx-pop__eco">EcoEsperanza</span>' : ''}</div>`
    + `<div class="sx-pop__meta">${st.country} · actualizado ${timeAgo(st.updatedAt)}</div>`
    + `<div class="sx-pop__sensors">${rows || '<span class="sx-pop__t">Sin sensores</span>'}</div>`;
}

/* =========================================================================
   Sidebar
   ========================================================================= */
function Sidebar({ stations, total, active, filters, setFilters, selectedId, onSelect, open }) {
  const countries = useMemo(() => [...new Set(SEED.concat([]).map(s => s.country))], []);
  const liveCountries = useMemo(() => [...new Set(stations.map(s => s.country))].sort(), [stations]);
  return (
    <aside className={`sx-side${open ? ' sx-side--open' : ''}`}>
      <div className="sx-side__pad">
        <div className="sx-stats">
          <div className="sx-stat"><div className="sx-stat__v">{total}</div><div className="sx-stat__l">Estaciones</div></div>
          <div className="sx-stat sx-stat--active"><div className="sx-stat__v">{active}</div><div className="sx-stat__l">Activas</div></div>
          <div className="sx-stat"><div className="sx-stat__v">{stations.length}</div><div className="sx-stat__l">En vista</div></div>
        </div>

        <label className="sx-search">
          <Icon n="search" />
          <input placeholder="Buscar estación…" value={filters.q}
            onChange={e => setFilters(f => ({ ...f, q: e.target.value }))} />
          {filters.q && <button onClick={() => setFilters(f => ({ ...f, q: '' }))} aria-label="Limpiar"><Icon n="x" /></button>}
        </label>

        <div className="sx-filters">
          <div>
            <div className="sx-flabel" style={{ marginBottom: '.4rem' }}>Tipo de sensor</div>
            <div className="sx-chips">
              {SENSOR_TYPES.map(s => (
                <button key={s.key} className={`sx-chip${filters.types.includes(s.key) ? ' sx-chip--on' : ''}`}
                  onClick={() => setFilters(f => ({ ...f, types: f.types.includes(s.key) ? f.types.filter(x => x !== s.key) : [...f.types, s.key] }))}>
                  <Icon n={s.icon} />{s.label}
                </button>
              ))}
            </div>
          </div>
          <div className="sx-row">
            <select className="sx-select" value={filters.country} onChange={e => setFilters(f => ({ ...f, country: e.target.value }))}>
              <option value="">Todos los países</option>
              {liveCountries.map(c => <option key={c} value={c}>{c}</option>)}
            </select>
          </div>
          <div className="sx-row" style={{ justifyContent: 'space-between', alignItems: 'center' }}>
            <span className="sx-flabel">Estado</span>
            <div className="sx-seg">
              {[['all', 'Todas'], ['active', 'Activas'], ['inactive', 'Inactivas']].map(([k, l]) =>
                <button key={k} className={filters.status === k ? 'on' : ''} onClick={() => setFilters(f => ({ ...f, status: k }))}>{l}</button>)}
            </div>
          </div>
        </div>
      </div>

      <div className="sx-listhead">
        <span>{stations.length} resultado{stations.length === 1 ? '' : 's'}</span>
        <span>Ordenado por cercanía de datos</span>
      </div>
      <div className="sx-list">
        {stations.length === 0 && <div className="sx-empty">No hay estaciones que coincidan con los filtros.</div>}
        {stations.slice(0, 300).map(st => {
          const pm = pmOf(st); const band = pmBand(pm);
          const color = st.eco ? '#E9A81C' : (isActive(st) ? band.hex : '#93A399');
          return (
            <div key={st.id} className={`sx-item${st.id === selectedId ? ' sx-item--on' : ''}`} onClick={() => onSelect(st.id)}>
              <span className="sx-item__dot" style={{ background: color }} />
              <div className="sx-item__main">
                <div className="sx-item__name">{st.name}</div>
                <div className="sx-item__meta">{st.country} · {st.sensors.length} sensores · {timeAgo(st.updatedAt)}</div>
              </div>
              {st.eco && <span className="sx-item__eco" title="Estación EcoEsperanza"><Icon n="sprout" /></span>}
            </div>
          );
        })}
      </div>
    </aside>
  );
}

/* =========================================================================
   App
   ========================================================================= */
function App() {
  const [all, setAll] = useState(SEED);
  const [source, setSource] = useState('muestra');   // 'muestra' | 'vivo'
  const [loading, setLoading] = useState(false);
  const [updatedAt, setUpdatedAt] = useState(Date.now());
  const [selectedId, setSelectedId] = useState(null);
  const [sideOpen, setSideOpen] = useState(false);
  const [dark, setDark] = useState(false);
  const [filters, setFilters] = useState({ q: '', types: [], country: '', status: 'all' });
  const focusRef = useRef(() => {});

  const loadLive = useCallback(async (force) => {
    const cached = readCache();
    if (!force && cached && Date.now() - cached.ts < CACHE_TTL) {
      setAll(cached.data); setSource('vivo'); setUpdatedAt(cached.ts); return;
    }
    setLoading(true);
    try {
      const data = await fetchLive();
      writeCache(data); setAll(data); setSource('vivo'); setUpdatedAt(Date.now());
    } catch (e) {
      setAll(SEED); setSource('muestra'); setUpdatedAt(Date.now());
    } finally { setLoading(false); }
  }, []);

  // On mount: use fresh cache, else attempt live in the background (seed shows instantly).
  useEffect(() => {
    const cached = readCache();
    if (cached && Date.now() - cached.ts < CACHE_TTL) { setAll(cached.data); setSource('vivo'); setUpdatedAt(cached.ts); }
    else { loadLive(false); }
  }, [loadLive]);

  useEffect(() => { document.documentElement.dataset.theme = dark ? 'dark' : ''; }, [dark]);
  useEffect(() => { drawIcons(); });

  const total = all.length;
  const active = useMemo(() => all.filter(isActive).length, [all]);

  const filtered = useMemo(() => {
    const q = filters.q.trim().toLowerCase();
    return all.filter(st => {
      if (q && !st.name.toLowerCase().includes(q) && !st.country.toLowerCase().includes(q)) return false;
      if (filters.country && st.country !== filters.country) return false;
      if (filters.status === 'active' && !isActive(st)) return false;
      if (filters.status === 'inactive' && isActive(st)) return false;
      if (filters.types.length && !filters.types.every(t => st.sensors.some(s => s.type === t))) return false;
      return true;
    });
  }, [all, filters]);

  const onSelect = useCallback((id) => { setSelectedId(id); focusRef.current(id); if (window.innerWidth <= 860) setSideOpen(false); }, []);
  const registerFocus = useCallback((fn) => { focusRef.current = fn; }, []);

  return (
    <div className="sx-root">
      <header className="sx-top">
        <div className="sx-top__brand">
          <img src="assets/logo-mark.png" alt="" />
          <div className="sx-top__titles">
            <span className="sx-top__t">Mapa de Sensores Ambientales</span>
            <span className="sx-top__s">Red global de monitoreo · datos abiertos · EcoEsperanza</span>
          </div>
        </div>
        <div className="sx-top__actions">
          <span className={`sx-src sx-src--${source === 'vivo' ? 'live' : 'seed'}`}>
            <span className="sx-src__dot" />{source === 'vivo' ? 'En vivo' : 'Muestra'} · {timeAgo(updatedAt)}
          </span>
          <button className="sx-ibtn" onClick={() => loadLive(true)} aria-label="Actualizar datos" title="Actualizar (API openSenseMap)">
            <Icon n="refresh-cw" className={loading ? 'spin' : ''} />
          </button>
          <button className="sx-ibtn" onClick={() => setDark(d => !d)} aria-label="Cambiar tema" title="Modo claro / oscuro">
            <Icon n={dark ? 'sun' : 'moon'} />
          </button>
          <a className="sx-back" href="index.html"><Icon n="arrow-left" />Volver al sitio</a>
        </div>
      </header>

      <div className="sx-body">
        <Sidebar stations={filtered} total={total} active={active} filters={filters} setFilters={setFilters}
          selectedId={selectedId} onSelect={onSelect} open={sideOpen} />
        <div className="sx-map">
          <button className="sx-sidetoggle" onClick={() => setSideOpen(o => !o)}>
            {sideOpen ? 'Ocultar lista' : `Ver ${filtered.length} estaciones`}
          </button>
          <SensorMap stations={filtered} selectedId={selectedId} onSelect={onSelect} registerFocus={registerFocus} />
          <div className="sx-legend">
            <b>Calidad del aire (PM2.5)</b>
            <div className="sx-legend__row"><span className="sx-legend__sw" style={{ background: '#2BA557' }} />Buena ≤ 12</div>
            <div className="sx-legend__row"><span className="sx-legend__sw" style={{ background: '#E9A81C' }} />Moderada ≤ 35</div>
            <div className="sx-legend__row"><span className="sx-legend__sw" style={{ background: '#C0392B' }} />Dañina &gt; 55</div>
            <div className="sx-legend__row"><span className="sx-legend__sw" style={{ background: '#E9A81C', boxShadow: '0 0 0 2px #fff, 0 0 0 3px #C98A00' }} />EcoEsperanza</div>
          </div>
          {loading && <div className="sx-loading"><div className="sx-loading__box"><Icon n="loader" />Consultando openSenseMap…</div></div>}
        </div>
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
setTimeout(drawIcons, 60); setTimeout(drawIcons, 300);
