/* Office Sidekick — Additional tools + compact dashboard widgets */

const { useState: useStateM, useEffect: useEffectM, useRef: useRefM, useMemo: useMemoM, useCallback: useCallbackM } = React;

/* ============================================================
   WATERCOOLER — break-time content
   Modes: Dad jokes · Fun facts · Good news · Sports trivia
   Uses a curated local library + optional fresh AI-generated content
   ============================================================ */
const WC_LIB = {
  joke: [
    "I told my wife she was drawing her eyebrows too high. She looked surprised.",
    "Why don't skeletons fight each other? They don't have the guts.",
    "I'm reading a book about anti-gravity. It's impossible to put down.",
    "I used to hate facial hair, but then it grew on me.",
    "Parallel lines have so much in common… it's a shame they'll never meet.",
    "I asked the librarian if they had books on paranoia. She whispered, 'They're right behind you.'",
    "What do you call cheese that isn't yours? Nacho cheese.",
    "Why did the scarecrow win an award? He was outstanding in his field.",
    "I'm on a seafood diet. I see food and I eat it.",
    "Did you hear about the mathematician who's afraid of negative numbers? He'll stop at nothing to avoid them.",
  ],
  fact: [
    "Octopuses have three hearts, nine brains, and blue blood — and two of the hearts stop beating when they swim.",
    "A day on Venus is longer than its year. Venus rotates once every 243 Earth days but orbits the Sun every 225.",
    "Bananas are berries. Strawberries aren't.",
    "There's enough DNA in a single human body, stretched end-to-end, to reach the Sun and back ~600 times.",
    "Honey never spoils. Archaeologists have eaten 3,000-year-old honey from Egyptian tombs.",
    "Wombat poop is cube-shaped — and scientists only figured out why in 2018.",
    "The shortest war in history was between Britain and Zanzibar in 1896. It lasted 38 minutes.",
    "A group of flamingos is called a 'flamboyance.'",
    "Sharks have been around longer than trees — by about 50 million years.",
    "Your stomach gets a new lining every 3-4 days, otherwise it would digest itself.",
  ],
  news: [
    "Reforestation projects in West Africa restored over 5 million hectares of degraded land in 2025 — beating their own target by 18 months.",
    "Researchers at MIT demonstrated a battery that can charge to 80% in under 5 minutes using sodium-ion chemistry — no lithium required.",
    "Wild beaver populations in the UK passed 2,000 individuals for the first time since the 16th century, with measurable improvements to local flood resilience.",
    "A community-led ocean cleanup off the coast of Indonesia removed over 1,200 tonnes of plastic from coral reef zones this year.",
    "A new vaccine for malaria reached 75% efficacy in trials — the first to clear the WHO's threshold for widespread rollout.",
    "Solar generation overtook coal in global electricity production for the first calendar quarter in 2026.",
    "Snow leopards were officially downgraded from 'endangered' to 'vulnerable' after two decades of conservation work in Central Asia.",
    "A teen-led literacy program in rural Kenya distributed its 1 millionth book this spring.",
    "Cycling infrastructure investment in EU cities tripled between 2020 and 2025, and traffic fatalities dropped in every major adopting city.",
    "Researchers grew a kidney organoid from stem cells that filtered waste for 30+ days — a major step toward lab-grown transplants.",
  ],
  sport: [
    "The longest tennis match in history lasted 11 hours, 5 minutes — Isner vs Mahut at Wimbledon 2010. They played for three days.",
    "Cricket's longest Test match (Durban, 1939) was abandoned as a draw after 9 days because the England team had to catch a boat home.",
    "In NBA history, only one player has scored 100 points in a game: Wilt Chamberlain in 1962. There's no video footage of it.",
    "Football's offside rule has been simplified 5 times since 1863. The current version still confuses literally everyone.",
    "A marathon is 26.2 miles because Queen Alexandra wanted the race in 1908 to start at Windsor Castle and finish in front of the royal box.",
    "Formula 1 cars produce so much downforce they could theoretically drive upside-down on a ceiling at 130+ mph.",
    "The Stanley Cup is older than the NHL. It was first awarded in 1893; the NHL was founded in 1917.",
    "An Olympic gold medal is mostly silver — only about 6 grams of gold plating.",
    "Brazilian goalkeeper Rogério Ceni scored 131 career goals — most by any goalkeeper in football history.",
    "Babe Ruth's salary in 1930 was higher than the US President's. Asked why, he reportedly said: 'I had a better year than him.'",
  ],
};

const WC_MODES = [
  { id: "joke",  label: "Dad joke",     icon: "😄", color: "amber",  promptHint: "a clean, family-friendly one-line dad joke" },
  { id: "fact",  label: "Fun fact",     icon: "💡", color: "blue",   promptHint: "a surprising, well-sourced fun fact (1-2 sentences)" },
  { id: "news",  label: "Good news",    icon: "🌱", color: "green",  promptHint: "a piece of positive, verifiable good news from the last year (1-2 sentences)" },
  { id: "sport", label: "Sports trivia", icon: "🏆", color: "violet", promptHint: "a fascinating, true sports trivia fact (1-2 sentences)" },
];

function useWatercoolerContent(initialMode = "joke") {
  const [mode, setMode] = useStateM(initialMode);
  const [item, setItem] = useStateM(() => WC_LIB[initialMode][0]);
  const [seen, setSeen] = useStateM({ joke: 0, fact: 0, news: 0, sport: 0 });
  const [aiLoading, setAiLoading] = useStateM(false);
  const [aiNote, setAiNote] = useStateM(null); // "fresh from AI" badge

  const shuffle = useCallbackM(() => {
    const lib = WC_LIB[mode];
    let idx;
    do { idx = Math.floor(Math.random() * lib.length); } while (idx === seen[mode] && lib.length > 1);
    setSeen(s => ({ ...s, [mode]: idx }));
    setItem(lib[idx]);
    setAiNote(null);
  }, [mode, seen]);

  const switchMode = useCallbackM((newMode) => {
    setMode(newMode);
    setItem(WC_LIB[newMode][seen[newMode] || 0]);
    setAiNote(null);
  }, [seen]);

  const askAI = useCallbackM(async () => {
    if (!window.claude?.complete) {
      setAiNote("AI unavailable");
      return;
    }
    setAiLoading(true);
    setAiNote(null);
    try {
      const m = WC_MODES.find(x => x.id === mode);
      const prompt = `Give me ${m.promptHint}. Reply with ONLY the content — no preamble, no quotes around it, no "here's a..." intro. Keep it under 280 characters.`;
      const reply = await window.claude.complete(prompt);
      const clean = (reply || "").trim().replace(/^["']|["']$/g, "");
      if (clean) {
        setItem(clean);
        setAiNote("fresh from AI");
      } else {
        setAiNote("AI returned nothing");
      }
    } catch (e) {
      setAiNote("AI error — try again");
    } finally {
      setAiLoading(false);
    }
  }, [mode]);

  return { mode, item, switchMode, shuffle, askAI, aiLoading, aiNote };
}

function WatercoolerTool() {
  const wc = useWatercoolerContent("joke");
  const mode = WC_MODES.find(m => m.id === wc.mode);

  return (
    <div style={{ display: "grid", gap: 18 }}>
      <div className="card wc-card" style={{ minHeight: 340 }}>
        <div className="chips wc-tabs">
          {WC_MODES.map(m => (
            <button
              key={m.id}
              className={"chip wc-tab " + (wc.mode === m.id ? "active" : "")}
              onClick={() => wc.switchMode(m.id)}
            >
              <span className="wc-tab-ic">{m.icon}</span>
              <span>{m.label}</span>
            </button>
          ))}
        </div>

        <div className="wc-stage">
          <div className={"wc-quote " + (mode.color || "")} key={wc.item /* re-mounts on change for fade */}>
            <div className="wc-quote-mark">"</div>
            <p>{wc.item}</p>
          </div>

          <div className="wc-meta">
            {wc.aiNote && <span className="wc-pill">✨ {wc.aiNote}</span>}
            <span className="wc-pill subtle">{mode.label} · {wc.item.length} chars</span>
          </div>
        </div>

        <div className="wc-actions">
          <button className="btn btn-primary" onClick={wc.shuffle}>
            🔀  Next {mode.label.toLowerCase()}
          </button>
          <button className="btn btn-ghost" onClick={wc.askAI} disabled={wc.aiLoading}>
            {wc.aiLoading ? "✨ Thinking…" : "✨ Fresh from AI"}
          </button>
          <button className="btn btn-ghost" onClick={() => navigator.clipboard?.writeText(wc.item)}>
            📋  Copy
          </button>
        </div>
      </div>

      <div className="card" style={{ padding: "14px 18px" }}>
        <p className="sub" style={{ fontSize: 12.5, margin: 0, lineHeight: 1.6 }}>
          <b style={{ color: "var(--vps-text-secondary)" }}>The 90-second mental reset.</b> Step away from the spreadsheet,
          read one of these out loud, and come back sharper. Sports trivia and good news are curated from public reporting; jokes and
          fun facts are evergreen. Hit <b>✨ Fresh from AI</b> if you want something brand new.
        </p>
      </div>
    </div>
  );
}

function WatercoolerCompact() {
  const wc = useWatercoolerContent("joke");
  const mode = WC_MODES.find(m => m.id === wc.mode);
  return (
    <div className="wc-compact">
      <div className="wc-compact-tabs">
        {WC_MODES.map(m => (
          <button
            key={m.id}
            className={"wc-compact-tab " + (wc.mode === m.id ? "active" : "")}
            onClick={() => wc.switchMode(m.id)}
            title={m.label}
          >
            <span>{m.icon}</span>
          </button>
        ))}
      </div>
      <div className={"wc-compact-quote " + (mode.color || "")}>
        <p>{wc.item}</p>
      </div>
      <button className="wc-compact-next" onClick={wc.shuffle}>
        🔀  Another {mode.label.toLowerCase()}
      </button>
    </div>
  );
}

/* ============================================================
   STOPWATCH
   ============================================================ */
function useStopwatch() {
  const [elapsed, setElapsed] = useStateM(0);
  const [running, setRunning] = useStateM(false);
  const startedAtRef = useRefM(null);
  const baseRef = useRefM(0);
  const rafRef = useRefM(null);
  const [laps, setLaps] = useStateM([]);

  useEffectM(() => {
    if (!running) return;
    const tick = () => {
      setElapsed(baseRef.current + (Date.now() - startedAtRef.current));
      rafRef.current = requestAnimationFrame(tick);
    };
    rafRef.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(rafRef.current);
  }, [running]);

  const start = () => {
    startedAtRef.current = Date.now();
    setRunning(true);
  };
  const stop = () => {
    baseRef.current += Date.now() - startedAtRef.current;
    setRunning(false);
  };
  const reset = () => {
    baseRef.current = 0;
    setElapsed(0);
    setRunning(false);
    setLaps([]);
  };
  const lap = () => setLaps(ls => [{ at: elapsed, n: ls.length + 1 }, ...ls]);

  return { elapsed, running, start, stop, reset, lap, laps };
}

function fmtElapsed(ms) {
  const total = Math.max(0, ms);
  const h = Math.floor(total / 3600000);
  const m = Math.floor((total % 3600000) / 60000);
  const s = Math.floor((total % 60000) / 1000);
  const cs = Math.floor((total % 1000) / 10);
  const base = `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${String(cs).padStart(2, "0")}`;
  return h > 0 ? `${String(h).padStart(2, "0")}:${base}` : base;
}

function StopwatchTool() {
  const sw = useStopwatch();
  return (
    <div className="card" style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: "40px 24px" }}>
      <span className="label" style={{ margin: 0 }}>{sw.running ? "Running" : sw.elapsed > 0 ? "Paused" : "Ready"}</span>
      <div style={{
        fontFamily: "var(--font-mono)", fontWeight: 700,
        fontSize: "clamp(72px, 14vw, 140px)", lineHeight: 1, margin: "16px 0",
        background: "linear-gradient(135deg, var(--vps-text-primary) 25%, var(--vps-blue-200) 70%, var(--vps-amber-200) 100%)",
        WebkitBackgroundClip: "text", backgroundClip: "text", color: "transparent",
        letterSpacing: "-0.02em",
      }}>{fmtElapsed(sw.elapsed)}</div>
      <div className="row" style={{ justifyContent: "center", marginTop: 12 }}>
        {!sw.running
          ? <button className="btn btn-primary" onClick={sw.start}>▶ Start</button>
          : <button className="btn btn-danger" onClick={sw.stop}>■ Stop</button>}
        <button className="btn btn-ghost" onClick={sw.lap} disabled={!sw.running}>⚑ Lap</button>
        <button className="btn btn-ghost" onClick={sw.reset}>Reset</button>
      </div>
      {sw.laps.length > 0 && (
        <div style={{ marginTop: 28, width: "100%", maxWidth: 460 }}>
          <div className="label">Laps</div>
          <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
            {sw.laps.map((l, i) => {
              const prev = sw.laps[i + 1];
              const split = prev ? l.at - prev.at : l.at;
              return (
                <div key={l.n} className="mono" style={{
                  display: "grid", gridTemplateColumns: "60px 1fr 1fr",
                  gap: 12, padding: "8px 12px", border: "1px solid var(--vps-border)",
                  borderRadius: 8, fontSize: 13,
                }}>
                  <span style={{ color: "var(--vps-text-muted)" }}>#{l.n}</span>
                  <span>{fmtElapsed(split)}</span>
                  <span style={{ color: "var(--vps-blue-300)", textAlign: "right" }}>{fmtElapsed(l.at)}</span>
                </div>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
}

function StopwatchCompact() {
  const sw = useStopwatch();
  return (
    <div className="sw-compact">
      <div className="sw-compact-time mono">{fmtElapsed(sw.elapsed)}</div>
      <div className="sw-compact-actions">
        {!sw.running
          ? <button className="btn btn-amber btn-sm" onClick={sw.start}>▶ Start</button>
          : <button className="btn btn-danger btn-sm" onClick={sw.stop}>■ Stop</button>}
        <button className="btn btn-ghost btn-sm" onClick={sw.reset}>Reset</button>
      </div>
    </div>
  );
}

/* ============================================================
   WEATHER (Open-Meteo, no key)
   ============================================================ */
const WX_CODES = {
  0:  { label: "Clear",                    icon: "☀️" },
  1:  { label: "Mainly clear",             icon: "🌤️" },
  2:  { label: "Partly cloudy",            icon: "⛅" },
  3:  { label: "Overcast",                 icon: "☁️" },
  45: { label: "Fog",                      icon: "🌫️" },
  48: { label: "Rime fog",                 icon: "🌫️" },
  51: { label: "Light drizzle",            icon: "🌦️" },
  53: { label: "Moderate drizzle",         icon: "🌦️" },
  55: { label: "Dense drizzle",            icon: "🌧️" },
  61: { label: "Slight rain",              icon: "🌦️" },
  63: { label: "Moderate rain",            icon: "🌧️" },
  65: { label: "Heavy rain",               icon: "🌧️" },
  71: { label: "Light snow",               icon: "🌨️" },
  73: { label: "Moderate snow",            icon: "🌨️" },
  75: { label: "Heavy snow",               icon: "❄️" },
  80: { label: "Rain showers",             icon: "🌦️" },
  81: { label: "Heavy showers",            icon: "🌧️" },
  82: { label: "Violent showers",          icon: "⛈️" },
  95: { label: "Thunderstorm",             icon: "⛈️" },
  96: { label: "Thunderstorm with hail",   icon: "⛈️" },
  99: { label: "Severe thunderstorm",      icon: "⛈️" },
};

const WX_PRESETS = [
  { name: "London",       lat: 51.5074, lon: -0.1278 },
  { name: "New York",     lat: 40.7128, lon: -74.0060 },
  { name: "San Francisco",lat: 37.7749, lon: -122.4194 },
  { name: "Tokyo",        lat: 35.6762, lon: 139.6503 },
  { name: "Sydney",       lat: -33.8688, lon: 151.2093 },
  { name: "Paris",        lat: 48.8566, lon: 2.3522 },
];

function useWeather(initial = WX_PRESETS[0]) {
  const [loc, setLoc] = useStateM(initial);
  const [data, setData] = useStateM(null);
  const [loading, setLoading] = useStateM(false);
  const [error, setError] = useStateM(null);
  const [unit, setUnit] = useStateM("C");

  const fetchWx = useCallbackM(async (l) => {
    setLoading(true); setError(null);
    try {
      const url = `https://api.open-meteo.com/v1/forecast?latitude=${l.lat}&longitude=${l.lon}&current=temperature_2m,apparent_temperature,weather_code,wind_speed_10m,relative_humidity_2m&daily=temperature_2m_max,temperature_2m_min&timezone=auto&temperature_unit=${unit === "C" ? "celsius" : "fahrenheit"}`;
      const res = await fetch(url);
      if (!res.ok) throw new Error("Weather API returned " + res.status);
      const json = await res.json();
      setData(json);
    } catch (e) {
      setError(e.message || "Couldn't reach Open-Meteo");
    } finally {
      setLoading(false);
    }
  }, [unit]);

  useEffectM(() => { fetchWx(loc); }, [loc, unit, fetchWx]);

  return { loc, setLoc, data, loading, error, unit, setUnit, refetch: () => fetchWx(loc) };
}

function WeatherTool() {
  const wx = useWeather();
  const code = wx.data?.current?.weather_code;
  const cond = WX_CODES[code] || { label: "—", icon: "🌡️" };
  const temp = wx.data?.current?.temperature_2m;
  const feels = wx.data?.current?.apparent_temperature;
  const wind = wx.data?.current?.wind_speed_10m;
  const humidity = wx.data?.current?.relative_humidity_2m;
  const tmax = wx.data?.daily?.temperature_2m_max?.[0];
  const tmin = wx.data?.daily?.temperature_2m_min?.[0];

  return (
    <div style={{ display: "grid", gap: 16 }}>
      <div className="card" style={{ display: "grid", gridTemplateColumns: "1fr auto", alignItems: "center", gap: 24, padding: "28px 28px" }}>
        <div>
          <div className="label" style={{ margin: 0 }}>Now in {wx.loc.name}</div>
          <div style={{ display: "flex", alignItems: "baseline", gap: 12, marginTop: 10 }}>
            <span style={{ fontSize: 64 }}>{cond.icon}</span>
            <div>
              <div style={{
                fontFamily: "var(--font-mono)", fontSize: "clamp(48px, 8vw, 88px)", lineHeight: 1, fontWeight: 700,
                background: "linear-gradient(135deg, var(--vps-text-primary) 25%, var(--vps-blue-200) 70%, var(--vps-amber-200) 100%)",
                WebkitBackgroundClip: "text", backgroundClip: "text", color: "transparent",
                letterSpacing: "-0.02em",
              }}>
                {wx.loading ? "…" : temp != null ? Math.round(temp) + "°" : "—"}
              </div>
              <div className="sub" style={{ marginTop: 4 }}>{cond.label}</div>
            </div>
          </div>
        </div>
        <div className="chips" style={{ flexDirection: "column", alignItems: "stretch", gap: 8 }}>
          <div className="chips">
            {["C", "F"].map(u => (
              <button key={u} className={"chip " + (wx.unit === u ? "active" : "")} onClick={() => wx.setUnit(u)}>°{u}</button>
            ))}
          </div>
        </div>
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(140px, 1fr))", gap: 12 }}>
        {[
          ["Feels like", feels != null ? Math.round(feels) + "°" : "—"],
          ["High today", tmax != null ? Math.round(tmax) + "°" : "—"],
          ["Low today",  tmin != null ? Math.round(tmin) + "°" : "—"],
          ["Wind",       wind != null ? wind.toFixed(1) + " km/h" : "—"],
          ["Humidity",   humidity != null ? humidity + "%" : "—"],
        ].map(([l, v]) => (
          <div key={l} className="card" style={{ padding: "14px 16px", textAlign: "center" }}>
            <div className="label" style={{ margin: 0 }}>{l}</div>
            <div className="mono" style={{ fontSize: 22, fontWeight: 600, marginTop: 8 }}>{v}</div>
          </div>
        ))}
      </div>

      <div className="card">
        <span className="label">Quick switch</span>
        <div className="chips" style={{ marginTop: 8 }}>
          {WX_PRESETS.map(p => (
            <button key={p.name} className={"chip " + (wx.loc.name === p.name ? "active" : "")} onClick={() => wx.setLoc(p)}>{p.name}</button>
          ))}
        </div>
        {wx.error && (
          <p className="sub" style={{ marginTop: 12, color: "var(--vps-red-400)", fontSize: 12 }}>
            {wx.error} — public APIs occasionally rate-limit. Click another city or try again in a moment.
          </p>
        )}
        <p className="sub" style={{ marginTop: 12, fontSize: 11.5 }}>
          Data: <b>Open-Meteo</b> · free, no API key, attribution-licensed (CC BY 4.0). Fetched directly from your browser.
        </p>
      </div>
    </div>
  );
}

function WeatherCompact() {
  const wx = useWeather();
  const code = wx.data?.current?.weather_code;
  const cond = WX_CODES[code] || { label: "…", icon: "🌡️" };
  const temp = wx.data?.current?.temperature_2m;
  return (
    <div className="live-compact">
      <div className="live-compact-main">
        <span style={{ fontSize: 38 }}>{cond.icon}</span>
        <div>
          <div className="live-compact-big">{wx.loading ? "…" : temp != null ? Math.round(temp) + "°" : "—"}</div>
          <div className="live-compact-sub">{cond.label}</div>
        </div>
      </div>
      <div className="live-compact-meta mono">{wx.loc.name}</div>
    </div>
  );
}

/* ============================================================
   FX RATES (open.er-api.com)
   ============================================================ */
function useFx() {
  const [base, setBase] = useStateM("USD");
  const [rates, setRates] = useStateM(null);
  const [updated, setUpdated] = useStateM(null);
  const [loading, setLoading] = useStateM(false);
  const [error, setError] = useStateM(null);

  useEffectM(() => {
    let cancelled = false;
    const run = async () => {
      setLoading(true); setError(null);
      try {
        const res = await fetch(`https://open.er-api.com/v6/latest/${base}`);
        if (!res.ok) throw new Error("FX API returned " + res.status);
        const json = await res.json();
        if (!cancelled) {
          setRates(json.rates || null);
          setUpdated(json.time_last_update_utc || null);
        }
      } catch (e) {
        if (!cancelled) setError(e.message || "Couldn't reach the FX feed");
      } finally {
        if (!cancelled) setLoading(false);
      }
    };
    run();
    return () => { cancelled = true; };
  }, [base]);

  return { base, setBase, rates, updated, loading, error };
}

const FX_QUICK = ["EUR", "GBP", "JPY", "CAD", "AUD", "CHF", "CNY", "INR", "BRL", "MXN", "ZAR", "SEK"];

function FxTool() {
  const fx = useFx();
  const [amount, setAmount] = useStateM(100);
  const [target, setTarget] = useStateM("EUR");
  const rate = fx.rates?.[target];

  return (
    <div style={{ display: "grid", gap: 16 }}>
      <div className="card">
        <div style={{ display: "grid", gridTemplateColumns: "1fr auto 1fr", gap: 12, alignItems: "end" }}>
          <div>
            <label className="label">Amount</label>
            <input className="input" type="number" value={amount} onChange={e => setAmount(+e.target.value || 0)} style={{ fontFamily: "var(--font-mono)", fontSize: 22, fontWeight: 600 }} />
            <select className="select" value={fx.base} onChange={e => fx.setBase(e.target.value)} style={{ marginTop: 10 }}>
              {["USD","EUR","GBP","JPY","CAD","AUD","CHF","CNY","INR","BRL","MXN","ZAR","SEK"].map(c => <option key={c} value={c}>{c}</option>)}
            </select>
          </div>
          <div style={{ paddingBottom: 18, fontSize: 24, color: "var(--vps-blue-400)" }}>→</div>
          <div>
            <label className="label">Converts to</label>
            <input className="input" readOnly value={rate != null ? (amount * rate).toFixed(target === "JPY" ? 0 : 2) : "—"} style={{ fontFamily: "var(--font-mono)", fontSize: 22, fontWeight: 600, background: "rgba(96,165,250,0.10)" }} />
            <select className="select" value={target} onChange={e => setTarget(e.target.value)} style={{ marginTop: 10 }}>
              {fx.rates ? Object.keys(fx.rates).sort().map(c => <option key={c} value={c}>{c}</option>) : <option>{target}</option>}
            </select>
          </div>
        </div>
        {fx.rates && (
          <div className="sub mono" style={{ marginTop: 14, fontSize: 12 }}>
            1 {fx.base} = <b style={{ color: "var(--vps-text-primary)" }}>{rate?.toFixed(target === "JPY" ? 2 : 4)} {target}</b>
            {fx.updated && <span style={{ color: "var(--vps-text-muted)", marginLeft: 10 }}>· updated {new Date(fx.updated).toLocaleString()}</span>}
          </div>
        )}
        {fx.error && <p className="sub" style={{ marginTop: 12, color: "var(--vps-red-400)", fontSize: 12 }}>{fx.error}</p>}
      </div>

      <div className="card">
        <span className="label">Top pairs · 1 {fx.base} =</span>
        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(140px, 1fr))", gap: 10, marginTop: 10 }}>
          {FX_QUICK.filter(c => c !== fx.base).map(c => {
            const r = fx.rates?.[c];
            return (
              <div key={c} className="fx-pair">
                <span className="fx-pair-code">{c}</span>
                <span className="mono fx-pair-rate">{r != null ? r.toFixed(c === "JPY" ? 2 : 4) : "—"}</span>
              </div>
            );
          })}
        </div>
        <p className="sub" style={{ marginTop: 12, fontSize: 11.5 }}>
          Data: <b>ExchangeRate-API</b> open access · daily-updated · no key required.
        </p>
      </div>
    </div>
  );
}

function FxCompact() {
  const fx = useFx();
  const pairs = [["EUR", "€"], ["GBP", "£"], ["JPY", "¥"]];
  return (
    <div className="live-compact">
      <div className="live-compact-sub">USD →</div>
      <div style={{ display: "flex", flexDirection: "column", gap: 6, marginTop: 4 }}>
        {pairs.map(([c, sym]) => {
          const r = fx.rates?.[c];
          return (
            <div key={c} className="fx-mini-row">
              <span className="fx-mini-code">{sym} {c}</span>
              <span className="mono fx-mini-val">{r != null ? r.toFixed(c === "JPY" ? 2 : 4) : "…"}</span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

/* ============================================================
   CRYPTO SPOT (Binance ticker)
   ============================================================ */
const CRYPTO_LIST = [
  { sym: "BTCUSDT",  short: "BTC",  name: "Bitcoin" },
  { sym: "ETHUSDT",  short: "ETH",  name: "Ethereum" },
  { sym: "SOLUSDT",  short: "SOL",  name: "Solana" },
  { sym: "ADAUSDT",  short: "ADA",  name: "Cardano" },
];

function useCrypto() {
  const [prices, setPrices] = useStateM({});
  const [loading, setLoading] = useStateM(true);
  const [error, setError] = useStateM(null);

  const fetchPrices = useCallbackM(async () => {
    setLoading(true); setError(null);
    try {
      const symbols = JSON.stringify(CRYPTO_LIST.map(c => c.sym));
      const res = await fetch(`https://api.binance.com/api/v3/ticker/price?symbols=${encodeURIComponent(symbols)}`);
      if (!res.ok) throw new Error("Binance API returned " + res.status);
      const arr = await res.json();
      const map = {};
      for (const row of arr) map[row.symbol] = parseFloat(row.price);
      setPrices(map);
    } catch (e) {
      setError(e.message || "Couldn't reach Binance");
    } finally {
      setLoading(false);
    }
  }, []);

  useEffectM(() => {
    fetchPrices();
    const id = setInterval(fetchPrices, 30000);
    return () => clearInterval(id);
  }, [fetchPrices]);

  return { prices, loading, error, refetch: fetchPrices };
}

function CryptoTool() {
  const c = useCrypto();
  return (
    <div style={{ display: "grid", gap: 16 }}>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: 12 }}>
        {CRYPTO_LIST.map(coin => {
          const px = c.prices[coin.sym];
          return (
            <div key={coin.sym} className="card" style={{ padding: "20px 22px" }}>
              <div className="row">
                <span className="label" style={{ margin: 0 }}>{coin.short}</span>
                <div className="spacer"></div>
                <span className="mono sub" style={{ fontSize: 10 }}>{coin.name}</span>
              </div>
              <div style={{ fontFamily: "var(--font-mono)", fontSize: 30, fontWeight: 700, marginTop: 10 }}>
                {px != null ? "$" + px.toLocaleString(undefined, { maximumFractionDigits: 2 }) : (c.loading ? "…" : "—")}
              </div>
              <div className="mono sub" style={{ fontSize: 11, marginTop: 4 }}>{coin.sym}</div>
            </div>
          );
        })}
      </div>
      <div className="card" style={{ padding: "12px 18px" }}>
        <div className="row">
          <span className="sub" style={{ fontSize: 12 }}>
            Data: <b>Binance public spot ticker</b> · refreshes every 30s · prices in USDT (≈ USD).
          </span>
          <div className="spacer"></div>
          <button className="copy-btn" onClick={c.refetch}>↺ Refresh</button>
        </div>
        {c.error && <p className="sub" style={{ marginTop: 8, color: "var(--vps-red-400)", fontSize: 12 }}>{c.error}</p>}
      </div>
    </div>
  );
}

function CryptoCompact() {
  const c = useCrypto();
  const top = ["BTCUSDT", "ETHUSDT"];
  return (
    <div className="live-compact">
      <div style={{ display: "flex", flexDirection: "column", gap: 8, marginTop: 4 }}>
        {top.map(sym => {
          const coin = CRYPTO_LIST.find(x => x.sym === sym);
          const px = c.prices[sym];
          return (
            <div key={sym} className="fx-mini-row">
              <span className="fx-mini-code">{coin.short}</span>
              <span className="mono fx-mini-val">
                {px != null ? "$" + px.toLocaleString(undefined, { maximumFractionDigits: 0 }) : "…"}
              </span>
            </div>
          );
        })}
      </div>
      <div className="live-compact-meta mono" style={{ marginTop: 8 }}>Spot · USDT</div>
    </div>
  );
}

/* ============================================================
   SHEETS FORMULA — cross-sheet lookup generator
   ============================================================ */
function SheetsLookupTool() {
  const [cfg, setCfg] = useStateM({
    target: "google",           // google | excel
    sourceUrl: "https://docs.google.com/spreadsheets/d/1AbCdEfGh/edit",
    sourceSheetName: "Customers",
    sourceKeyCol: "A",
    sourceReturnCol: "C",
    lookupCell: "A2",
    fallbackValue: "Not found",
    useLet: true,
  });
  const [copied, setCopied] = useStateM(false);

  const formula = useMemoM(() => {
    const f = cfg.fallbackValue.replace(/"/g, '""');
    if (cfg.target === "google") {
      const rangeOpen  = `IMPORTRANGE("${cfg.sourceUrl}", "${cfg.sourceSheetName}!${cfg.sourceKeyCol}:${cfg.sourceKeyCol}")`;
      const rangeReturn = `IMPORTRANGE("${cfg.sourceUrl}", "${cfg.sourceSheetName}!${cfg.sourceReturnCol}:${cfg.sourceReturnCol}")`;
      if (cfg.useLet) {
        return `=LET(
  key, ${cfg.lookupCell},
  keys, ${rangeOpen},
  vals, ${rangeReturn},
  IFERROR(
    INDEX(vals, MATCH(key, keys, 0)),
    "${f}"
  )
)`;
      }
      return `=IFERROR(INDEX(${rangeReturn}, MATCH(${cfg.lookupCell}, ${rangeOpen}, 0)), "${f}")`;
    }
    // Excel
    const ref = `[${cfg.sourceUrl}]${cfg.sourceSheetName}`;
    const keys = `${ref}!$${cfg.sourceKeyCol}:$${cfg.sourceKeyCol}`;
    const vals = `${ref}!$${cfg.sourceReturnCol}:$${cfg.sourceReturnCol}`;
    if (cfg.useLet) {
      return `=LET(
  key, ${cfg.lookupCell},
  result, XLOOKUP(key, ${keys}, ${vals}, "${f}"),
  result
)`;
    }
    return `=XLOOKUP(${cfg.lookupCell}, ${keys}, ${vals}, "${f}")`;
  }, [cfg]);

  const set = (k, v) => setCfg(c => ({ ...c, [k]: v }));

  return (
    <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
      <style>{`@media (max-width: 980px) { .sf-grid { grid-template-columns: 1fr !important; } }`}</style>
      <div className="sf-grid" style={{ display: "contents" }}>
        <div className="card">
          <div className="field">
            <label className="label">Target spreadsheet</label>
            <div className="chips">
              <button className={"chip " + (cfg.target === "google" ? "active" : "")} onClick={() => set("target", "google")}>Google Sheets</button>
              <button className={"chip " + (cfg.target === "excel" ? "active" : "")} onClick={() => set("target", "excel")}>Excel</button>
            </div>
          </div>
          <div className="field">
            <label className="label">Source workbook {cfg.target === "google" ? "URL" : "filename"}</label>
            <input className="input" value={cfg.sourceUrl} onChange={e => set("sourceUrl", e.target.value)} />
          </div>
          <div className="field">
            <label className="label">Source sheet / tab name</label>
            <input className="input" value={cfg.sourceSheetName} onChange={e => set("sourceSheetName", e.target.value)} />
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
            <div className="field">
              <label className="label">Key column</label>
              <input className="input" value={cfg.sourceKeyCol} onChange={e => set("sourceKeyCol", e.target.value)} />
            </div>
            <div className="field">
              <label className="label">Return column</label>
              <input className="input" value={cfg.sourceReturnCol} onChange={e => set("sourceReturnCol", e.target.value)} />
            </div>
          </div>
          <div className="field">
            <label className="label">Lookup key cell (in current sheet)</label>
            <input className="input" value={cfg.lookupCell} onChange={e => set("lookupCell", e.target.value)} />
          </div>
          <div className="field">
            <label className="label">Fallback when no match</label>
            <input className="input" value={cfg.fallbackValue} onChange={e => set("fallbackValue", e.target.value)} />
          </div>
          <div className="field">
            <label className="label">Wrap in LET (recommended)</label>
            <div className="chips">
              <button className={"chip " + (cfg.useLet ? "active" : "")} onClick={() => set("useLet", true)}>Yes — easier to debug</button>
              <button className={"chip " + (!cfg.useLet ? "active" : "")} onClick={() => set("useLet", false)}>No — one-liner</button>
            </div>
          </div>
        </div>

        <div className="card">
          <div className="row" style={{ marginBottom: 10 }}>
            <span className="label" style={{ margin: 0 }}>Generated formula</span>
            <div className="spacer"></div>
            <button className={"copy-btn " + (copied ? "copied" : "")} onClick={() => {
              navigator.clipboard.writeText(formula).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1200); });
            }}>{copied ? "Copied" : "Copy formula"}</button>
          </div>
          <pre className="formula-output">{formula}</pre>
          <div style={{ marginTop: 14, padding: "12px 14px", background: "rgba(37,99,235,0.06)", border: "1px solid rgba(96,165,250,0.20)", borderRadius: 8, fontSize: 12, color: "var(--vps-blue-300)", lineHeight: 1.6 }}>
            {cfg.target === "google" ? (
              <>
                <b>Heads up:</b> the first time you use IMPORTRANGE, Google Sheets will prompt you to authorize access between
                the two workbooks. Click <b>Allow access</b> in the cell tooltip.
              </>
            ) : (
              <>
                <b>Heads up:</b> the source workbook must be open in Excel for the path reference to resolve. For shared OneDrive
                files, use the full file URL instead of <span className="mono">[filename]</span>.
              </>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   REGEX TESTER
   ============================================================ */
function RegexTesterTool() {
  const [pattern, setPattern] = useStateM("\\b(\\w+)@(\\w+\\.\\w+)\\b");
  const [flags, setFlags] = useStateM("g");
  const [input, setInput] = useStateM("Contact us at hello@vibeprosoft.com or support@example.org.\nAlternates: nobody@nowhere.io and team@ vibeprosoft .com (broken).");
  const [replace, setReplace] = useStateM("[$1@$2]");
  const [showReplace, setShowReplace] = useStateM(false);

  const { matches, error, highlighted, replaced } = useMemoM(() => {
    try {
      const re = new RegExp(pattern, flags);
      const matches = [];
      if (flags.includes("g")) {
        let m;
        const reCopy = new RegExp(pattern, flags);
        while ((m = reCopy.exec(input)) !== null) {
          matches.push({ match: m[0], index: m.index, groups: m.slice(1) });
          if (m.index === reCopy.lastIndex) reCopy.lastIndex++;
        }
      } else {
        const m = input.match(re);
        if (m) matches.push({ match: m[0], index: m.index, groups: m.slice(1) });
      }
      // Highlight
      let html = "";
      let last = 0;
      const reHi = new RegExp(pattern, flags.includes("g") ? flags : flags + "g");
      let m;
      while ((m = reHi.exec(input)) !== null) {
        html += escapeHtml(input.slice(last, m.index));
        html += `<mark class="rx-hl">${escapeHtml(m[0])}</mark>`;
        last = m.index + m[0].length;
        if (m.index === reHi.lastIndex) reHi.lastIndex++;
      }
      html += escapeHtml(input.slice(last));
      const replaced = input.replace(re, replace);
      return { matches, error: null, highlighted: html, replaced };
    } catch (e) {
      return { matches: [], error: e.message, highlighted: escapeHtml(input), replaced: input };
    }
  }, [pattern, flags, input, replace]);

  const toggleFlag = (f) => setFlags(prev => prev.includes(f) ? prev.replace(f, "") : prev + f);

  return (
    <div style={{ display: "grid", gap: 16 }}>
      <div className="card">
        <div style={{ display: "grid", gridTemplateColumns: "1fr auto", gap: 12, alignItems: "end" }}>
          <div className="field" style={{ margin: 0 }}>
            <label className="label">Pattern</label>
            <div style={{ display: "flex", alignItems: "center", gap: 6, background: "rgba(0,0,0,0.30)", border: "1px solid " + (error ? "var(--vps-red-400)" : "var(--vps-border)"), borderRadius: 10, padding: "10px 12px" }}>
              <span className="mono" style={{ color: "var(--vps-text-muted)" }}>/</span>
              <input className="mono" value={pattern} onChange={e => setPattern(e.target.value)} style={{ flex: 1, background: "transparent", border: 0, color: "var(--vps-text-primary)", fontSize: 13.5, outline: "none" }} />
              <span className="mono" style={{ color: "var(--vps-text-muted)" }}>/{flags}</span>
            </div>
          </div>
          <div>
            <label className="label">Flags</label>
            <div className="chips">
              {[["g", "global"], ["i", "ignore case"], ["m", "multiline"], ["s", "dotall"]].map(([f, label]) => (
                <button key={f} className={"chip " + (flags.includes(f) ? "active" : "")} onClick={() => toggleFlag(f)} title={label}>{f}</button>
              ))}
            </div>
          </div>
        </div>
        {error && <div style={{ marginTop: 10, padding: "8px 12px", background: "rgba(239,68,68,0.10)", border: "1px solid rgba(239,68,68,0.40)", borderRadius: 8, color: "var(--vps-red-400)", fontSize: 12 }}>{error}</div>}
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
        <style>{`@media (max-width: 980px) { .rx-grid { grid-template-columns: 1fr !important; } }`}</style>
        <div className="rx-grid" style={{ display: "contents" }}>
          <div className="card">
            <div className="row" style={{ marginBottom: 8 }}>
              <span className="label" style={{ margin: 0 }}>Test string</span>
            </div>
            <textarea className="textarea" style={{ minHeight: 220 }} value={input} onChange={e => setInput(e.target.value)} spellCheck="false"></textarea>
          </div>
          <div className="card">
            <div className="row" style={{ marginBottom: 8 }}>
              <span className="label" style={{ margin: 0 }}>Highlights · {matches.length} match{matches.length === 1 ? "" : "es"}</span>
            </div>
            <div className="rx-output mono" dangerouslySetInnerHTML={{ __html: highlighted || "&nbsp;" }} />
          </div>
        </div>
      </div>

      <div className="card">
        <div className="row" style={{ marginBottom: 8 }}>
          <span className="label" style={{ margin: 0 }}>Replace</span>
          <div className="spacer"></div>
          <button className="copy-btn" onClick={() => setShowReplace(s => !s)}>{showReplace ? "Hide" : "Show"}</button>
        </div>
        {showReplace && (
          <>
            <input className="input mono" placeholder="Replacement (use $1, $2 for groups)" value={replace} onChange={e => setReplace(e.target.value)} style={{ marginBottom: 8 }} />
            <pre className="formula-output">{replaced}</pre>
          </>
        )}
      </div>

      {matches.length > 0 && (
        <div className="card">
          <span className="label">Match details</span>
          <div style={{ marginTop: 10, display: "grid", gridTemplateColumns: "60px 1fr 1fr", gap: 8, fontSize: 12, fontFamily: "var(--font-mono)" }}>
            <span style={{ color: "var(--vps-text-muted)" }}>idx</span>
            <span style={{ color: "var(--vps-text-muted)" }}>match</span>
            <span style={{ color: "var(--vps-text-muted)" }}>groups</span>
            {matches.map((m, i) => (
              <React.Fragment key={i}>
                <span style={{ color: "var(--vps-amber-300)" }}>{m.index}</span>
                <span style={{ color: "var(--vps-blue-300)" }}>{m.match}</span>
                <span style={{ color: "var(--vps-text-body)" }}>{m.groups.length ? m.groups.join(" · ") : "—"}</span>
              </React.Fragment>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

function escapeHtml(s) {
  return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

/* ============================================================
   JSON TOOLS
   ============================================================ */
function JsonToolsTool() {
  const [input, setInput] = useStateM(`{"name":"VibeProSoft","tools":12,"users":{"active":2034,"trial":51},"flags":["new","2026"]}`);
  const [indent, setIndent] = useStateM(2);
  const [copied, setCopied] = useStateM(false);

  const { output, error, stats } = useMemoM(() => {
    try {
      const obj = JSON.parse(input);
      return {
        output: JSON.stringify(obj, null, indent),
        error: null,
        stats: {
          chars: input.length,
          keys: countKeys(obj),
          depth: maxDepth(obj),
          type: Array.isArray(obj) ? "array" : typeof obj,
        },
      };
    } catch (e) {
      return { output: "", error: e.message, stats: null };
    }
  }, [input, indent]);

  const minify = useMemoM(() => {
    try { return JSON.stringify(JSON.parse(input)); } catch { return ""; }
  }, [input]);

  return (
    <div style={{ display: "grid", gap: 16 }}>
      <div className="card">
        <div className="row" style={{ marginBottom: 10 }}>
          <span className="label" style={{ margin: 0 }}>Input JSON</span>
          <div className="spacer"></div>
          <div className="chips">
            {[2, 4, "tab"].map(i => (
              <button key={i} className={"chip " + (indent === i || (i === "tab" && indent === "\t") ? "active" : "")} onClick={() => setIndent(i === "tab" ? "\t" : i)}>
                {i === "tab" ? "tab" : i + " sp"}
              </button>
            ))}
          </div>
        </div>
        <textarea className="textarea" style={{ minHeight: 160 }} value={input} onChange={e => setInput(e.target.value)} spellCheck="false"></textarea>
      </div>

      {error ? (
        <div className="card" style={{ borderColor: "rgba(239,68,68,0.40)", background: "rgba(239,68,68,0.06)" }}>
          <span className="label" style={{ color: "var(--vps-red-400)" }}>Invalid JSON</span>
          <p className="mono" style={{ marginTop: 8, color: "var(--vps-red-400)", fontSize: 13 }}>{error}</p>
        </div>
      ) : (
        <>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(110px, 1fr))", gap: 10 }}>
            <Stat label="Chars in"  v={stats.chars} />
            <Stat label="Chars out (min)"  v={minify.length} />
            <Stat label="Saved" v={Math.max(0, stats.chars - minify.length) + " ch"} />
            <Stat label="Keys" v={stats.keys} />
            <Stat label="Depth" v={stats.depth} />
            <Stat label="Root" v={stats.type} />
          </div>

          <div className="card">
            <div className="row" style={{ marginBottom: 10 }}>
              <span className="label" style={{ margin: 0 }}>Formatted</span>
              <div className="spacer"></div>
              <button className={"copy-btn " + (copied ? "copied" : "")} onClick={() => { navigator.clipboard.writeText(output).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1200); }); }}>{copied ? "Copied" : "Copy formatted"}</button>
              <button className="copy-btn" onClick={() => navigator.clipboard.writeText(minify)}>Copy minified</button>
            </div>
            <pre className="formula-output" style={{ maxHeight: 420, overflow: "auto" }}>{output}</pre>
          </div>
        </>
      )}
    </div>
  );
}

function Stat({ label, v }) {
  return (
    <div className="card" style={{ padding: "10px 12px", textAlign: "center" }}>
      <div style={{ fontSize: 10, letterSpacing: "0.18em", textTransform: "uppercase", color: "var(--vps-text-muted)" }}>{label}</div>
      <div className="mono" style={{ fontSize: 18, marginTop: 4, fontWeight: 600 }}>{v}</div>
    </div>
  );
}

function countKeys(o) {
  if (o === null || typeof o !== "object") return 0;
  let n = 0;
  if (Array.isArray(o)) { for (const x of o) n += countKeys(x); return n; }
  for (const k of Object.keys(o)) { n++; n += countKeys(o[k]); }
  return n;
}
function maxDepth(o, d = 1) {
  if (o === null || typeof o !== "object") return d;
  let m = d;
  const vals = Array.isArray(o) ? o : Object.values(o);
  for (const v of vals) m = Math.max(m, maxDepth(v, d + 1));
  return m;
}

/* ============================================================
   TEXT CLEANUP
   ============================================================ */
function CleanupTool() {
  const [input, setInput] = useStateM("Here  is some text   with double  spaces. \n\n\n\nAlso “smart quotes” and — em dashes —\nthat sometimes need to go.\n\tTab\there.\n  Trailing spaces here.   ");
  const [opts, setOpts] = useStateM({
    dedupeSpaces: true,
    trimLines: true,
    collapseBlanks: true,
    smartQuotes: false,
    emDashes: false,
    tabsToSpaces: false,
    stripNonAscii: false,
    fixLineEndings: true,
  });

  const output = useMemoM(() => {
    let s = input;
    if (opts.fixLineEndings) s = s.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
    if (opts.tabsToSpaces) s = s.replace(/\t/g, "  ");
    if (opts.smartQuotes) {
      s = s.replace(/[\u2018\u2019]/g, "'").replace(/[\u201C\u201D]/g, '"');
    }
    if (opts.emDashes) s = s.replace(/[\u2014\u2013]/g, "-");
    if (opts.stripNonAscii) s = s.replace(/[^\x00-\x7F]/g, "");
    if (opts.dedupeSpaces) s = s.replace(/[ ]{2,}/g, " ");
    if (opts.trimLines) s = s.split("\n").map(l => l.replace(/[ \t]+$/g, "").replace(/^[ \t]+/g, "")).join("\n");
    if (opts.collapseBlanks) s = s.replace(/\n{3,}/g, "\n\n");
    return s;
  }, [input, opts]);

  const togg = (k) => setOpts(o => ({ ...o, [k]: !o[k] }));

  const opts_list = [
    ["fixLineEndings", "Normalize line endings", "Convert \\r\\n and \\r → \\n"],
    ["dedupeSpaces", "Collapse multiple spaces", "Two or more spaces → one"],
    ["trimLines", "Trim line whitespace", "Remove leading & trailing spaces per line"],
    ["collapseBlanks", "Collapse blank lines", "3+ blank lines → 1"],
    ["tabsToSpaces", "Tabs → 2 spaces", "Sometimes needed for plain-text email"],
    ["smartQuotes", "Smart quotes → straight", "“ ” ‘ ’ → \" \" ' '"],
    ["emDashes", "Em/en dashes → hyphens", "— – → -"],
    ["stripNonAscii", "Strip non-ASCII", "Aggressive — kills emoji and accents"],
  ];

  return (
    <div style={{ display: "grid", gridTemplateColumns: "260px 1fr 1fr", gap: 16 }}>
      <style>{`@media (max-width: 1080px) { .cu-grid { grid-template-columns: 1fr !important; } }`}</style>
      <div className="cu-grid" style={{ display: "contents" }}>
        <div className="card">
          <span className="label">Cleanup rules</span>
          <div style={{ display: "flex", flexDirection: "column", gap: 8, marginTop: 10 }}>
            {opts_list.map(([k, label, hint]) => (
              <button key={k} className={"cu-opt " + (opts[k] ? "on" : "")} onClick={() => togg(k)}>
                <span className={"cu-opt-tick " + (opts[k] ? "on" : "")}>{opts[k] ? "✓" : ""}</span>
                <div>
                  <div className="cu-opt-label">{label}</div>
                  <div className="cu-opt-hint">{hint}</div>
                </div>
              </button>
            ))}
          </div>
        </div>
        <div className="card">
          <div className="row" style={{ marginBottom: 10 }}>
            <span className="label" style={{ margin: 0 }}>Input</span>
            <div className="spacer"></div>
            <span className="mono sub" style={{ fontSize: 11 }}>{input.length} ch</span>
          </div>
          <textarea className="textarea" style={{ minHeight: 380 }} value={input} onChange={e => setInput(e.target.value)} spellCheck="false"></textarea>
        </div>
        <div className="card">
          <div className="row" style={{ marginBottom: 10 }}>
            <span className="label" style={{ margin: 0 }}>Cleaned</span>
            <div className="spacer"></div>
            <span className="mono sub" style={{ fontSize: 11, color: "var(--vps-green-400)" }}>
              {output.length} ch · {(input.length - output.length)} saved
            </span>
            <button className="copy-btn" onClick={() => navigator.clipboard.writeText(output)}>Copy</button>
          </div>
          <textarea className="textarea" readOnly style={{ minHeight: 380, background: "rgba(96,165,250,0.04)" }} value={output}></textarea>
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   COMPACT WIDGETS for existing tools
   ============================================================ */
function PomodoroCompact() {
  const app = window.Office.useApp();
  const { pomo, startPomo, pausePomo } = app;
  const pct = pomo.totalMs ? (1 - pomo.remainingMs / pomo.totalMs) * 100 : 0;
  return (
    <div className="sw-compact">
      <div className="sw-compact-time mono" style={{ fontSize: 36 }}>{window.Office.fmtMs(pomo.remainingMs)}</div>
      <div className="focus-ring" style={{ width: "100%", margin: "10px 0" }}>
        <div className="focus-ring-fill" style={{ width: pct + "%" }}></div>
      </div>
      <div className="sw-compact-actions">
        {pomo.running
          ? <button className="btn btn-danger btn-sm" onClick={pausePomo}>⏸ Pause</button>
          : <button className="btn btn-amber btn-sm" onClick={() => startPomo()}>▶ Start</button>}
        <span className="mono sub" style={{ fontSize: 11 }}>
          {pomo.mode === "focus" ? `S${pomo.sessionIdx}/${pomo.cycles}` : pomo.mode === "short" ? "break" : "long break"}
        </span>
      </div>
    </div>
  );
}

function ClockStripCompact() {
  const app = window.Office.useApp();
  const now = app.now;
  const localTz = useMemoM(() => Intl.DateTimeFormat().resolvedOptions().timeZone, []);
  const cities = [
    { name: "Local",    tz: localTz,             local: true },
    { name: "New York", tz: "America/New_York" },
    { name: "London",   tz: "Europe/London" },
    { name: "Tokyo",    tz: "Asia/Tokyo" },
  ];
  return (
    <div className="clock-strip" style={{ marginTop: 0 }}>
      {cities.map(c => (
        <div key={c.name} className={"clock-row " + (c.local ? "local" : "")}>
          <span className="city">{c.name}</span>
          <span className="time">{new Intl.DateTimeFormat([], { timeZone: c.tz, hour: "2-digit", minute: "2-digit", hour12: false }).format(now)}</span>
        </div>
      ))}
    </div>
  );
}

function ScratchpadCompact() {
  const app = window.Office.useApp();
  const { scratch, setScratch } = app;
  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
      <textarea
        className="textarea"
        style={{ minHeight: 110, fontSize: 12.5, padding: "10px 12px" }}
        placeholder="Quick notes — vanish on close…"
        value={scratch}
        onChange={e => setScratch(e.target.value)}
      ></textarea>
      <div className="mono sub" style={{ fontSize: 10.5, marginTop: 6, textAlign: "right" }}>
        {scratch.trim() ? scratch.trim().split(/\s+/).length : 0} words
      </div>
    </div>
  );
}

/* ============================================================
   REGISTER tools + compact widgets
   ============================================================ */
Object.assign(window.ToolComponents, {
  "watercooler":   WatercoolerTool,
  "stopwatch":     StopwatchTool,
  "weather":       WeatherTool,
  "fx":            FxTool,
  "crypto":        CryptoTool,
  "sheets-lookup": SheetsLookupTool,
  "regex":         RegexTesterTool,
  "json-tools":    JsonToolsTool,
  "cleanup":       CleanupTool,
});

window.CompactWidgets = {
  "pomodoro":     PomodoroCompact,
  "watercooler":  WatercoolerCompact,
  "stopwatch":    StopwatchCompact,
  "world-clock":  ClockStripCompact,
  "weather":      WeatherCompact,
  "fx":           FxCompact,
  "crypto":       CryptoCompact,
  "scratchpad":   ScratchpadCompact,
};

// Re-mount now that everything is registered
window.OfficeMountApp();
