/* global React, ReactDOM, WeatherData, StationData, Astronomy, Dashboard, WIDGET_CATALOG,
   TweaksPanel, useTweaks, TweakSection, TweakToggle, TweakColor, TweakRadio, TweakSlider, TweakButton, TweakSelect,
   CommandPalette, showToast, Units */
const { useState, useEffect, useMemo, useCallback, useRef } = React;

function msUntilNextHour() {
  const now = new Date();
  const next = new Date(now); next.setMinutes(0,0,0); next.setHours(now.getHours()+1);
  return next.getTime() - now.getTime();
}
// Stanice měří po 5 min a server stáhne nový záznam nejdřív 30 s po
// pětiminutovce — ptáme se 45 s po ní (stejně jako stránka stanice).
// Ikona v záložce prohlížeče = aktuální meteoikona (jako v záhlaví).
function setWeatherFavicon(src) {
  if (!src) return;
  const href = new URL(src, location.href).href;
  const link = document.querySelector('link[rel="icon"]') || document.head.appendChild(document.createElement('link'));
  if (link.href === href) return;
  link.rel = 'icon'; link.type = 'image/png'; link.href = href;
}

function msUntilNext5() {
  const now = Date.now(), P = 5 * 60e3, OFF = 45e3;
  let next = Math.floor(now / P) * P + OFF;
  if (next < now + 5e3) next += P;
  return next - now;
}

// ── Config export / import ───────────────────────────────────────────
// Dump every `pm_*` localStorage key (layout, tweaks, custom presets,
// station prefs, chart cfg, etc.) into a JSON file the user can save and
// re-import on another machine or to roll back changes.
function exportConfig() {
  const data = {};
  for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    if (!key || !key.startsWith('pm_')) continue;
    const raw = localStorage.getItem(key);
    try { data[key] = JSON.parse(raw); }
    catch { data[key] = raw; }
  }
  const payload = {
    app: 'pocasimeteo',
    version: 1,
    exportedAt: new Date().toISOString(),
    data,
  };
  const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `pocasimeteo-config-${new Date().toISOString().slice(0, 10)}.json`;
  document.body.appendChild(a);
  a.click();
  a.remove();
  URL.revokeObjectURL(url);
  window.showToast?.('Konfigurace exportována');
}

function importConfig(file) {
  if (!file) return;
  const reader = new FileReader();
  reader.onload = () => {
    try {
      const parsed = JSON.parse(reader.result);
      if (!parsed || typeof parsed.data !== 'object') throw new Error('Neplatný formát');
      // Wipe existing pm_* keys first so the imported state replaces (not
      // merges with) what was there — half-merged state is worse than a
      // clean swap.
      const toRemove = [];
      for (let i = 0; i < localStorage.length; i++) {
        const k = localStorage.key(i);
        if (k && k.startsWith('pm_')) toRemove.push(k);
      }
      toRemove.forEach(k => localStorage.removeItem(k));
      Object.entries(parsed.data).forEach(([key, value]) => {
        if (!key.startsWith('pm_')) return;
        const stored = typeof value === 'string' ? value : JSON.stringify(value);
        localStorage.setItem(key, stored);
      });
      window.showToast?.('Konfigurace načtena – obnovuji…');
      setTimeout(() => location.reload(), 700);
    } catch (e) {
      console.error('Config import failed', e);
      window.showToast?.('Chyba: neplatný soubor', { icon: 'exclamation-triangle' });
    }
  };
  reader.readAsText(file);
}

// ── Color palettes for the picker ─────────────────────────────────────
const ACCENT_PALETTES = {
  sky:     { accent: '#38bdf8', name: 'Sky' },
  violet:  { accent: '#a78bfa', name: 'Violet' },
  mint:    { accent: '#34d399', name: 'Mint' },
  amber:   { accent: '#fbbf24', name: 'Amber' },
  rose:    { accent: '#fb7185', name: 'Rose' },
  ember:   { accent: '#fb923c', name: 'Ember' },
  cyan:    { accent: '#22d3ee', name: 'Cyan' },
  emerald: { accent: '#10b981', name: 'Emerald' },
  indigo:  { accent: '#818cf8', name: 'Indigo' },
};

// Výchozí Graf předpovědi (pm_chart_cfg): teplota, srážky a tlak s ikonami na 48 h —
// Lukášovo nastavení z exportu 22. 10. 2026 10:43, teplota zapnutá na jeho pokyn
// 22. 10. 2026 23:30. Hodinová předpověď ve výchozím 24 h (pm_hourly_range).
// v = verze uloženého nastavení (viz nactiChartCfg).
const CHART_DEFAULTS = { temp:true, precip:true, wind:false, humidity:false, pressure:true, icons:true, hours:48, v:2 };
// Lokalita bez stanice (pm_bez_dat) má vlastní uložené nastavení grafu (pm_chart_cfg_bez),
// výchozí je stejné.
const CHART_DEFAULTS_BEZ = { ...CHART_DEFAULTS };
// Graf ukládá nastavení hned při načtení, takže kdo stránku otevřel před 23. 10. 2026,
// má uložené tehdejší výchozí (bez teploty), i když nic nezvolil. Přesně to staré
// výchozí se jednou převede na nové; jiné uložené nastavení zůstává.
const CHART_STARE_VYCHOZI = { temp:false, precip:true, wind:false, humidity:false, pressure:true, icons:true, hours:48 };
function nactiChartCfg(bez) {
  const vychozi = bez ? CHART_DEFAULTS_BEZ : CHART_DEFAULTS;
  try {
    const raw = localStorage.getItem(bez ? 'pm_chart_cfg_bez' : 'pm_chart_cfg');
    if (raw) {
      const ulozene = JSON.parse(raw);
      if (!bez && !ulozene.v && Object.keys(CHART_STARE_VYCHOZI).every(k => ulozene[k] === CHART_STARE_VYCHOZI[k])) {
        ulozene.temp = true;
      }
      return { ...vychozi, ...ulozene, v: 2 };
    }
  } catch {}
  return { ...vychozi };
}
const HOURLY_DEFAULT = 24;

function App() {
  const [tweaks, setTweak] = window.useTweaks
    ? window.useTweaks(window.TWEAK_DEFAULTS)
    : [window.TWEAK_DEFAULTS, () => {}];

  // Widgets read this while rendering (sensor sparklines take the accent).
  window.__accentCharts = !!tweaks.useAccent;
  const [models, setModels] = useState([]);
  const [currentModel, setCurrentModel] = useState('MASTER');
  const [autoModel, setAutoModel] = useState(true);
  const [editing, setEditing] = useState(false);
  // Konec úprav (Hotovo, E, tužka, panel): změněné rozložení, které není
  // v žádné předvolbě, nabídnout k uložení — aktuální rozložení se pamatuje
  // samo, ale přepnutím předvolby by se ztratilo (Lukáš 21. 10. 2026).
  const editStart = useRef(null);
  useEffect(() => {
    if (editing) {
      const cur = window.__dash_currentLayout;
      editStart.current = cur ? JSON.parse(JSON.stringify(cur)) : null;
      return;
    }
    const start = editStart.current;
    editStart.current = null;
    const now = window.__dash_currentLayout;
    if (!start || !now || sameFullLayout(start, now)) return;
    const presets = window.getAllPresets?.() || [];
    // Odpovídá předvolbě: vestavěné stačí šířky a pořadí (přepsat ji nejde),
    // vlastní musí sedět celá (jinak nabídnout její přepsání).
    const match = presets.find(p => samePresetLayout(p.layout, now));
    if (match && (match.builtin || sameFullLayout(match.layout, now))) return;
    const target = match || presets.find(p => !p.builtin && samePresetLayout(p.layout, start));
    const saveNew = () => {
      const name = window.prompt('Název předvolby:', 'Moje rozložení');
      if (!name || !name.trim() || !window.__dash_currentLayout) return;
      window.savePresetFromLayout?.(name.trim(), window.__dash_currentLayout);
      window.showToast?.(`Předvolba „${name.trim()}“ uložena`);
    };
    const actions = target
      ? [{ label: `Uložit do „${target.name}“`, onClick: () => {
            window.overwriteCustomPreset?.(target.id, window.__dash_currentLayout);
            window.showToast?.(`Předvolba „${target.name}“ přepsána`);
          } },
         { label: 'Jako novou…', secondary: true, onClick: saveNew }]
      : [{ label: 'Uložit jako předvolbu…', onClick: saveNew }];
    window.showToast?.('Rozložení se změnilo — uložit ho do předvolby?',
      { icon: 'bookmark-plus', actions, close: true, duration: 12000 });
  }, [editing]);
  const [tweaksOpen, setTweaksOpen] = useState(false);
  const [catalogOpen, setCatalogOpen] = useState(false);
  const [cmdOpen, setCmdOpen] = useState(false);
  const [siteName, setSiteName] = useState('Praha 6 Ruzyně');
  const [siteId, setSiteId] = useState('praha-6-ruzyne');
  // hourly range and chart range persist between sessions — users want their
  // 12/24/48/72 choice to stick.
  const [hourlyRange, setHourlyRange] = useState(() => {
    try { return Number(localStorage.getItem('pm_hourly_range')) || HOURLY_DEFAULT; } catch { return HOURLY_DEFAULT; }
  });
  // bez stanice se pozná z pm_bez_dat (při první návštěvě až po načtení stanice, viz chartBez níže)
  const chartBezRef = useRef(!!window.cteBezDat?.(window.StationData?.siteId));
  const [chartCfg, setChartCfg] = useState(() => nactiChartCfg(chartBezRef.current));
  useEffect(() => { try { localStorage.setItem('pm_hourly_range', String(hourlyRange)); } catch {} }, [hourlyRange]);
  useEffect(() => {
    try { localStorage.setItem(chartBezRef.current ? 'pm_chart_cfg_bez' : 'pm_chart_cfg', JSON.stringify(chartCfg)); } catch {}
  }, [chartCfg]);
  const [tick, setTick] = useState(0);

  const [station, setStation] = useState(null);
  const [stationStatus, setStationStatus] = useState('loading');
  // Jakmile je jasné, že stanice data neposílá (nebo zase posílá), přepnout nastavení grafu.
  const chartBez = stationStatus !== 'ok' && !!window.cteBezDat?.(StationData.siteId);
  useEffect(() => {
    if (chartBezRef.current === chartBez) return;
    chartBezRef.current = chartBez;
    setChartCfg(nactiChartCfg(chartBez));
  }, [chartBez]);
  const [forecastUpdatedAt, setForecastUpdatedAt] = useState(null);
  const [stationUpdatedAt, setStationUpdatedAt] = useState(null);

  // theme / variant / density / accent + all the new v2 data-attrs
  useEffect(() => {
    const root = document.documentElement;
    root.setAttribute('data-theme',      tweaks.theme   || 'dark');
    root.setAttribute('data-variant',    tweaks.variant || 'aurora');
    root.setAttribute('data-density',    tweaks.density || 'compact');
    root.setAttribute('data-sticky',     tweaks.stickyHeader === false ? 'off' : 'on');
    root.setAttribute('data-charts',     tweaks.compactCharts ? 'dense' : 'normal');
    root.setAttribute('data-tiles',      tweaks.tiles === false ? 'off' : 'on');
    // „Akcent podle teploty“ — the accent follows the outside temperature on
    // the same scale as the station page (shared setting).
    // Dokud nejsou data stanice, platí poslední spočítaná barva (pm_accent_now,
    // sdílená se stanicí) — ne pevný akcent, ať barva neproblikne.
    const tNow = station?.latest?.TeplotaVnejsi;
    const dynAcc = tweaks.dynamicAccent && tNow != null && window.tempColor
      ? window.tempColor(tNow, (tweaks.theme || 'dark') === 'dark') : null;
    let acc = dynAcc;
    if (!acc && tweaks.dynamicAccent) { try { acc = localStorage.getItem('pm_accent_now'); } catch {} }
    root.style.setProperty('--accent', acc || tweaks.accent || '#38bdf8');
    if (dynAcc) { try { localStorage.setItem('pm_accent_now', dynAcc); } catch {} }
    root.style.setProperty('--r-scale',      String(tweaks.radiusScale ?? 1));
    root.style.setProperty('--font-scale',   String(tweaks.fontScale   ?? 1));
    root.style.setProperty('--shadow-scale', String(tweaks.shadowScale ?? 1));
    root.style.setProperty('--grid-gap',     (tweaks.gridGap ?? 10) + 'px');
    root.style.setProperty('--hour-card-w',  (tweaks.hourCardWidth ?? 60) + 'px');
    root.style.setProperty('--sensor-min',   (tweaks.sensorMin ?? 130) + 'px');
    root.style.setProperty('--chrome-alpha', String(tweaks.chromeAlpha ?? 1));
  }, [tweaks, station]);

  // Jediná uživatelská jednotka — vítr. Ostatní jsou fixní defaulty
  // (°C, hPa, mm, 24 h) řešené přes Units.* passthrough.
  useEffect(() => {
    window.UnitPrefs = { wind: tweaks.unitWind || 'mps' };
  }, [tweaks.unitWind]);

  // Page (browser tab) title: "Počasí <lokalita>" (Lukáš 22. 10. 2026; the
  // page is „Počasí“ in the Stanice | Počasí | Statistiky switch), station
  // locality preferred. The server writes the same title for link previews.
  useEffect(() => {
    const name = (station && station.locality) || siteName;
    document.title = name ? `Počasí ${name}` : 'Počasí';
  }, [station, siteName]);

  // Auto-theme: switch dark/light based on local sunrise/sunset
  useEffect(() => {
    if (!tweaks.autoTheme) return;
    const update = () => {
      const c = Astronomy.getCoords(siteId);
      const { sunrise, sunset } = Astronomy.sunTimes(new Date(), c.lat, c.lon);
      if (!sunrise || !sunset) return;
      const now = new Date();
      const isDay = now >= sunrise && now <= sunset;
      const wanted = isDay ? 'light' : 'dark';
      if (tweaks.theme !== wanted) setTweak('theme', wanted);
    };
    update();
    const id = setInterval(update, 5 * 60 * 1000);
    return () => clearInterval(id);
  }, [tweaks.autoTheme, siteId]); // eslint-disable-line

  // Záhlaví spojené s lištou rozložení: při rolování se přilepené záhlaví
  // oddělí (CSS podle data-stuck na <html>).
  useEffect(() => {
    const on = () => {
      const h = document.querySelector('.app > .topbar');
      document.documentElement.toggleAttribute('data-stuck', !!h && window.scrollY > 0 && h.getBoundingClientRect().top <= 8.5);
    };
    // --hdr-h = výška záhlaví: společný podklad záhlaví a lišty rozložení
    // (preset-bar::before) sahá o ni nahoru, aby přechod navazoval
    const size = () => { const h = document.querySelector('.app > .topbar'); if (h) document.documentElement.style.setProperty('--hdr-h', h.offsetHeight + 'px'); };
    const ro = new ResizeObserver(size);
    const hdr = document.querySelector('.app > .topbar');
    if (hdr) ro.observe(hdr);
    on(); size();
    window.addEventListener('scroll', on, { passive: true });
    return () => { window.removeEventListener('scroll', on); ro.disconnect(); };
  }, []);

  // clock tick
  useEffect(() => {
    const id = setInterval(() => setTick(t => t + 1), 30000);
    return () => clearInterval(id);
  }, []);

  const firstContentRef = useRef(false);
  const refreshAskedRef = useRef(null);
  const loadForecastRef = useRef(null);
  // ── Forecast loader ──────────────────────────
  const loadForecast = useCallback(async () => {
    const info = await WeatherData.loadSiteInfo();
    if (info?.site?.name) setSiteName(info.site.name);
    if (info?.site?.id) setSiteId(info.site.id);
    const ids = [...await WeatherData.loadAvailableModels()];
    // MASTER chybí v available_models.json, když ho API při stažení nevrátilo
    // (23. 10. 2026 00:00 Ruzyně a Stodůlky) — MASTER_data.json z dřívějška ale
    // pořád platí: Denní přehled z něj bere další dny, ikona „teď“ hodinu.
    const masterNavic = !ids.includes('MASTER');
    if (masterNavic) ids.unshift('MASTER');
    const loaded = await Promise.all(ids.map(async id => ({
      id, meta: WeatherData.MODEL_META[id] || {id, name:id, sub:''}, data: await WeatherData.loadModelData(id)
    })));
    // dodatečný MASTER jen s předpovědí dopředu (hodinová data začínají poslední minulou hodinou)
    const ok = loaded.filter(m => m.data && !(masterNavic && m.id === 'MASTER' && m.data.hourly.length < 2));
    // when the forecast was produced (site_info.json „21.10.2026 07:00:03“)
    const m = /(\d+)\.(\d+)\.(\d{4})\s+(\d+):(\d+)(?::(\d+))?/.exec(info?.lastUpdate || '');
    const runAt = m ? new Date(+m[3], m[2] - 1, +m[1], +m[4], +m[5], +(m[6] || 0)) : null;
    const apply = () => { setModels(ok); setForecastUpdatedAt(runAt); };
    // Data z jiné hodiny (stanice bez hodinového cronu): požádat server o nové,
    // stejně jako stará předpověď — /weather-api stáhne nejvýš 1× za hodinu na
    // stanici a souběžné žádosti počká. Po stažení se předpověď načte znovu.
    const site = StationData.siteId;
    const hodina = Math.floor(Date.now() / 3600e3);
    if (site && (!runAt || Math.floor(runAt.getTime() / 3600e3) !== hodina) && refreshAskedRef.current !== hodina) {
      refreshAskedRef.current = hodina;
      fetch(`/weather-api/api/fetch/${encodeURIComponent(site)}`, { method: 'POST' })
        .then(r => (r.ok ? r.json() : null))
        .then(j => { if (j && j.status === 'success') loadForecastRef.current?.(); })
        .catch(() => {});
    }
    // First content after the page skeleton crossfades in (index2.html
    // __revealContent) instead of popping; later refreshes update in place.
    if (!firstContentRef.current && ok.length && window.__revealContent) {
      firstContentRef.current = true;
      window.__revealContent(() => ReactDOM.flushSync(apply));
    } else apply();
  }, []);

  loadForecastRef.current = loadForecast;

  const loadStation = useCallback(async () => {
    if (StationData.ready) await StationData.ready;
    if (!StationData.hasApiKey) { setStationStatus('no-key'); return; }
    const result = await StationData.loadStation();
    if (result.ok) {
      setStation(result.station);
      setStationStatus('ok');
      setStationUpdatedAt(new Date());
    } else {
      setStationStatus('offline');
    }
    if (result.ok || result.reason === 'no-data') zapisBezDat(StationData.siteId, !result.ok);
  }, []);

  // „Noční snímky webkamery“ changed (here, or on the station page in another
  // tab — shared pm_tweaks_v1): fetch the station again for the other picture.
  const webcamNocFirst = useRef(true);
  useEffect(() => {
    if (webcamNocFirst.current) { webcamNocFirst.current = false; return; }
    loadStation();
  }, [tweaks.webcamNoc]);   // eslint-disable-line react-hooks/exhaustive-deps

  useEffect(() => {
    loadForecast();
    loadStation();
    let fI = null;
    const fT = setTimeout(() => { loadForecast(); fI = setInterval(loadForecast, 60*60*1000); }, msUntilNextHour());
    let sI = null;
    const sT = setTimeout(() => { loadStation(); sI = setInterval(loadStation, 5*60*1000); }, msUntilNext5());
    return () => {
      clearTimeout(fT); clearTimeout(sT);
      if (fI) clearInterval(fI);
      if (sI) clearInterval(sI);
    };
  }, [loadForecast, loadStation]);

  useEffect(() => {
    const onVis = () => {
      if (document.visibilityState === 'visible') {
        if (!stationUpdatedAt || Date.now() - stationUpdatedAt.getTime() > 60000) loadStation();
      }
    };
    document.addEventListener('visibilitychange', onVis);
    return () => document.removeEventListener('visibilitychange', onVis);
  }, [loadStation, stationUpdatedAt]);

  const coords = useMemo(() => Astronomy.getCoords(siteId), [siteId]);

  const scores = useMemo(() => {
    if (!station?.latest) return models.map(m => ({ id: m.id, name: m.meta.name, sub: m.meta.sub, score: null }));
    // Compare the forecast against averaged station readings over the past hour
    // instead of the single most-recent sample. A 5-minute gust spike used to
    // tank a model's score even if it nailed the hour overall; averaging the
    // bulk metrics and taking MAX for gusts matches how the forecast itself
    // models conditions (hourly mean + peak gust). Falls back to latest if the
    // measurements buffer is empty (e.g. fresh page load).
    const windowMs = 60 * 60 * 1000;
    const cutoff = Date.now() - windowMs;
    const recent = (station.measurements || []).filter(m => m.time && m.time.getTime() >= cutoff);
    const avg = (vals) => {
      const xs = vals.filter(v => v != null && !isNaN(v));
      return xs.length ? xs.reduce((s, v) => s + v, 0) / xs.length : null;
    };
    const maxOf = (vals) => {
      const xs = vals.filter(v => v != null && !isNaN(v));
      return xs.length ? Math.max(...xs) : null;
    };
    const stationAsForecast = recent.length ? {
      temp:     avg(recent.map(m => m.temp))     ?? station.latest.TeplotaVnejsi,
      humidity: avg(recent.map(m => m.humidity)) ?? station.latest.VlhkostVnejsi,
      pressure: avg(recent.map(m => m.pressure)) ?? station.latest.TlakRel,
      wind:     avg(recent.map(m => m.wind))     ?? station.latest.Vitr,
      gust:     maxOf(recent.map(m => m.gust))   ?? station.latest.VitrNarazy,
    } : {
      temp:     station.latest.TeplotaVnejsi,
      humidity: station.latest.VlhkostVnejsi,
      pressure: station.latest.TlakRel,
      wind:     station.latest.Vitr,
      gust:     station.latest.VitrNarazy,
    };
    return models.map(m => {
      const s = m.data ? WeatherData.scoreModel(stationAsForecast, m.data.hourly[0]) : null;
      return { id: m.id, name: m.meta.name, sub: m.meta.sub, score: s };
    });
  }, [models, station]);

  const autoPickId = useMemo(() => {
    if (!scores.length) return null;
    const sorted = [...scores].filter(s => s.score).sort(WeatherData.compareFit);
    return sorted[0]?.id || null;
  }, [scores]);

  const effectiveModel = autoModel && autoPickId ? autoPickId : currentModel;
  const activeData = models.find(m => m.id === effectiveModel)?.data;
  // One "now" icon for the topbar and the Aktuálně widget, same rule as the
  // station page — not the hour of whichever model is picked.
  const nowWx = WeatherData.currentWeather(models, station, activeData?.hourly?.[0]);
  // ikona v záložce podle aktuálního počasí
  useEffect(() => { if (nowWx?.icon) setWeatherFavicon(WeatherData.iconUrl(nowWx.icon)); }, [nowWx?.icon]);

  // Push the current temperature bucket onto <html data-temp="..."> so CSS
  // can hint a background tint. Prefers the station reading, falls back to
  // forecast hour 0. (Must sit below `activeData` — reading it earlier is a
  // temporal-dead-zone error once the code isn't down-levelled to `var`.)
  useEffect(() => {
    const t = station?.latest?.TeplotaVnejsi ?? activeData?.hourly?.[0]?.temp;
    if (t == null) return;
    const bucket = t >= 25 ? 'hot' : t >= 15 ? 'warm' : t >= 5 ? 'cool' : t >= -5 ? 'cold' : 'frost';
    document.documentElement.setAttribute('data-temp', bucket);
    if (station?.latest?.TeplotaVnejsi != null) { try { localStorage.setItem('pm_temp_now', bucket); } catch {} }   // sdíleno se stanicí
  }, [station, activeData]);

  // host edit-mode
  useEffect(() => {
    const onMsg = (ev) => {
      const d = ev.data || {};
      if (d.type === '__activate_edit_mode')   setTweaksOpen(true);
      if (d.type === '__deactivate_edit_mode') setTweaksOpen(false);
    };
    window.addEventListener('message', onMsg);
    window.parent.postMessage({ type: '__edit_mode_available' }, '*');
    return () => window.removeEventListener('message', onMsg);
  }, []);

  // Command palette keyboard shortcut (⌘K / Ctrl+K)
  useEffect(() => {
    const onKey = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
        e.preventDefault();
        setCmdOpen(o => !o);
      }
      // 'E' toggles edit
      if (e.key.toLowerCase() === 'e' && !e.metaKey && !e.ctrlKey && !e.altKey
          && !(e.target instanceof HTMLInputElement) && !(e.target instanceof HTMLTextAreaElement)) {
        setEditing(v => !v);
      }
      // 'T' toggles theme
      if (e.key.toLowerCase() === 't' && !e.metaKey && !e.ctrlKey && !e.altKey
          && !(e.target instanceof HTMLInputElement) && !(e.target instanceof HTMLTextAreaElement)) {
        setTweak({ theme: tweaks.theme === 'dark' ? 'light' : 'dark', autoTheme: false });
      }
    };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [tweaks.theme]); // eslint-disable-line

  // Status info — bez "živě" labelu, jen barevná tečka + informace o stáří dat.
  const statusInfo = useMemo(() => {
    if (stationStatus === 'no-key' || (stationStatus === 'offline' && window.cteBezDat?.(StationData.siteId)))
      return { cls: 'recent-warn', label: 'jen předpověď', detail: '' };
    if (stationStatus === 'loading') return { cls: 'recent-warn', label: 'načítám…', detail: '' };
    if (stationStatus === 'offline' || !station?.timestamp) return { cls: 'recent-bad', label: 'stanice offline', detail: '' };
    const ageMin = Math.round((Date.now() - station.timestamp.getTime()) / 60000);
    const cls = ageMin <= 10 ? 'recent-fresh' : ageMin <= 30 ? 'recent-warn' : 'recent-bad';
    return { cls, label: `před ${ageMin} min`, detail: '' };
  }, [stationStatus, station, tick]);

  // ── Command palette commands ──────────────────────────────────
  const commands = useMemo(() => {
    const list = [];
    // Theme + appearance
    list.push({ id: 'theme.toggle', label: 'Přepnout téma', desc: 'Světlé ↔ tmavé', icon: 'circle-half',
      section: 'Vzhled', kbd: 'T',
      run: () => setTweak({ theme: tweaks.theme === 'dark' ? 'light' : 'dark', autoTheme: false }) });
    list.push({ id: 'tweaks.open', label: 'Otevřít panel Vzhled a rozložení', icon: 'sliders', section: 'Vzhled',
      run: () => setTweaksOpen(true) });
    list.push({ id: 'edit.toggle', label: editing ? 'Ukončit úpravy' : 'Upravit dashboard',
      desc: 'Přesouvání a měnění velikostí widgetů', icon: 'pencil-square', section: 'Akce', kbd: 'E',
      run: () => setEditing(e => !e) });
    list.push({ id: 'widgets.add', label: 'Přidat widget', icon: 'plus-lg', section: 'Akce',
      run: () => { setEditing(true); setCatalogOpen(true); } });
    list.push({ id: 'layout.reset', label: 'Obnovit výchozí rozložení', icon: 'arrow-counterclockwise', section: 'Akce',
      run: () => { window.__dash_reset?.(); window.showToast('Rozložení obnoveno'); } });
    // Preset switcher — built-in + custom user presets
    (window.getAllPresets?.() || []).forEach(p => {
      list.push({ id: `preset.${p.id}`, label: `Preset: ${p.name}`, desc: p.desc,
        icon: p.icon || 'grid', section: 'Layout',
        run: () => {
          window.__dash_setLayout?.(p.layout);
          window.showToast(`Preset „${p.name}" načten`);
        } });
    });
    list.push({ id: 'preset.save', label: 'Uložit aktuální layout jako preset…',
      icon: 'bookmark-plus', section: 'Layout',
      run: () => {
        const name = (typeof window !== 'undefined' && window.prompt)
          ? window.prompt('Název presetu:', 'Můj layout')
          : null;
        if (!name) return;
        const cur = window.__dash_currentLayout;
        if (!cur) return;
        window.savePresetFromLayout?.(name.trim(), cur);
        window.showToast(`Preset „${name}" uložen`);
      } });
    list.push({ id: 'autoModel.toggle', label: autoModel ? 'Vypnout AUTO model' : 'Zapnout AUTO model',
      icon: 'magic', section: 'Předpověď',
      run: () => setAutoModel(v => !v) });
    // Density presets
    ['ultra','compact','comfortable','spacious'].forEach(d => {
      list.push({ id: `density.${d}`, label: `Hustota: ${d}`, icon: 'arrows-collapse', section: 'Vzhled',
        run: () => setTweak('density', d) });
    });
    // Variant presets
    ['aurora','studio','console','ledger'].forEach(v => {
      list.push({ id: `variant.${v}`, label: `Varianta: ${v}`, icon: 'palette', section: 'Vzhled',
        run: () => setTweak('variant', v) });
    });
    // Accent colors
    Object.entries(ACCENT_PALETTES).forEach(([k, p]) => {
      list.push({ id: `accent.${k}`, label: `Akcent: ${p.name}`, icon: 'droplet-fill', section: 'Vzhled',
        run: () => setTweak('accent', p.accent) });
    });
    // Model picks
    models.forEach(m => {
      list.push({ id: `model.${m.id}`, label: `Model: ${m.meta.name}`, desc: m.meta.sub,
        icon: 'cpu', section: 'Předpověď',
        run: () => { setAutoModel(false); setCurrentModel(m.id); } });
    });
    // Quick toggles
    list.push({ id: 'wind.flow', label: tweaks.windFlow !== false ? 'Vypnout proudění větru' : 'Zapnout proudění větru',
      icon: 'wind', section: 'Vítr',
      run: () => setTweak('windFlow', !tweaks.windFlow) });
    list.push({ id: 'hide.titles', label: tweaks.hideTitles ? 'Zobrazit nadpisy' : 'Skrýt nadpisy widgetů',
      icon: 'eye', section: 'Layout',
      run: () => setTweak('hideTitles', !tweaks.hideTitles) });
    // Hidden widgets — quick restore
    (window.__dash_hidden || []).forEach(id => {
      const meta = window.WIDGET_CATALOG[id];
      if (!meta) return;
      list.push({ id: `show.${id}`, label: `Vrátit: ${meta.title}`, icon: meta.icon, section: 'Widgety',
        run: () => { setEditing(true); window.__dash_show?.(id); window.showToast(`${meta.title} přidán zpět`); } });
    });
    return list;
  // eslint-disable-next-line
  }, [tweaks, autoModel, models, editing]);

  // Výběr modelu je v liště rozložení vpravo (Lukáš 21. 10. 2026); s vypnutou
  // lištou zůstane v záhlaví za stavem dat.
  const presetBarOn = tweaks.showPresetBar !== false;
  const modelPicker = (
    <ModelPickerDropdown
      effectiveModel={effectiveModel}
      autoModel={autoModel}
      setAutoModel={setAutoModel}
      scores={scores}
      autoPickId={autoPickId}
      models={models}
      onPick={(id) => { setAutoModel(false); setCurrentModel(id); }}
    />
  );

  return (
    <div className="app">
      <TopBar
        tweaks={tweaks} setTweak={setTweak}
        statusInfo={statusInfo}
        modelPicker={presetBarOn ? null : modelPicker}
        editing={editing}
        setEditing={setEditing}
        tweaksOpen={tweaksOpen}
        setTweaksOpen={setTweaksOpen}
        setCatalogOpen={setCatalogOpen}
        siteName={siteName}
        station={station}
        activeData={activeData}
        nowWx={nowWx}
      />

      {activeData && presetBarOn && (
        <PresetBar editing={editing} modelPicker={modelPicker} />
      )}

      {/* Same rule as the original page: warn when the forecast is older than 8 h */}
      {forecastUpdatedAt && Date.now() - forecastUpdatedAt.getTime() > 8 * 3600e3 && (
        <div className="fc-stale" role="status">
          <i className="bi bi-exclamation-triangle-fill" aria-hidden="true"></i>
          Předpověď je starší než 8 hodin — naposledy aktualizována {fmtStamp(forecastUpdatedAt)}.
        </div>
      )}

      {/* Edit mode toolbar floats at the bottom — nothing on the page moves
          when entering/leaving edit mode. */}
      {editing && !catalogOpen && <EditPill onAdd={() => setCatalogOpen(true)} onDone={() => setEditing(false)} />}

      {activeData ? (
        <Dashboard
          hourly={activeData.hourly}
          daily={activeData.daily}
          dailyMore={effectiveModel !== 'MASTER' ? models.find(m => m.id === 'MASTER')?.data?.daily : null}
          nowWx={nowWx}
          station={station}
          stationStatus={stationStatus}
          coords={coords}
          scores={scores}
          currentModel={effectiveModel}
          autoModel={autoModel}
          autoPickId={autoPickId}
          onPickModel={(id) => { setAutoModel(false); setCurrentModel(id); }}
          onToggleAuto={setAutoModel}
          hourlyRange={hourlyRange}
          setHourlyRange={setHourlyRange}
          chartCfg={chartCfg}
          setChartCfg={setChartCfg}
          showStation={tweaks.showStation !== false}
          windFlow={tweaks.windFlow !== false}
          windFlowSpeed={tweaks.windFlowSpeed || 1}
          chartFillArea={tweaks.chartFillArea !== false}
          chartMinMax={tweaks.chartMinMax !== false}
          hideTitles={tweaks.hideTitles === true}
          editing={editing}
          onAddOpen={() => setCatalogOpen(true)}
        />
      ) : (
        <div className="skel">
          {[1,2,3,4,5,6].map(i => <div key={i} className="skel-card"/>)}
        </div>
      )}

      {activeData && (
        <footer className="site-foot">
          <span>API data poskytována aplikací <a href="https://www.pocasimeteo.cz" target="_blank" rel="noopener noreferrer">www.PocasiMeteo.cz</a></span>
          {forecastUpdatedAt && <span>Předpověď aktualizována {fmtStamp(forecastUpdatedAt)}</span>}
        </footer>
      )}


      {catalogOpen && <CatalogOverlay onClose={() => setCatalogOpen(false)} />}
      {cmdOpen && <CommandPalette commands={commands} onClose={() => setCmdOpen(false)} />}

      {tweaksOpen && (
        <TweaksDrawer
          tweaks={tweaks}
          setTweak={setTweak}
          autoModel={autoModel}
          setAutoModel={setAutoModel}
          onClose={() => setTweaksOpen(false)}
          editing={editing}
          setEditing={setEditing}
        />
      )}
    </div>
  );
}

// „21. 10. 7:00“ — date only when it isn't today
function fmtStamp(d) {
  const today = new Date().toDateString() === d.toDateString();
  return `${today ? 'dnes' : `${d.getDate()}. ${d.getMonth() + 1}.`} v ${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`;
}

// Okno Podpořit: volba částky, úroveň podpory, QR Platba se zprávou „Podpora <lokalita>“ a popisky jako
// hlavní stránka /lokalita/ – vše kreslí sdílený /podpora-qr.js (window.podporaPanel) pro stanici, Počasí
// i Statistiky (Lukáš 23. 10. 2026). Načte se až s oknem; při chybě starý statický /qr.png na 77 Kč.
const PRAZDNY_OBRAZEK = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
let podporaQR = null;
function nactiPodporuQR() {
  return podporaQR || (podporaQR = new Promise((ok, chyba) => {
    const s = document.createElement('script');
    s.src = '/podpora-qr.js?v=3';
    s.onload = () => (window.podporaPanel ? ok(window.podporaPanel) : chyba());
    s.onerror = () => { podporaQR = null; chyba(); };
    document.head.appendChild(s);
  }));
}
function PodporaQR({ lokalita, nazev }) {
  const misto = useRef(null);
  useEffect(() => {
    let plati = true;
    nactiPodporuQR()
      .then((panel) => { if (plati && misto.current) panel(misto.current, lokalita, nazev); })
      .catch(() => {
        if (plati && misto.current) misto.current.innerHTML = '<p class="donate-account">5472281093/0800</p><img class="donate-qr" src="/qr.png" alt="QR kód pro podporu">';
      });
    return () => { plati = false; };
  }, [lokalita, nazev]);
  return <div ref={misto}><img className="donate-qr" src={PRAZDNY_OBRAZEK} alt="" /></div>;
}

// Odznak podporovatele na rohu loga (Lukáš 22. 10. 2026): window.PM_PODPORA = {uroven, nazev}
// vkládá server (routes.py podpora_js) jen u lokality s darem; úrovně jsou v UROVNE_PODPORY.
// Kresby a barvy úrovní jako hlavní stránka /lokalita/ (mapa stanic): srdce, srdce s jiskrou, koruna.
const ODZNAKY_PODPORY = {
  1: '<path d="M12 20.5s-8-4.9-8-10.6C4 7.1 6 5 8.5 5c1.5 0 2.8.8 3.5 2 .7-1.2 2-2 3.5-2C18 5 20 7.1 20 9.9c0 5.7-8 10.6-8 10.6z"/>',
  2: '<path d="M10 21s-7-4.2-7-9.2C3 9.4 4.7 7.5 6.9 7.5c1.3 0 2.4.7 3.1 1.7.7-1 1.8-1.7 3.1-1.7 2.2 0 3.9 1.9 3.9 4.3 0 5-7 9.2-7 9.2z"/>' +
     '<path d="M18.5 1.5l1 2.5 2.5 1-2.5 1-1 2.5-1-2.5-2.5-1 2.5-1z"/>',
  3: '<path d="M3 8l4.5 3.5L12 4l4.5 7.5L21 8l-1.8 10H4.8z"/><path d="M5 19.5h14v2H5z"/>',
};
function PodporaOdznak() {
  const p = window.PM_PODPORA;
  if (!p || !p.uroven) return null;
  const u = ODZNAKY_PODPORY[p.uroven] ? p.uroven : 1;
  const popis = `${p.nazev} projektu Počasí Meteo`;
  return (
    <i className={`podpora-odznak u${u}`} title={popis} aria-label={popis} role="img">
      <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" dangerouslySetInnerHTML={{ __html: ODZNAKY_PODPORY[u] }} />
    </i>
  );
}

// ─────────────────────────────────────────────────────────────────────
// TopBar — masthead shared with the station page: brand (weather icon +
// POČASÍMETEO over the locality) | Stanice/Předpověď switch | status + actions
// (.topbar-right). CSS: "Záhlaví" block at the end of styles-v2.css.
// ─────────────────────────────────────────────────────────────────────
function TopBar({ tweaks, setTweak, statusInfo, modelPicker,
                  editing, setEditing,
                  tweaksOpen, setTweaksOpen, setCatalogOpen,
                  siteName, station, activeData, nowWx }) {
  const h0 = activeData?.hourly?.[0];
  const showQuickPills = tweaks.showQuickPills !== false;
  // Locality + icon for the static header skeleton in index2.html (and the
  // station page's), so navigating between the two pages doesn't flash.
  const locality = (station && station.locality) || siteName;
  const brandIcon = nowWx ? WeatherData.iconUrl(nowWx.icon) : null;
  useEffect(() => {
    const site = window.PM_LOKALITA || location.pathname.split('/')[1];
    if (!site || !locality) return;
    try {
      const all = JSON.parse(localStorage.getItem('pm_zahlavi') || 'null') || {};
      all[site] = { loc: locality, icon: brandIcon ? new URL(brandIcon, location.href).href : null };
      localStorage.setItem('pm_zahlavi', JSON.stringify(all));
    } catch (e) { /* ignore */ }
  }, [locality, brandIcon]);
  const [donateOpen, setDonateOpen] = useState(false);

  return (
    <header className="topbar">
      {/* logo vede na hlavní stránku s mapou stanic (Lukáš 23. 10. 2026) */}
      <a className="brand" href="/lokalita/" title="Mapa stanic">
        <div className="brand-mark">
          <img src={nowWx ? WeatherData.iconUrl(nowWx.icon) : 'icons/a02d.png'} alt="" title={nowWx?.cond} />
          <PodporaOdznak />
        </div>
        <div className="brand-text">
          <div className="brand-title">POČASÍMETEO</div>
          <div className="brand-locality">{(station && station.locality) || siteName}</div>
        </div>
      </a>

      <SiteNav active="predpoved" />

      <div className="topbar-right">
      <div className="topbar-status" title={[statusInfo.label, statusInfo.detail].filter(Boolean).join(' · ')}>
        <span className={`status-dot ${statusInfo.cls}`}><i /></span>
        <span className="status-label">{statusInfo.label}</span>
        {statusInfo.detail && <><span>·</span><span>{statusInfo.detail}</span></>}
        {modelPicker && <><span>·</span>{modelPicker}</>}
      </div>

      {showQuickPills && h0?.precipProb >= 15 && (
        <div className="topbar-quick">
          <div className="topbar-pill" title="Pravděpodobnost srážek">
            <i className="bi bi-droplet-fill"/>
            <strong>{h0.precipProb}%</strong>
          </div>
        </div>
      )}

      <div className="topbar-actions">
        <button className={`topbar-btn ${editing?'primary':''}`} aria-pressed={editing}
                onClick={() => setEditing(e => !e)} title="Upravit (E)">
          <i className={`bi ${editing?'bi-check2':'bi-pencil-square'}`}/>
        </button>
        {/* Same tail as the station page: Podpořit, theme, Vzhled. */}
        <button className="topbar-btn topbar-btn-donate topbar-btn-wide" aria-label="Podpořit projekt"
                title="Podpořit projekt" onClick={() => setDonateOpen(true)}>
          <i className="bi bi-heart-fill" aria-hidden="true"/>
          <span className="topbar-btn-label">Podpořit</span>
        </button>
        <button className="topbar-btn"
                onClick={() => setTweak({ theme: tweaks.theme === 'dark' ? 'light' : 'dark', autoTheme: false })} title="Téma (T) — platí i pro stránku stanice">
          <i className={`bi ${tweaks.theme === 'dark' ? 'bi-sun' : 'bi-moon-stars'}`}/>
        </button>
        <button className="topbar-btn" aria-pressed={tweaksOpen}
                onClick={() => setTweaksOpen(o => !o)} title="Vzhled a rozložení">
          <i className="bi bi-sliders"/>
        </button>
      </div>
      </div>
      {donateOpen && (
        <Modal title="Podpořte stanici" kind="donate" bodyClass="donate-modal-body"
               onClose={() => setDonateOpen(false)}>
          <PodporaQR lokalita={StationData.siteId} nazev={(station && station.locality) || siteName} />
        </Modal>
      )}
    </header>
  );
}


// ─────────────────────────────────────────────────────────────────────
// SiteNav — the two faces of one station: live station page (/ms) and this
// forecast dashboard. Same pill on both pages so they read as one product.
// The station page is addressed by locality (/lokalita/stanice/?<site>), never
// by API key. /lokalita/ runs the /mstest app until /ms takes over; the bare
// /lokalita/?<site> redirects to the default page (VychoziStranka below).
// ─────────────────────────────────────────────────────────────────────
const STATION_PAGE = '/lokalita/stanice/';

// Výchozí stránka lokality: kam vede samotné /lokalita/?<lokalita> — server
// čte cookie pm_vychozi (path /lokalita), bez ní Počasí. Stejná volba je
// v panelu Vzhled na stránce stanice (Lukáš 22. 10. 2026).
// Ikony = naše kresby z přepínače stránek (StationGlyph, ForecastGlyph, StatsGlyph).
const VYCHOZI_STRANKY = [
  { value: 'stanice',    label: 'Stanice',    glyph: <StationGlyph /> },
  { value: 'pocasi',     label: 'Počasí',     glyph: <ForecastGlyph /> },
  { value: 'statistiky', label: 'Statistiky', glyph: <StatsGlyph /> },
];
const cteVychozi = () => {
  const m = document.cookie.match(/(?:^|;\s*)pm_vychozi=(stanice|pocasi|statistiky)(?:;|$)/);
  return m ? m[1] : 'pocasi';
};
const zapisVychozi = (v) => {
  document.cookie = `pm_vychozi=${v}; path=/lokalita; max-age=${400 * 86400}; SameSite=Lax${location.protocol === 'https:' ? '; Secure' : ''}`;
};
// Lokalita bez dat stanice (jen předpověď): přepínač ukáže jen Předpověď.
// Pamatuje se v pm_bez_dat (sdílí stránka stanice, ta na předpověď přesměruje).
function cteBezDat(site) {
  try { return !!(site && (JSON.parse(localStorage.getItem('pm_bez_dat') || 'null') || {})[site]); } catch { return false; }
}
window.cteBezDat = cteBezDat;
function zapisBezDat(site, bez) {
  if (!site || cteBezDat(site) === bez) return;
  try {
    const all = JSON.parse(localStorage.getItem('pm_bez_dat') || 'null') || {};
    if (bez) all[site] = 1; else delete all[site];
    localStorage.setItem('pm_bez_dat', JSON.stringify(all));
  } catch {}
  window.dispatchEvent(new Event('pm-bez-dat'));
}
// Ikony přepínače Stanice | Předpověď (vlastní kresba, stejné na obou stránkách
// i v kostře záhlaví): meteostanice = anemometr s miskami, korouhvička a
// trojnožka; předpověď = slunce za mrakem.
function StationGlyph({ className = 'sn-ico' }) {
  return (<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M12 7v13.5"/><path d="M8.3 21.6l3.7-2.6 3.7 2.6"/><path d="M12 7l-5-2.6M12 7l5-2.6M12 7v-4"/><path d="M5.2 2.7a1.9 1.9 0 1 0 .8 3.4M18.8 2.7a1.9 1.9 0 1 1-.8 3.4M10.3 2.4a1.8 1.8 0 0 0 3.4 0"/><path d="M9.4 12.2h6M15.4 10.6v3.2"/><path d="M7.6 12.2l2.4-1.7v3.4z" fill="currentColor" /></svg>);
}
function ForecastGlyph() {
  return (<svg className="sn-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="2.8"/><path d="M8 2.6v1.1M2.6 8h1.1M4.2 4.2l.8.8M11.8 4.2l-.8.8M4.2 11.8l.8-.8"/><path d="M9.2 20h8.3a3.2 3.2 0 0 0 .3-6.39 4.4 4.4 0 0 0-8.3 1.4A2.5 2.5 0 0 0 9.2 20z"/></svg>);
}
// Statistics = bars with a trend line (same drawing on all three pages).
function StatsGlyph() {
  return (<svg className="sn-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M3.5 20.5h17"/><path d="M6.5 17.5v-4M10.5 17.5v-7M14.5 17.5v-5M18.5 17.5v-9.5" strokeWidth="2.4"/><path d="M5 9.5l4.5-4 4 2.5 5.5-4.5"/><path d="M16.6 3.5H19v2.4"/></svg>);
}
// Station statistics (third item of the switch): the shared page
// /lokalita/statistiky/?<site> when the site has /<site>/data.js, else the
// per-site /<site>/index2.html, otherwise the old /<site>/ page. On /lokalita/
// the server puts the answer in the page (window.PM_STAT_HREF, routes.py
// odkaz_statistik); elsewhere HEAD requests find out which exists (each miss
// is a red 404 in the console). The answer is kept in pm_stat_href (shared
// with the station page) so the link is right on the next visit straight away.
function useStatsHref(site) {
  const known = 'PM_STAT_HREF' in window ? window.PM_STAT_HREF : undefined;
  const read = () => { try { return (JSON.parse(localStorage.getItem('pm_stat_href') || 'null') || {})[site] || null; } catch { return null; } };
  const [href, setHref] = useState(() => (known !== undefined ? known : read()));
  useEffect(() => {
    if (!site) return undefined;
    let dead = false;
    const remember = (u) => {
      if (dead) return;
      setHref(u);
      try {
        const all = JSON.parse(localStorage.getItem('pm_stat_href') || 'null') || {};
        if (u) all[site] = u; else delete all[site];
        localStorage.setItem('pm_stat_href', JSON.stringify(all));
      } catch {}
    };
    if (known !== undefined) { remember(known); return () => { dead = true; }; }
    const head = (u) => fetch(u, { method: 'HEAD', cache: 'no-cache' }).then((r) => (r.ok ? u : null), () => null);
    head(`/${site}/data.js`).then((u) => (u ? `/lokalita/statistiky/?${site}` : null))
      .then((u) => u || head(`/${site}/index2.html`))
      .then((u) => u || head(`/${site}/index.html`).then((v) => (v ? `/${site}/` : null)))
      .then(remember);
    return () => { dead = true; };
  }, [site]); // eslint-disable-line
  return site ? href : null;
}
// The highlight („jezdec“) is its own element with a view-transition-name, so
// when you switch pages it slides under the new item (--i = position, --n = count).
function SiteNav({ active }) {
  const site = (window.StationData && window.StationData.siteId) || '';
  const [bezDat, setBezDat] = useState(() => cteBezDat(site));
  useEffect(() => {
    const h = () => setBezDat(cteBezDat(site));
    window.addEventListener('pm-bez-dat', h);
    return () => window.removeEventListener('pm-bez-dat', h);
  }, [site]);
  const stationHref = site && !bezDat ? `${STATION_PAGE}?${encodeURIComponent(site)}` : null;
  const statsHref = useStatsHref(site);
  // Jen to, co lokalita má: bez dat stanice ani statistik zůstane samotná Předpověď.
  const items = [
    stationHref && { id: 'stanice', href: stationHref, glyph: <StationGlyph />, label: 'Stanice' },
    { id: 'predpoved', href: null, glyph: <ForecastGlyph />, label: 'Počasí' },
    statsHref && { id: 'statistiky', href: statsHref, glyph: <StatsGlyph />, label: 'Statistiky' },
  ].filter(Boolean);
  const idx = Math.max(0, items.findIndex((it) => it.id === active));
  return (
    <nav className={`site-nav ${items.length > 1 ? 'is-multi' : 'is-single'}`} style={{ '--n': items.length, '--i': idx }}
         aria-label="Stanice, počasí a statistiky">
      <span className="site-nav-thumb" aria-hidden="true"></span>
      {items.map((it) => it.id === active
        ? <span key={it.id} className="site-nav-item active" aria-current="page">{it.glyph}<span>{it.label}</span></span>
        : it.href
          ? <a key={it.id} className="site-nav-item" href={it.href}>{it.glyph}<span>{it.label}</span></a>
          : <span key={it.id} className="site-nav-item site-nav-locked" title={it.locked}>{it.glyph}<span>{it.label}</span></span>)}
    </nav>
  );
}

// ─────────────────────────────────────────────────────────────────────
// ModelPickerDropdown — the model in the topbar. Popover: „Automatický
// výběr“ (the model closest to the station right now), then every model
// sorted by that fit, each with what it forecasts for today (icon, min–max,
// rain) and how well it fits the station now (score + temperature offset).
// Keyboard: ↑ ↓ Home End move, Enter picks, Esc closes. Phones: bottom sheet.
// ─────────────────────────────────────────────────────────────────────
function ModelPickerDropdown({ effectiveModel, autoModel, setAutoModel, scores, autoPickId, onPick, models }) {
  const [open, setOpen] = useState(false);
  const btnRef = useRef(null);
  const popRef = useRef(null);
  const [pos, setPos] = useState(null);

  // Anchored under the button (fixed, so `.topbar-status` can't clip it);
  // narrow screens get a bottom sheet instead.
  useEffect(() => {
    if (!open) return;
    const place = () => {
      if (!btnRef.current) return;
      if (window.innerWidth < 560) { setPos({ sheet: true }); return; }
      const rect = btnRef.current.getBoundingClientRect();
      const width = Math.min(440, window.innerWidth - 24);
      const left = Math.max(12, Math.min(rect.right - width, window.innerWidth - width - 12));
      setPos({ top: rect.bottom + 8, left, width, maxH: window.innerHeight - rect.bottom - 24 });
    };
    place();
    window.addEventListener('resize', place);
    window.addEventListener('scroll', place, true);
    return () => {
      window.removeEventListener('resize', place);
      window.removeEventListener('scroll', place, true);
    };
  }, [open]);

  useEffect(() => {
    if (!open) return;
    const onDoc = (e) => {
      if (btnRef.current && btnRef.current.contains(e.target)) return;
      if (popRef.current && popRef.current.contains(e.target)) return;
      setOpen(false);
    };
    const onKey = (e) => { if (e.key === 'Escape') { setOpen(false); btnRef.current?.focus(); } };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDoc);
      document.removeEventListener('keydown', onKey);
    };
  }, [open]);
  // focus the current choice when the list opens
  useEffect(() => {
    if (open && pos && popRef.current) popRef.current.querySelector('[data-opt][aria-checked="true"], [data-opt][aria-selected="true"]')?.focus({ preventScroll: true });
  }, [open, !!pos]);
  const onListKey = (e) => {
    const opts = [...(popRef.current?.querySelectorAll('[data-opt]') || [])];
    const i = opts.indexOf(document.activeElement);
    const go = (k) => { e.preventDefault(); opts[Math.max(0, Math.min(opts.length - 1, k))]?.focus(); };
    if (e.key === 'ArrowDown') go(i + 1);
    else if (e.key === 'ArrowUp') go(i - 1);
    else if (e.key === 'Home') go(0);
    else if (e.key === 'End') go(opts.length - 1);
  };

  // Models sorted by fit (best first: most quantities in agreement, then the
  // score); unscored models keep their order at the end.
  const sorted = useMemo(() => {
    const arr = [...(scores || [])];
    arr.sort(WeatherData.compareFit);
    return arr;
  }, [scores]);
  const hasScores = sorted.some(m => m.score);
  const nameOf = (id) => WeatherData.MODEL_META[id]?.name || id;
  const activeName = nameOf(effectiveModel);
  const now = new Date();
  const todayOf = (id) => {
    const d = (models || []).find(m => m.id === id)?.data;
    if (!d || !d.daily || !d.daily.length) return null;
    return d.daily.find(x => x.date.toDateString() === now.toDateString()) || d.daily[0];
  };
  const tU = (v) => Math.round(window.Units ? window.Units.temp(v) : v);
  const choose = (fn) => { fn(); setOpen(false); btnRef.current?.focus(); };

  const popNode = open && pos && (
    <>
      {pos.sheet && <div className="mp2-backdrop" />}
      <div className={`mp2 ${pos.sheet ? 'is-sheet' : ''}`} ref={popRef} role="dialog" aria-label="Model předpovědi"
           onKeyDown={onListKey}
           style={pos.sheet ? undefined : { top: pos.top, left: pos.left, width: pos.width, maxHeight: Math.max(260, pos.maxH) }}>
        <div className="mp2-head">
          <span><i className="bi bi-cpu" aria-hidden="true"></i>Model předpovědi</span>
          {pos.sheet && <button type="button" className="mp2-x" onClick={() => setOpen(false)} aria-label="Zavřít"><i className="bi bi-x-lg" /></button>}
        </div>
        <button type="button" data-opt role="switch" aria-checked={!!autoModel}
                className={`mp2-auto ${autoModel ? 'on' : ''}`}
                onClick={() => choose(() => setAutoModel(!autoModel))}>
          <span className="mp2-auto-ico" aria-hidden="true"><i className="bi bi-stars"></i></span>
          <span className="mp2-auto-text">
            <b>Automatický výběr <span className="mp2-auto-badge">AUTO</span></b>
            <small>{hasScores
              ? <>Vždy model, který je teď nejvíc v souladu s měřením stanice{autoPickId ? <> — teď <strong>{nameOf(autoPickId)}</strong></> : null}.</>
              : 'Jakmile budou data ze stanice, vybere model, který je s ní nejvíc v souladu.'}</small>
          </span>
          <span className="mp2-toggle" aria-hidden="true"><i /></span>
        </button>
        <div className="mp2-cols" aria-hidden="true">
          <span>{hasScores ? 'Podle souladu se stanicí' : 'Modely'}</span>
          <span>Dnes</span>
          {hasScores && <span>Soulad</span>}
        </div>
        <div className="mp2-list" role="listbox" aria-label="Modely">
          {sorted.map((m, i) => {
            const active = effectiveModel === m.id;
            const auto = autoModel && autoPickId === m.id;
            const t = todayOf(m.id);
            const d = m.score?.diffs;
            const fit = m.score ? Math.round(m.score.total) : null;
            const sc = m.score;
            const cls = sc ? (sc.ok === sc.judged ? 'good' : sc.ok >= sc.judged - 2 ? 'warn' : 'bad') : '';
            const rain = t ? (t.precip || 0) : 0;
            return (
              <button type="button" key={m.id} data-opt role="option" aria-selected={active}
                      className={`mp2-row ${active ? 'active' : ''} ${auto ? 'is-auto' : ''}`}
                      title={d ? `Odchylka od stanice teď: teplota ${window.czNum(d.dT, 1)} °C · vlhkost ${d.dH} % · tlak ${d.dP} hPa · vítr ${window.czNum(d.dW, 1)} m/s · nárazy ${window.czNum(d.dG, 1)} m/s` : undefined}
                      onClick={() => choose(() => onPick(m.id))}>
                <span className="mp2-rank">{active ? <i className="bi bi-check-lg" aria-hidden="true"></i> : (m.score ? i + 1 : '')}</span>
                <span className="mp2-name">
                  <b>{m.name}{auto && <span className="mp2-auto-badge">AUTO</span>}</b>
                  {m.sub && <small>{m.sub}</small>}
                </span>
                <span className="mp2-today">
                  {t ? <>
                    <img src={WeatherData.iconUrl(t.icon)} alt={t.cond || ''} title={t.cond || ''} />
                    <span className="mp2-t">{tU(t.tmin)}–{tU(t.tmax)}°</span>
                    <span className={`mp2-r ${rain < 0.1 ? 'is-zero' : ''}`}><i className="bi bi-droplet-fill" aria-hidden="true"></i>{rain < 0.1 ? '0' : window.czNum(rain, rain < 10 ? 1 : 0)}</span>
                  </> : <span className="mp2-r is-zero">—</span>}
                </span>
                {hasScores && (
                  <span className={`mp2-fit ${cls}`} title={sc ? `${sc.ok} z ${sc.judged} veličin v souladu se stanicí · skóre ${fit} %` : undefined}>
                    <b>{sc ? `${sc.ok}/${sc.judged}` : '—'}</b>
                    <span className="mp2-bar" aria-hidden="true"><i style={{ width: (fit || 0) + '%' }}></i></span>
                    {sc && <small>{fit} %</small>}
                  </span>
                )}
              </button>
            );
          })}
        </div>
        <div className="mp2-foot">
          {hasScores
            ? 'Pořadí: nejdřív kolik veličin je v souladu se stanicí (teplota ±1 °C, vlhkost ±7 %, tlak ±2 hPa, vítr ±1,5 m/s, nárazy ±2,5 m/s), při shodě skóre (teplota 40 %, ostatní po 15 %).'
            : 'Bez dat ze stanice se shoda modelů nedá spočítat.'}
        </div>
      </div>
    </>
  );

  // Trigger = plain status text like the station page's „měření 17:30“:
  // „model Yr.no (auto) ⌄“, no pill, no badge.
  return (
    <div className="model-picker">
      <button ref={btnRef} type="button"
              className="model-picker-btn"
              onClick={() => setOpen(o => !o)}
              title={autoModel ? `Model předpovědi: ${activeName} (automatický výběr)` : `Model předpovědi: ${activeName}`}
              aria-haspopup="dialog" aria-expanded={open}>
        <span className="model-picker-name"><span className="model-picker-pre">model </span>{activeName}{autoModel ? ' (auto)' : ''}</span>
        <i className="bi bi-chevron-down" aria-hidden="true"/>
      </button>
      {popNode && ReactDOM.createPortal(popNode, document.body)}
    </div>
  );
}

// Ikona předvolby: vlastní kresba (`glyph`, stejná jako v přepínači stránek),
// jinak Bootstrap Icons (`icon`).
function PresetIcon({ p, className = '' }) {
  if (p.glyph === 'stanice') return <StationGlyph className={`pi-ico ${className}`.trim()} />;
  return <i className={`bi bi-${p.icon || 'star'} ${className}`.trim()} aria-hidden="true"/>;
}

// ─────────────────────────────────────────────────────────────────────
// PresetBar — layout presets right on the page, one tap to switch. Managing
// them (rename, overwrite, delete, hide from the bar, save new) is in the
// Vzhled panel (TweaksDrawer). Active = preset whose visible tiles + spans
// match the current layout (samePresetLayout).
// ─────────────────────────────────────────────────────────────────────
function PresetBar({ editing, modelPicker }) {
  const [, setTick] = useState(0);
  useEffect(() => {
    const bump = () => setTick(t => t + 1);
    document.addEventListener('dash-layout', bump);
    window.addEventListener('presets-change', bump);
    return () => {
      document.removeEventListener('dash-layout', bump);
      window.removeEventListener('presets-change', bump);
    };
  }, []);
  const hidden = window.loadHiddenPresets?.() || [];
  const presets = (window.getAllPresets?.() || []).filter(p => !hidden.includes(p.id));
  const current = window.__dash_currentLayout;
  if (!presets.length && !modelPicker) return null;
  return (
    <nav className={`preset-bar ${editing ? 'is-editing' : ''}`} aria-label="Předvolby rozložení a model předpovědi">
      <span className="preset-bar-lbl"><i className="bi bi-grid-1x2" aria-hidden="true"/>Rozložení</span>
      {presets.length > 0 && <div className="preset-bar-list">
        {presets.map(p => (
          <span key={p.id} className="preset-chip-wrap">
            <button type="button" className="preset-chip"
                    aria-pressed={samePresetLayout(p.layout, current)}
                    title={[p.short && p.name, p.desc].filter(Boolean).join(' — ') || p.name}
                    onClick={() => window.__dash_setLayout?.(p.layout)}>
              <PresetIcon p={p} />
              <span>{p.short || p.name}</span>
            </button>
            {/* edit mode: hide any preset (built-in too) from the bar; back via Vrátit or Vzhled */}
            {editing && (
              <button type="button" className="preset-chip-x" title="Skrýt z lišty" aria-label={`Skrýt ${p.name} z lišty`}
                      onClick={() => {
                        window.setPresetHidden?.(p.id, true);
                        window.showToast?.(`${p.name}: skryto z lišty`, { icon: 'eye-slash',
                          action: { label: 'Vrátit', onClick: () => window.setPresetHidden?.(p.id, false) } });
                      }}>
                <i className="bi bi-x" aria-hidden="true"/>
              </button>
            )}
          </span>
        ))}
      </div>}
      {/* model vpravo (mřížka v CSS), předvolby zůstanou uprostřed pod přepínačem stránek */}
      {modelPicker && <div className="preset-bar-model">{modelPicker}</div>}
    </nav>
  );
}

// ─────────────────────────────────────────────────────────────────────
// EditPill — floating edit toolbar: hint, Zpět / Znovu (Ctrl+Z / Ctrl+Y),
// Přidat widget, Zrušit (back to the layout from before editing — only when
// something changed) and Hotovo. The history lives in Dashboard
// (window.__dash_history, event `dash-history`).
// ─────────────────────────────────────────────────────────────────────
function EditPill({ onAdd, onDone }) {
  const [h, setH] = useState(() => window.__dash_history?.state || { undo: 0, redo: 0, dirty: false });
  useEffect(() => {
    const on = (e) => setH(e.detail);
    document.addEventListener('dash-history', on);
    return () => document.removeEventListener('dash-history', on);
  }, []);
  const H = () => window.__dash_history;
  return (
    <div className="edit-pill" role="toolbar" aria-label="Úpravy rozložení">
      <span className="edit-pill-hint">
        <i className="bi bi-arrows-move" aria-hidden="true"></i>
        Přetáhni widget na jiné místo ·
        <span className="eph-desk">okraje mění velikost</span>
        <span className="eph-narrow">velikost v nastavení widgetu</span>
      </span>
      <span className="ep-group">
        <button type="button" className="ep-icon" disabled={!h.undo} onClick={() => H()?.undo()}
                title="Zpět (Ctrl+Z)" aria-label="Zpět">
          <i className="bi bi-arrow-counterclockwise" aria-hidden="true"></i>
        </button>
        <button type="button" className="ep-icon" disabled={!h.redo} onClick={() => H()?.redo()}
                title="Znovu (Ctrl+Shift+Z)" aria-label="Znovu">
          <i className="bi bi-arrow-clockwise" aria-hidden="true"></i>
        </button>
      </span>
      <button type="button" onClick={onAdd} title="Přidat widget">
        <i className="bi bi-plus-lg" aria-hidden="true"></i><span className="ep-lbl">Přidat widget</span>
      </button>
      {h.dirty && (
        <button type="button" onClick={() => { H()?.revert(); onDone(); }}
                title="Zrušit úpravy — rozložení jako před nimi">
          <i className="bi bi-x-lg" aria-hidden="true"></i><span className="ep-lbl">Zrušit</span>
        </button>
      )}
      <button type="button" className="primary" onClick={onDone}>
        <i className="bi bi-check2" aria-hidden="true"></i>Hotovo
      </button>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────
// CatalogOverlay — same as v1
// ─────────────────────────────────────────────────────────────────────
function CatalogOverlay({ onClose }) {
  const node = (
    <div className="catalog-overlay" onClick={onClose}>
      <div className="catalog" onClick={e => e.stopPropagation()}>
        <button className="topbar-btn catalog-close" onClick={onClose} aria-label="Zavřít"><i className="bi bi-x-lg"></i></button>
        <h3>Přidat / vrátit widget</h3>
        <p className="lead">Vyberte, který blok chcete zobrazit na dashboardu. Skryté widgety zůstanou v knihovně.</p>
        <div className="catalog-grid">
          {Object.values(window.WIDGET_CATALOG).map(meta => {
            const isHidden = (window.__dash_hidden || []).includes(meta.id);
            return (
              <button key={meta.id} className="catalog-item"
                disabled={!isHidden}
                onClick={() => { window.__dash_show && window.__dash_show(meta.id); onClose(); }}>
                <div className="ico"><i className={`bi bi-${meta.icon}`}></i></div>
                <div>
                  <div className="name">{meta.title} {!isHidden && <small style={{color:'var(--ok)',fontWeight:600}}>· už zobrazeno</small>}</div>
                  <div className="desc">{meta.desc}</div>
                </div>
              </button>
            );
          })}
        </div>
      </div>
    </div>
  );
  return ReactDOM.createPortal(node, document.body);
}

// ─────────────────────────────────────────────────────────────────────
// TweaksDrawer — panel „Vzhled a rozložení". Same shell (tweaks-panel.jsx)
// restyled in app colours (styles-v2.css, "Vzhled panel"), grouped into
// sections; presets on top. Theme / accent / variant are shared with the
// station page (/ms) through pm_tweaks_v1.
// ─────────────────────────────────────────────────────────────────────

// Section with an accent icon + uppercase heading (same language as widget heads).
function VzSection({ icon, title, hint, children }) {
  return (
    <section className="vz-sect">
      <div className="vz-sect-h"><i className={`bi bi-${icon}`} aria-hidden="true"></i>{title}</div>
      {hint && <div className="vz-hint">{hint}</div>}
      {children}
    </section>
  );
}

// Button-grid radio: [{ value, label, icon?, desc? }]. `cols` = columns.
function VzChoices({ label, value, options, onChange, cols = 3 }) {
  return (
    <div className="vz-choices" role="radiogroup" aria-label={label} style={{ '--cols': cols }}>
      {options.map(o => (
        <button key={o.value} type="button" className="vz-choice" role="radio"
                aria-checked={o.value === value} onClick={() => onChange(o.value)}
                title={o.desc || o.label}>
          {o.glyph || (o.icon && <i className={`bi bi-${o.icon}`} aria-hidden="true"></i>)}
          <span>{o.label}</span>
          {o.desc && <small>{o.desc}</small>}
        </button>
      ))}
    </div>
  );
}

// Which preset matches the current layout (order of visible tiles + spans).
function samePresetLayout(a, b) {
  if (!a || !b) return false;
  const vis = (l) => l.order.filter(id => !(l.hidden || []).includes(id));
  const va = vis(a), vb = vis(b);
  if (va.join(',') !== vb.join(',')) return false;
  const ro = (l, id) => ((l.rows || {})[id] || 1);
  return va.every(id => (a.spans || {})[id] === (b.spans || {})[id] && ro(a, id) === ro(b, id));
}
// Celé rozložení stejné: navíc výšky a nastavení viditelných widgetů.
function sameFullLayout(a, b) {
  if (!samePresetLayout(a, b)) return false;
  const js = (o) => JSON.stringify(o || {});
  return a.order.filter(id => !(a.hidden || []).includes(id)).every(id =>
    ((a.heights || {})[id] || 0) === ((b.heights || {})[id] || 0)
    && js((a.widgetConfig || {})[id]) === js((b.widgetConfig || {})[id]));
}

function TweaksDrawer({ tweaks, setTweak, autoModel, setAutoModel, onClose, editing, setEditing }) {
  // Presets are switched here (click a row) and, when it's on, in the
  // on-page PresetBar; the panel also manages them.
  const [, setPresetTick] = useState(0);
  useEffect(() => {
    const bump = () => setPresetTick(t => t + 1);
    document.addEventListener('dash-layout', bump);
    window.addEventListener('presets-change', bump);
    return () => {
      document.removeEventListener('dash-layout', bump);
      window.removeEventListener('presets-change', bump);
    };
  }, []);
  const presets = window.getAllPresets?.() || [];
  const hiddenIds = window.loadHiddenPresets?.() || [];
  const current = window.__dash_currentLayout;
  const themeValue = tweaks.autoTheme ? 'auto' : (tweaks.theme || 'dark');
  const [vychozi, setVychozi] = useState(cteVychozi);
  const savePreset = () => {
    const name = window.prompt('Název předvolby:', 'Moje rozložení');
    if (!name || !name.trim() || !current) return;
    window.savePresetFromLayout?.(name.trim(), current);
    window.showToast?.(`Předvolba „${name.trim()}" uložena`);
  };
  const renamePreset = (p) => {
    const name = window.prompt('Nový název předvolby:', p.name);
    if (!name || !name.trim() || name.trim() === p.name) return;
    window.renameCustomPreset?.(p.id, name.trim());
  };
  const overwritePreset = (p) => {
    if (!current || !window.confirm(`Uložit současné rozložení do předvolby „${p.name}"?`)) return;
    window.overwriteCustomPreset?.(p.id, current);
    window.showToast?.(`Předvolba „${p.name}" přepsána`);
  };
  const deletePreset = (p) => {
    if (!window.confirm(`Smazat předvolbu „${p.name}"?`)) return;
    window.deleteCustomPreset?.(p.id);
  };

  return (
    <TweaksPanel title="Vzhled a rozložení" onClose={onClose}>
      <VzSection icon="grid-1x2" title="Rozložení"
                 hint="Klepnutím rozložení přepnete. Vlastní jde přejmenovat, přepsat a smazat. Lištu s rozloženími nad widgety zapnete níž, oko pak určí, co v ní bude.">
        <div className="vz-plist">
          {presets.map(p => {
            const off = hiddenIds.includes(p.id);
            const now = samePresetLayout(p.layout, current);
            return (
              <div key={p.id} className={`vz-prow ${off && tweaks.showPresetBar !== false ? 'is-off' : ''} ${now ? 'is-now' : ''}`}>
                <button type="button" className="vz-prow-pick" aria-pressed={now}
                        title={p.desc ? `${p.name} — ${p.desc}` : p.name}
                        onClick={() => { if (!now) { window.__dash_setLayout?.(p.layout); window.showToast?.(`Rozložení „${p.name}“`); } }}>
                  <PresetIcon p={p} className="vz-prow-ico" />
                  <span className="vz-prow-name">{p.name}</span>
                  {now && <span className="vz-prow-now">teď</span>}
                </button>
                {!p.builtin && (
                  <>
                    <button type="button" className="vz-ibtn" title="Přejmenovat" aria-label={`Přejmenovat ${p.name}`}
                            onClick={() => renamePreset(p)}><i className="bi bi-pencil" aria-hidden="true"></i></button>
                    <button type="button" className="vz-ibtn" title="Přepsat současným rozložením" aria-label={`Přepsat ${p.name}`}
                            onClick={() => overwritePreset(p)}><i className="bi bi-arrow-repeat" aria-hidden="true"></i></button>
                    <button type="button" className="vz-ibtn danger" title="Smazat" aria-label={`Smazat ${p.name}`}
                            onClick={() => deletePreset(p)}><i className="bi bi-trash3" aria-hidden="true"></i></button>
                  </>
                )}
                {tweaks.showPresetBar !== false && (
                  <button type="button" className="vz-ibtn" aria-pressed={!off}
                          title={off ? 'Ukázat v liště' : 'Skrýt z lišty'} aria-label={off ? `Ukázat ${p.name} v liště` : `Skrýt ${p.name} z lišty`}
                          onClick={() => window.setPresetHidden?.(p.id, !off)}>
                    <i className={`bi bi-${off ? 'eye-slash' : 'eye'}`} aria-hidden="true"></i>
                  </button>
                )}
              </div>
            );
          })}
        </div>
        <TweakToggle label="Lišta s rozloženími nad widgety" value={tweaks.showPresetBar !== false}
                     onChange={(v) => setTweak('showPresetBar', v)} />
        <div className="vz-actions">
          <button type="button" className="vz-btn" onClick={() => setEditing(e => !e)}>
            <i className={`bi bi-${editing ? 'check2' : 'pencil-square'}`} aria-hidden="true"></i>
            {editing ? 'Hotovo' : 'Upravit widgety'}
          </button>
          <button type="button" className="vz-btn" onClick={savePreset}>
            <i className="bi bi-bookmark-plus" aria-hidden="true"></i>Uložit předvolbu
          </button>
        </div>
      </VzSection>

      <VzSection icon="house-door" title="Výchozí stránka"
                 hint="Co otevře adresa lokality bez stránky (…/lokalita/?lokalita). Platí i pro stránku stanice.">
        <VzChoices label="Výchozí stránka" value={vychozi} cols={3} options={VYCHOZI_STRANKY}
          onChange={(v) => { zapisVychozi(v); setVychozi(v); }} />
      </VzSection>

      <VzSection icon="circle-half" title="Motiv" hint="Platí i pro stránku stanice.">
        <VzChoices label="Motiv" value={themeValue} cols={3}
          options={[
            { value: 'light', label: 'Světlý', icon: 'sun' },
            { value: 'dark',  label: 'Tmavý',  icon: 'moon-stars' },
            { value: 'auto',  label: 'Podle slunce', icon: 'sunrise' },
          ]}
          onChange={(v) => v === 'auto' ? setTweak({ autoTheme: true })
                                        : setTweak({ theme: v, autoTheme: false })} />
      </VzSection>

      <VzSection icon="palette" title="Styl a barva">
        <VzChoices label="Styl" value={tweaks.variant || 'aurora'} cols={2}
          options={[
            { value: 'aurora',  label: 'Aurora',  desc: 'sklo a barevná záře' },
            { value: 'studio',  label: 'Studio',  desc: 'plné plochy, stíny' },
            { value: 'console', label: 'Konzole', desc: 'terminál, strojové písmo' },
            { value: 'ledger',  label: 'Papír',   desc: 'noviny, patkové písmo' },
          ]}
          onChange={(v) => setTweak('variant', v)} />
        <TweakColor label="Akcent" value={tweaks.accent}
                    options={['#38bdf8','#a78bfa','#34d399','#fbbf24','#fb7185','#fb923c','#22d3ee','#818cf8']}
                    onChange={v=>setTweak('accent',v)} />
        <TweakToggle label="Akcent podle teploty" value={!!tweaks.dynamicAccent}
                     onChange={(v) => setTweak('dynamicAccent', v)} />
        <TweakToggle label="Akcent i v grafech a přístrojích" value={!!tweaks.useAccent}
                     onChange={(v) => setTweak('useAccent', v)} />
        <TweakToggle label="Dlaždice (podklad a rámeček widgetů)" value={tweaks.tiles !== false}
                     onChange={(v) => setTweak('tiles', v)} />
      </VzSection>

      <VzSection icon="aspect-ratio" title="Hustota a velikost" hint="Platí i pro stránku stanice.">
        <VzChoices label="Hustota" value={tweaks.density || 'compact'} cols={2}
          options={[
            { value: 'ultra',       label: 'Hustá' },
            { value: 'compact',     label: 'Kompaktní' },
            { value: 'comfortable', label: 'Běžná' },
            { value: 'spacious',    label: 'Vzdušná' },
          ]}
          onChange={(v) => setTweak('density', v)} />
        <TweakSlider label="Velikost písma" value={tweaks.fontScale ?? 1}
                     min={0.85} max={1.3} step={0.05} unit="×"
                     onChange={v=>setTweak('fontScale',v)} />
        <TweakSlider label="Mezery mezi dlaždicemi" value={tweaks.gridGap ?? 10}
                     min={2} max={28} step={1} unit=" px"
                     onChange={v=>setTweak('gridGap',v)} />
        <TweakSlider label="Zaoblení rohů" value={tweaks.radiusScale ?? 1}
                     min={0} max={1.6} step={0.1} unit="×"
                     onChange={v=>setTweak('radiusScale',v)} />
      </VzSection>

      <VzSection icon="window-stack" title="Dlaždice a záhlaví">
        <TweakToggle label="Skrýt nadpisy widgetů" value={!!tweaks.hideTitles}
                     onChange={v=>setTweak('hideTitles',v)} />
        <TweakSlider label="Min. šířka dlaždic čidel" value={tweaks.sensorMin ?? 130}
                     min={90} max={220} step={10} unit=" px"
                     onChange={v=>setTweak('sensorMin',v)} />
        <TweakToggle label="Záhlaví vždy nahoře" value={tweaks.stickyHeader !== false}
                     onChange={v=>setTweak('stickyHeader',v)} />
        <TweakToggle label="Pravděpodobnost srážek v záhlaví" value={tweaks.showQuickPills !== false}
                     onChange={v=>setTweak('showQuickPills',v)} />
      </VzSection>

      <VzSection icon="camera-video" title="Webkamera" hint="Platí i pro stránku stanice.">
        <TweakToggle label="Noční snímky webkamery" value={!!tweaks.webcamNoc}
                     onChange={v=>setTweak('webcamNoc',v)} />
      </VzSection>

      <VzSection icon="wind" title="Vítr">
        <VzChoices label="Jednotka větru" value={tweaks.unitWind || 'mps'} cols={4}
          options={[
            { value: 'mps', label: 'm/s' }, { value: 'kmh', label: 'km/h' },
            { value: 'mph', label: 'mph' }, { value: 'kn',  label: 'uzly' },
          ]}
          onChange={(v) => setTweak('unitWind', v)} />
        <TweakToggle label="Animované proudění" value={tweaks.windFlow !== false}
                     onChange={v=>setTweak('windFlow',v)} />
        {tweaks.windFlow !== false && (
          <TweakSlider label="Rychlost animace" value={tweaks.windFlowSpeed || 1}
                       min={0.3} max={3} step={0.1} unit="×"
                       onChange={v=>setTweak('windFlowSpeed',v)} />
        )}
      </VzSection>

      <VzSection icon="graph-up" title="Grafy a předpověď">
        <TweakToggle label="Kompaktní grafy" value={!!tweaks.compactCharts}
                     onChange={v=>setTweak('compactCharts',v)} />
        <TweakToggle label="Výplň pod křivkou" value={tweaks.chartFillArea !== false}
                     onChange={v=>setTweak('chartFillArea',v)} />
        <TweakToggle label="Značky minima a maxima" value={tweaks.chartMinMax !== false}
                     onChange={v=>setTweak('chartMinMax',v)} />
        <TweakToggle label="Vybírat nejpřesnější model (AUTO)" value={autoModel}
                     onChange={v=>setAutoModel(v)} />
      </VzSection>

      <VzSection icon="save" title="Záloha nastavení" hint="Vzhled, rozložení i předvolby do souboru — třeba pro jiný počítač.">
        <div className="vz-actions">
          <button type="button" className="vz-btn" onClick={exportConfig}>
            <i className="bi bi-download" aria-hidden="true"></i>Uložit do souboru
          </button>
          <button type="button" className="vz-btn" onClick={() => {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = '.json,application/json';
            input.onchange = (e) => importConfig(e.target.files[0]);
            input.click();
          }}>
            <i className="bi bi-upload" aria-hidden="true"></i>Načíst ze souboru
          </button>
        </div>
        <div className="vz-actions">
          <button type="button" className="vz-btn vz-btn-quiet"
                  onClick={() => { window.__dash_reset?.(); window.showToast?.('Rozložení obnoveno'); setPresetTick(t => t + 1); }}>
            <i className="bi bi-arrow-counterclockwise" aria-hidden="true"></i>Výchozí rozložení
          </button>
          <button type="button" className="vz-btn vz-btn-quiet"
                  onClick={() => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true }))}>
            <i className="bi bi-command" aria-hidden="true"></i>Příkazy (Ctrl+K)
          </button>
        </div>
      </VzSection>
    </TweaksPanel>
  );
}

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