const { useState, useMemo, useEffect } = React;

/* ============================================================
   i18n — lightweight EN/FR. t(en, fr) picks by current lang;
   provided via context so every component can localise.
   ============================================================ */
const LangContext = React.createContext({ lang: "en", t: (en) => en, setLang: () => {} });
const useL = () => React.useContext(LangContext);

const STATUS_DEFS = [
  { id: "kept",       label: "Kept",        label_fr: "Tenue",          short: "Kept",        short_fr: "Tenue",       def: "substantially or fully delivered",        def_fr: "réalisée en grande partie ou entièrement" },
  { id: "progress",   label: "In Progress", label_fr: "En cours",       short: "In progress", short_fr: "En cours",    def: "real movement, not yet delivered",        def_fr: "progrès réels, pas encore réalisée" },
  { id: "stalled",    label: "Stalled",     label_fr: "Au point mort",  short: "Stalled",     short_fr: "Au pt mort",  def: "started, then stuck or paused",           def_fr: "amorcée, puis bloquée ou suspendue" },
  { id: "broken",     label: "Broken",      label_fr: "Rompue",         short: "Broken",      short_fr: "Rompue",      def: "abandoned or reversed",                   def_fr: "abandonnée ou annulée" },
  { id: "notstarted", label: "Not Started", label_fr: "Non commencée",  short: "Not started", short_fr: "Non comm.",   def: "no action, or insufficient sourced reporting", def_fr: "aucune action, ou sources insuffisantes" },
];

const TOPICS = ["Affordability", "Housing", "Workers", "Trade", "Defence", "Public Safety", "Culture", "Environment", "Energy"];
const TOPIC_FR = {
  "Affordability": "Abordabilité", "Housing": "Logement", "Workers": "Travailleurs", "Trade": "Commerce",
  "Defence": "Défense", "Public Safety": "Sécurité publique", "Culture": "Culture",
  "Environment": "Environnement", "Energy": "Énergie",
};
const topicLabel = (topic, lang) => (lang === "fr" ? (TOPIC_FR[topic] || topic) : topic);

// Pledge → most relevant live macro indicator (the "context" layer).
const CONTEXT_MAP = { "tax-cut": "cpi", housing: "rate", apprentices: "jobs", auto: "fx", "trade-diversify": "fx", agrifood: "cpi", energy: "fx" };

function daysBetween(a, b) {
  return Math.round((new Date(b) - new Date(a)) / (1000 * 60 * 60 * 24));
}
function fmtDate(s, lang = "en") {
  const loc = lang === "fr" ? "fr-CA" : "en-CA";
  if (/^\d{4}$/.test(s)) return s;
  if (/^\d{4}-\d{2}$/.test(s)) return new Date(s + "-01T00:00:00").toLocaleDateString(loc, { year: "numeric", month: "short" });
  return new Date(s + "T00:00:00").toLocaleDateString(loc, { year: "numeric", month: "short", day: "numeric" });
}

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "density": "compact",
  "sortBy": "default",
  "theme": "cream"
}/*EDITMODE-END*/;

const THEMES = {
  cream: { "--bg": "#efece4", "--bg-elev": "#f7f4ec", "--ink": "#14161d", "--ink-2": "#2c3040", "--ink-3": "#696d7c", "--rule": "#d2cebf", "--rule-soft": "#e1ddcd", "--accent": "#ee2737", "--accent-deep": "#b51625" },
  paper: { "--bg": "#f4f3ef", "--bg-elev": "#ffffff", "--ink": "#0c0d11", "--ink-2": "#2a2c34", "--ink-3": "#73767f", "--rule": "#dedcd5", "--rule-soft": "#ecebe5", "--accent": "#ee2737", "--accent-deep": "#b51625" },
  ink:   { "--bg": "#0e1014", "--bg-elev": "#171a21", "--ink": "#ece9e0", "--ink-2": "#b9b6ab", "--ink-3": "#7e7c74", "--rule": "#2a2d36", "--rule-soft": "#1f222a", "--accent": "#ff4d5b", "--accent-deep": "#ee2737" },
};

function App() {
  const data = window.PLEDGE_DATA;
  const [lang, setLang] = useState("en");
  const t = (en, fr) => (lang === "fr" ? fr : en);
  const [activeStatuses, setActiveStatuses] = useState(new Set());
  const [activeTopics, setActiveTopics] = useState(new Set());
  const [search, setSearch] = useState("");
  const [tw, setTweak] = window.useTweaks(TWEAK_DEFAULTS);

  // Apply theme variables
  useEffect(() => {
    const theme = THEMES[tw.theme] || THEMES.cream;
    const root = document.documentElement;
    Object.entries(theme).forEach(([k, v]) => root.style.setProperty(k, v));
  }, [tw.theme]);

  useEffect(() => { document.documentElement.lang = lang; }, [lang]);

  // Topline aggregates
  const counts = useMemo(() => {
    const c = { kept: 0, progress: 0, stalled: 0, broken: 0, notstarted: 0 };
    data.pledges.forEach(p => c[p.status]++);
    return c;
  }, [data]);

  const total = data.pledges.length;
  const avgProgress = useMemo(() =>
    Math.round(data.pledges.reduce((s, p) => s + p.progress, 0) / total),
    [data, total]
  );

  // Filter + sort
  const visible = useMemo(() => {
    let list = data.pledges.filter(p => {
      if (activeStatuses.size && !activeStatuses.has(p.status)) return false;
      if (activeTopics.size && !activeTopics.has(p.topic)) return false;
      if (search.trim()) {
        const q = search.toLowerCase();
        const hay = `${p.title} ${p.title_fr || ""} ${p.pledge} ${p.pledge_fr || ""} ${p.note} ${p.note_fr || ""} ${p.topic}`.toLowerCase();
        if (!hay.includes(q)) return false;
      }
      return true;
    });
    if (tw.sortBy === "progress") {
      list = [...list].sort((a, b) => b.progress - a.progress);
    } else if (tw.sortBy === "recent") {
      list = [...list].sort((a, b) => new Date(b.lastUpdate) - new Date(a.lastUpdate));
    } else if (tw.sortBy === "status") {
      const order = ["broken", "stalled", "notstarted", "progress", "kept"];
      list = [...list].sort((a, b) => order.indexOf(a.status) - order.indexOf(b.status));
    }
    return list;
  }, [data, activeStatuses, activeTopics, search, tw.sortBy]);

  const toggle = (set, setter) => (id) => {
    const next = new Set(set);
    if (next.has(id)) next.delete(id); else next.add(id);
    setter(next);
  };

  const daysInOffice = daysBetween(data.meta.sworn_in, data.meta.as_of);

  return (
    <LangContext.Provider value={{ lang, t, setLang }}>
    <div className={`app density-${tw.density}`}>
      <div className="top-banner" />
      <Masthead asOf={data.meta.as_of} daysInOffice={daysInOffice} />
      <MacroStrip />
      <Hero />
      <Dashboard
        counts={counts}
        total={total}
        avgProgress={avgProgress}
        daysInOffice={daysInOffice}
        activeStatuses={activeStatuses}
        toggleStatus={toggle(activeStatuses, setActiveStatuses)}
        data={data}
      />
      {window.PollingSection ? React.createElement(window.PollingSection, { lang }) : null}
      <Controls
        activeStatuses={activeStatuses}
        toggleStatus={toggle(activeStatuses, setActiveStatuses)}
        activeTopics={activeTopics}
        toggleTopic={toggle(activeTopics, setActiveTopics)}
        search={search}
        setSearch={setSearch}
        sortBy={tw.sortBy}
        setSortBy={(v) => setTweak("sortBy", v)}
      />
      <PledgeList visible={visible} total={total} />
      <Methodology asOf={data.meta.as_of} />
      <Footnote asOf={data.meta.as_of} sample={data.meta.sample} />
      <TrackerTweaks t={tw} setTweak={setTweak} />
    </div>
    </LangContext.Provider>
  );
}

function Masthead({ asOf, daysInOffice }) {
  const { lang, t, setLang } = useL();
  return (
    <header className="masthead">
      <div className="wrap masthead-inner">
        <div className="brand">
          <div className="brand-mark">C</div>
          <span>The Carney Tracker</span>
        </div>
        <div className="masthead-meta">
          <button
            onClick={() => setLang(lang === "en" ? "fr" : "en")}
            title={t("Afficher en français", "View in English")}
            style={{ font: "inherit", fontFamily: "var(--mono)", fontSize: 11, letterSpacing: "0.06em", textTransform: "uppercase",
                     color: "var(--ink-2)", background: "transparent", border: "1px solid var(--rule)", borderRadius: 5,
                     padding: "3px 8px", cursor: "pointer" }}>
            {t("Français", "English")}
          </button>
          <span>{t("Day", "Jour")} <b>{daysInOffice}</b> {t("of mandate", "du mandat")}</span>
          <span>{t("Data as of", "Données au")} <b>{fmtDate(asOf, lang)}</b></span>
        </div>
      </div>
    </header>
  );
}

function Hero() {
  const { t } = useL();
  return (
    <section className="hero">
      <div className="wrap">
        <div className="hero-eyebrow">{t("A Liberal pledge tracker · 45th Parliament", "Suivi des promesses libérales · 45e législature")}</div>
        <h1>{t(
          <>Eleven promises. <em>One year in.</em> Where are we?</>,
          <>Onze promesses. <em>Un an plus tard.</em> Où en sommes-nous ?</>
        )}</h1>
        <p className="hero-dek">{t(
          "Tracking the Liberal Party's 2025 platform commitments — what's been delivered, what's stuck, and what's quietly drifting off the agenda.",
          "Suivi des engagements de la plateforme libérale de 2025 — ce qui a été réalisé, ce qui est bloqué, et ce qui s'éloigne discrètement de l'ordre du jour."
        )}</p>
      </div>
    </section>
  );
}

/* ============================================================
   MacroStrip — live Canadian macro indicators (BoC + StatCan),
   seeded from build-time macro.js; rate/fx refresh live.
   ============================================================ */

const MACRO_INDICATORS = [
  { id: "rate", label: "BoC Policy Rate", label_fr: "Taux directeur BdC", series: "V39079", unit: "%", fmt: (v) => v.toFixed(2),
    source: "Bank of Canada", source_fr: "Banque du Canada", sourceUrl: "https://www.bankofcanada.ca/core-functions/monetary-policy/key-interest-rate/",
    fallback: { value: 2.75, date: "2026-04-16", prev: 3.00 } },
  { id: "fx", label: "USD / CAD", label_fr: "USD / CAD", series: "FXUSDCAD", unit: "", fmt: (v) => v.toFixed(4),
    source: "Bank of Canada", source_fr: "Banque du Canada", sourceUrl: "https://www.bankofcanada.ca/rates/exchange/",
    fallback: { value: 1.3645, date: "2026-05-22", prev: 1.3702 } },
  { id: "cpi", label: "CPI Inflation (y/y)", label_fr: "Inflation IPC (a/a)", series: null, unit: "%", fmt: (v) => v.toFixed(1),
    source: "Statistics Canada", source_fr: "Statistique Canada", sourceUrl: "https://www150.statcan.gc.ca/n1/daily-quotidien/240416/dq240416a-eng.htm",
    fallback: { value: 2.1, date: "2026-04-15", prev: 2.3 } },
  { id: "jobs", label: "Unemployment", label_fr: "Chômage", series: null, unit: "%", fmt: (v) => v.toFixed(1),
    source: "Statistics Canada", source_fr: "Statistique Canada", sourceUrl: "https://www150.statcan.gc.ca/n1/daily-quotidien/240510/dq240510a-eng.htm",
    fallback: { value: 6.4, date: "2026-05-09", prev: 6.5 } },
];

async function fetchValet(series) {
  const url = `https://www.bankofcanada.ca/valet/observations/${series}/json?recent=2`;
  const r = await fetch(url);
  if (!r.ok) throw new Error("HTTP " + r.status);
  const j = await r.json();
  // Valet returns observations newest-first; normalise to chronological.
  const obs = (j.observations || []).slice().sort((a, b) => a.d.localeCompare(b.d));
  if (!obs.length) throw new Error("no observations");
  const key = Object.keys(obs[obs.length - 1]).find(k => k !== "d");
  const last = obs[obs.length - 1];
  const prev = obs[obs.length - 2];
  return { value: parseFloat(last[key].v), date: last.d, prev: prev ? parseFloat(prev[key].v) : null };
}

function MacroStrip() {
  const { lang, t } = useL();
  const [data, setData] = useState(() =>
    Object.fromEntries(MACRO_INDICATORS.map(i => {
      const seed = (window.MACRO_DATA && window.MACRO_DATA[i.id]) || i.fallback;
      return [i.id, { ...seed, live: false, loading: !!i.series }];
    }))
  );

  useEffect(() => {
    let cancelled = false;
    (async () => {
      const live = {};
      await Promise.all(MACRO_INDICATORS.filter(i => i.series).map(async (ind) => {
        try {
          const res = await fetchValet(ind.series);
          if (!cancelled) live[ind.id] = { ...res, live: true, loading: false };
        } catch (e) {
          if (!cancelled) live[ind.id] = { ...((window.MACRO_DATA && window.MACRO_DATA[ind.id]) || ind.fallback), live: false, loading: false };
        }
      }));
      if (!cancelled && Object.keys(live).length) setData(prev => ({ ...prev, ...live }));
    })();
    return () => { cancelled = true; };
  }, []);

  const anyLive = Object.values(data).some(d => d.live);

  return (
    <section className="macro-strip">
      <div className="wrap macro-inner">
        <div className="macro-label">
          Canada<br />
          <span className={anyLive ? "macro-live" : ""}>
            {anyLive ? t("Live data", "Données en direct") : t("Latest data", "Dernières données")}
          </span>
        </div>
        <div className="macro-row">
          {MACRO_INDICATORS.map(ind => {
            const d = data[ind.id];
            const delta = d.prev != null ? d.value - d.prev : null;
            const dir = delta == null ? null : delta > 0 ? "up" : delta < 0 ? "down" : null;
            const src = lang === "fr" ? ind.source_fr : ind.source;
            return (
              <div key={ind.id} className={`macro-cell ${d.loading ? "loading" : ""}`}>
                <div className="macro-cell-label">{lang === "fr" ? ind.label_fr : ind.label}</div>
                <div className="macro-cell-value">
                  <span>{d.loading ? "—" : ind.fmt(d.value)}</span>
                  {ind.unit && <span className="macro-cell-unit">{ind.unit}</span>}
                </div>
                <div className="macro-cell-meta">
                  {delta != null && (
                    <span className={`macro-delta ${dir || ""}`}>
                      {delta > 0 ? "▲" : delta < 0 ? "▼" : "·"} {Math.abs(delta).toFixed(ind.unit === "%" ? 2 : 4)}
                    </span>
                  )}
                  <span>·</span>
                  <span title={src}>{src.split(" ").map(w => w[0]).join("")} · {d.date}</span>
                </div>
              </div>
            );
          })}
        </div>
      </div>
    </section>
  );
}

function Dashboard({ counts, total, avgProgress, activeStatuses, toggleStatus, data }) {
  const { lang, t } = useL();
  const segments = STATUS_DEFS.filter(s => counts[s.id] > 0);
  const kept = counts.kept;
  const keptPct = Math.round((kept / total) * 100);
  const latest = data.pledges.reduce((a, b) => new Date(a.lastUpdate) > new Date(b.lastUpdate) ? a : b);
  const latestTitle = lang === "fr" ? (latest.title_fr || latest.title) : latest.title;

  return (
    <section className="dashboard wrap">
      <div className="score-block">
        <div className="score-label">{t("Overall Pledge Score", "Score global des promesses")}</div>
        <div className="score-figure">{avgProgress}<span className="pct">%</span></div>
        <div className="score-bar" role="img" aria-label={t("Distribution of pledge statuses", "Répartition des statuts")}>
          {segments.map(s => (
            <div key={s.id} className={`bg-${s.id}`} style={{ width: `${(counts[s.id] / total) * 100}%` }}
              title={`${lang === "fr" ? s.label_fr : s.label}: ${counts[s.id]}`} />
          ))}
        </div>
        <div className="score-legend">
          {STATUS_DEFS.map(s => (
            <div key={s.id} className={`score-legend-row ${activeStatuses.has(s.id) ? "active" : ""}`} onClick={() => toggleStatus(s.id)}>
              <span className={`score-legend-dot bg-${s.id}`} />
              <span>{lang === "fr" ? s.label_fr : s.label}</span>
              <span className="score-legend-count">{counts[s.id]}</span>
            </div>
          ))}
        </div>
      </div>

      <div className="stats-block">
        <div className="stats-grid">
          <div className="stat">
            <div className="stat-label">{t("Pledges Tracked", "Promesses suivies")}</div>
            <div className="stat-value">{total}</div>
            <div className="stat-sub">{t("From the Liberal 2025 platform", "Plateforme libérale 2025")}</div>
          </div>
          <div className="stat">
            <div className="stat-label">{t("Kept", "Tenues")}</div>
            <div className="stat-value">{kept} <span style={{fontSize: "0.5em", color: "var(--ink-3)"}}>· {keptPct}%</span></div>
            <div className="stat-sub">{t("Substantially or fully delivered", "Réalisées en grande partie ou entièrement")}</div>
          </div>
          <div className="stat">
            <div className="stat-label">{t("At Risk", "À risque")}</div>
            <div className="stat-value">{counts.stalled + counts.broken}</div>
            <div className="stat-sub">{t("Stalled or broken", "Au point mort ou rompues")}</div>
          </div>
          <div className="stat">
            <div className="stat-label">{t("Last Movement", "Dernier mouvement")}</div>
            <div className="stat-value" style={{fontSize: 22, marginTop: 8}}>{fmtDate(latest.lastUpdate, lang)}</div>
            <div className="stat-sub" style={{textTransform: "none"}}>{latestTitle}</div>
          </div>
        </div>
      </div>
    </section>
  );
}

function Controls({ activeStatuses, toggleStatus, activeTopics, toggleTopic, search, setSearch, sortBy, setSortBy }) {
  const { lang, t } = useL();
  const totalActive = activeStatuses.size + activeTopics.size;
  const clearAll = () => {
    activeStatuses.forEach(s => toggleStatus(s));
    activeTopics.forEach(s => toggleTopic(s));
  };

  return (
    <section className="controls wrap">
      <div className="controls-top">
        <div className="search">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
            <circle cx="11" cy="11" r="7" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
          </svg>
          <input type="text" placeholder={t("Search pledges, topics, notes…", "Rechercher promesses, sujets, notes…")}
            value={search} onChange={e => setSearch(e.target.value)} />
        </div>
        <select className="sort-select" value={sortBy} onChange={e => setSortBy(e.target.value)}>
          <option value="default">{t("Sort · Original", "Tri · Original")}</option>
          <option value="progress">{t("Sort · Most progress", "Tri · Plus de progrès")}</option>
          <option value="recent">{t("Sort · Most recent", "Tri · Plus récentes")}</option>
          <option value="status">{t("Sort · At-risk first", "Tri · À risque d'abord")}</option>
        </select>
      </div>

      <div className="controls-row">
        <span className="controls-label">{t("Status", "Statut")}</span>
        <div className="chip-row">
          {STATUS_DEFS.map(s => (
            <button key={s.id} className={`chip ${activeStatuses.has(s.id) ? "active" : ""}`} onClick={() => toggleStatus(s.id)}>
              <span className={`chip-dot bg-${s.id}`} />
              {lang === "fr" ? s.short_fr : s.short}
            </button>
          ))}
        </div>

        <div className="controls-divider" />

        <span className="controls-label">{t("Topic", "Sujet")}</span>
        <div className="chip-row">
          {TOPICS.map(topic => (
            <button key={topic} className={`chip ${activeTopics.has(topic) ? "active" : ""}`} onClick={() => toggleTopic(topic)}>
              {topicLabel(topic, lang)}
            </button>
          ))}
        </div>

        {totalActive > 0 && (
          <button className="chip chip-clear" onClick={clearAll} title={t("Clear all filters", "Tout effacer")}>
            {t("Clear", "Effacer")} · {totalActive}
          </button>
        )}
      </div>
    </section>
  );
}

function PledgeList({ visible, total }) {
  const { t } = useL();
  return (
    <section className="pledge-list wrap">
      <div className="pledge-count">{t("Showing", "Affichage de")} {visible.length} {t("of", "sur")} {total}</div>
      {visible.length === 0 && (
        <div className="empty">{t("No pledges match these filters.", "Aucune promesse ne correspond à ces filtres.")}</div>
      )}
      {visible.map((p, i) => <PledgeRow key={p.id} pledge={p} num={i + 1} />)}
    </section>
  );
}

function PledgeRow({ pledge, num }) {
  const { lang, t } = useL();
  const def = STATUS_DEFS.find(s => s.id === pledge.status);
  const title = lang === "fr" ? (pledge.title_fr || pledge.title) : pledge.title;
  const text = lang === "fr" ? (pledge.pledge_fr || pledge.pledge) : pledge.pledge;
  const note = lang === "fr" ? (pledge.note_fr || pledge.note) : pledge.note;

  // Live macro context tied to this pledge (the integrated-context differentiator).
  const ctxId = CONTEXT_MAP[pledge.id];
  let ctx = null;
  if (ctxId) {
    const ind = MACRO_INDICATORS.find(i => i.id === ctxId);
    const md = (window.MACRO_DATA && window.MACRO_DATA[ctxId]) || (ind && ind.fallback);
    if (ind && md) ctx = `${lang === "fr" ? ind.label_fr : ind.label} ${ind.fmt(md.value)}${ind.unit}`;
  }

  return (
    <article className="pledge">
      <div className="pledge-num">{String(num).padStart(2, "0")}</div>
      <div className="pledge-body">
        <div className="pledge-topic">{topicLabel(pledge.topic, lang)}</div>
        <h3 className="pledge-title">{title}</h3>
        <p className="pledge-text">{text}</p>
        <blockquote className={`pledge-note s-${pledge.status}`} style={{color: `var(--${pledge.status})`, background: "transparent"}}>
          <span style={{color: "var(--ink)"}}>{note}</span>
        </blockquote>
        <div className="pledge-foot">
          <span>{t("Updated", "Mise à jour")} {fmtDate(pledge.lastUpdate, lang)}</span>
          <span>·</span>
          <a href={pledge.source} target="_blank" rel="noopener">Source</a>
          {ctx && <><span>·</span><span title={t("Live economic context", "Contexte économique en direct")}>{t("Context", "Contexte")}: {ctx}</span></>}
        </div>
      </div>
      <div className="status-col">
        <span className={`status-pill s-${pledge.status}`}>
          <span className="dot" />
          {lang === "fr" ? def.label_fr : def.label}
        </span>
        <div className="meter">
          <div className="meter-track">
            <div className={`meter-fill bg-${pledge.status}`} style={{ width: `${pledge.progress}%` }} />
          </div>
          <div className="meter-label">
            <span>{t("Progress", "Progrès")}</span>
            <b>{pledge.progress}%</b>
          </div>
        </div>
      </div>
    </article>
  );
}

/* ============================================================
   Methodology — the trust layer (status defs, AI role, sources,
   non-partisan statement, open data). Bilingual, inline-styled.
   ============================================================ */
function Methodology({ asOf }) {
  const { lang, t } = useL();
  const col = { fontSize: 13.5, color: "var(--ink-2)", lineHeight: 1.6 };
  const h = { fontFamily: "var(--mono)", fontSize: 10, letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--ink-3)", margin: "0 0 10px" };
  return (
    <section className="methodology wrap" style={{ paddingBlock: "36px 8px", borderTop: "1px solid var(--rule)" }}>
      <div className="hero-eyebrow">{t("Methodology & sources", "Méthodologie et sources")}</div>
      <h2 style={{ fontFamily: "var(--serif)", fontWeight: 400, fontSize: "clamp(24px,3vw,34px)", lineHeight: 1.1, letterSpacing: "-0.02em", margin: "0 0 22px", maxWidth: "24ch" }}>
        {t("How these verdicts are made", "Comment ces verdicts sont établis")}
      </h2>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(260px,1fr))", gap: 28 }}>
        <div style={col}>
          <p style={h}>{t("Status definitions", "Définitions des statuts")}</p>
          {STATUS_DEFS.map(s => (
            <div key={s.id} style={{ display: "flex", gap: 8, alignItems: "baseline", margin: "0 0 8px" }}>
              <span className={`score-legend-dot bg-${s.id}`} style={{ marginTop: 4, flexShrink: 0 }} />
              <span><b style={{ color: "var(--ink)" }}>{lang === "fr" ? s.label_fr : s.label}</b> — {lang === "fr" ? s.def_fr : s.def}</span>
            </div>
          ))}
        </div>
        <div style={col}>
          <p style={h}>{t("How a status is set", "Comment un statut est établi")}</p>
          <p style={{ margin: "0 0 12px" }}>{t(
            "Each pledge is classified from a dossier of cited public reporting (government releases, Statistics Canada, Parliament's bill tracker, and major outlets). An AI model (DeepSeek) analyses only that dossier — never its own training memory — and the reasoning behind every verdict, with source quotes, is kept in an open audit file.",
            "Chaque promesse est classée à partir d'un dossier de sources publiques citées (communiqués gouvernementaux, Statistique Canada, le suivi des projets de loi du Parlement et grands médias). Un modèle d'IA (DeepSeek) analyse uniquement ce dossier — jamais sa mémoire d'entraînement — et le raisonnement de chaque verdict, avec citations, est conservé dans un fichier d'audit ouvert."
          )}</p>
          <p style={{ margin: 0 }}>{t(
            "A pledge's progress % is an analytic estimate derived from the cited facts — not a measured value. Where sourced reporting is insufficient, a pledge is marked Not Started rather than guessed.",
            "Le pourcentage de progrès d'une promesse est une estimation analytique fondée sur les faits cités — non une valeur mesurée. Faute de sources suffisantes, une promesse est marquée « Non commencée » plutôt que devinée."
          )}</p>
        </div>
        <div style={col}>
          <p style={h}>{t("Independence & data", "Indépendance et données")}</p>
          <p style={{ margin: "0 0 12px" }}>{t(
            "This is a non-partisan accountability tool — it tracks delivery against the published platform, not whether a promise was wise. Kept and Broken verdicts are shown on equal footing.",
            "Outil de reddition de comptes non partisan — il mesure la réalisation par rapport à la plateforme publiée, et non la pertinence d'une promesse. Les verdicts « Tenue » et « Rompue » sont présentés sur un pied d'égalité."
          )}</p>
          <p style={{ margin: "0 0 12px" }}>{t(
            "Vote share is a monthly average of published polls; approval is the Angus Reid Institute approve rating; macro indicators are live from the Bank of Canada and Statistics Canada.",
            "L'intention de vote est une moyenne mensuelle des sondages publiés; le taux d'approbation provient de l'Institut Angus Reid; les indicateurs macro sont en direct de la Banque du Canada et de Statistique Canada."
          )}</p>
          <p style={{ margin: 0 }}>
            {t("Open data", "Données ouvertes")}: <a href="data.json">data.json</a> · {t("Updated", "Mis à jour")} {fmtDate(asOf, lang)}
          </p>
        </div>
      </div>
    </section>
  );
}

function Footnote({ asOf, sample }) {
  const { lang, t } = useL();
  return (
    <footer className="footnote wrap">
      <p>
        {sample && <><b style={{color: "var(--ink)"}}>{t("Sample data.", "Données d'exemple.")}</b>{" "}</>}
        {t(
          <>Pledge text is from the Liberal Party of Canada's 2025 platform at <a href="https://liberal.ca/plan/">liberal.ca/plan</a>. Status and progress are analyzed from cited public reporting — each pledge links its source. See the methodology above; progress is an analytic estimate, not a measured value.</>,
          <>Le texte des promesses provient de la plateforme 2025 du Parti libéral du Canada (<a href="https://liberal.ca/plan/">liberal.ca/plan</a>). Le statut et le progrès sont analysés à partir de sources publiques citées — chaque promesse renvoie à sa source. Voir la méthodologie ci-dessus; le progrès est une estimation analytique, non une valeur mesurée.</>
        )}
      </p>
      <div className="footnote-meta">
        {t("Data as of", "Données au")} · {fmtDate(asOf, lang)}
      </div>
    </footer>
  );
}

function TrackerTweaks({ t, setTweak }) {
  const { TweaksPanel: Panel, TweakSection, TweakRadio, TweakSelect } = window;
  if (!Panel) return null;
  return (
    <Panel title="Tweaks">
      <TweakSection title="Appearance">
        <TweakRadio label="Theme" value={t.theme} onChange={(v) => setTweak("theme", v)}
          options={[{ value: "cream", label: "Cream" }, { value: "paper", label: "Paper" }, { value: "ink", label: "Ink" }]} />
        <TweakRadio label="Density" value={t.density} onChange={(v) => setTweak("density", v)}
          options={[{ value: "comfortable", label: "Comfortable" }, { value: "compact", label: "Compact" }]} />
      </TweakSection>
      <TweakSection title="Sort">
        <TweakSelect label="Sort pledges by" value={t.sortBy} onChange={(v) => setTweak("sortBy", v)}
          options={[{ value: "default", label: "Original order" }, { value: "progress", label: "Most progress" },
                    { value: "recent", label: "Most recent" }, { value: "status", label: "At-risk first" }]} />
      </TweakSection>
    </Panel>
  );
}

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