/* StrategyLabPage — final design (15 Sep 2026, built from Gianni's signed-off mockup).

   Two Lab-managed live strategies:
     Robust — always the best settings over ALL trades ever; the nightly job proposes a
              change only when a challenger leads the live card by >=5% of its all-time
              P&L (percentage, never dollars — a $ threshold goes stale as data grows).
     Regime — the best settings over the last 20 trading days, HELD until the Regime
              change checker (5 signals, 3-of-5, two nights) confirms the market changed.
   Proposals come from lab_proposals (pending) — the page's buttons apply or skip them;
   nothing ever changes a live card without a tap. Stats (Data coverage first, Entry
   stats, By month, Sizing simulations, Run history) are collapsed by default. */

const SL_pct = (v) => (v == null ? "—" : Math.round(Number(v) * 100));

function SL_money(v) {
  if (v == null) return "—";
  const n = Number(v);
  if (n < 0) return "−$" + Math.abs(Math.round(n)).toLocaleString("en-US");
  return (n > 0 ? "+" : "") + "$" + Math.round(n).toLocaleString("en-US");
}

/* grid/proposal config -> strategies-table columns */
function SL_payload(cfg) {
  return {
    stop_loss_pct: cfg.stop == null ? null : Number(cfg.stop),
    breakeven_at_pct: cfg.be == null ? null : Number(cfg.be),
    take_profit_pct: cfg.tp == null ? null : Number(cfg.tp),
    take_half_at_pct: cfg.half == null ? null : Number(cfg.half),
    trailing_tiers: cfg.trail ? [{ at: Number(cfg.trail[0]), trail: Number(cfg.trail[1]) }] : [],
    add_on_loss_pct: cfg.add == null ? null : Number(cfg.add),
    add_max_count: cfg.add == null ? null : (cfg.add_max || 1),
    add_size_mode: cfg.add == null ? null : (cfg.add_size || "double"),
  };
}

/* strategies row -> grid-shaped config (for diffs and edited-detection) */
function SL_rowCfg(row) {
  if (!row) return null;
  const f = (v) => (v == null ? null : Number(v));
  const tiers = row.trailing_tiers || [];
  const t0 = tiers[0];
  return {
    stop: f(row.stop_loss_pct), be: f(row.breakeven_at_pct), tp: f(row.take_profit_pct),
    half: f(row.take_half_at_pct),
    trail: t0 ? [f(t0.at != null ? t0.at : t0[0]), f(t0.trail != null ? t0.trail : t0[1])] : null,
    add: f(row.add_on_loss_pct),
    add_max: row.add_on_loss_pct == null ? null : (row.add_max_count || 1),
    add_size: row.add_on_loss_pct == null ? null : (row.add_size_mode || "double"),
  };
}

function SL_sameCfg(a, b) {
  if (!a || !b) return false;
  const f = (v) => (v == null ? null : Math.round(Number(v) * 1e4));
  const tr = (t) => (t && t.length ? f(t[0]) + "/" + f(t[1]) : "off");
  const addTail = (c) => (c.add == null ? "off" : (c.add_max || 1) + "/" + (c.add_size || "double"));
  return f(a.stop) === f(b.stop) && f(a.be) === f(b.be) && f(a.tp) === f(b.tp)
    && f(a.half) === f(b.half) && tr(a.trail) === tr(b.trail)
    && f(a.add) === f(b.add) && addTail(a) === addTail(b);
}

/* config -> per-setting words (diff tables) */
function SL_words(p) {
  if (!p) return {};
  return {
    "Stop loss": p.stop != null ? "−" + SL_pct(p.stop) + "%" : "none",
    "Breakeven": p.be != null ? "from +" + SL_pct(p.be) + "%" : "off",
    "Profit taking": p.tp != null ? "take profit +" + SL_pct(p.tp) + "%"
      : (p.trail && p.trail.length ? "trail: arms +" + SL_pct(p.trail[0]) + "%, gives back " + SL_pct(p.trail[1]) + "%" : "none"),
    "Take half": p.half != null ? "at +" + SL_pct(p.half) + "%" : "off",
    "Buy the dip (DCA)": p.add != null
      ? "at −" + SL_pct(p.add) + "% · up to " + (p.add_max || 1) + "× · " + ((p.add_size || "double") === "entry" ? "same as entry" : "doubling")
      : "off",
  };
}

function SL_chips(p) {
  const out = [];
  if (!p) return out;
  if (p.stop != null) out.push("stop −" + SL_pct(p.stop) + "%");
  if (p.be != null) out.push("BE +" + SL_pct(p.be) + "%");
  if (p.tp != null) out.push("TP +" + SL_pct(p.tp) + "%");
  if (p.trail && p.trail.length) out.push("trail " + SL_pct(p.trail[0]) + "→" + SL_pct(p.trail[1]));
  if (p.half != null) out.push("half +" + SL_pct(p.half) + "%");
  if (p.add != null) out.push("dip-buy −" + SL_pct(p.add) + "% · " + (p.add_max || 1) + "× · " + ((p.add_size || "double") === "entry" ? "entry" : "dbl"));
  return out;
}

/* Vraag de machine om DEZE kaart opnieuw door te rekenen. Gestuurd, niet gepold:
   dit gebeurt een paar keer per week, en de heartbeat kijkt toch al elke 15-90s of er
   een opdracht klaarstaat — dus het kost geen enkele extra query (Gianni, 21 sep). */
async function SL_requestRescore() {
  try {
    await window.NT_CLIENT.from("machine_commands")
      .insert({ machine_id: "G-Home", command: "rescore", status: "pending" });
  } catch (e) { /* lukt dit niet, dan zet de nachtelijke analyse het alsnog recht */ }
}

/* "?" chip — desktop popover, mobile bottom sheet */
function SL_Help({ text }) {
  const [open, setOpen] = React.useState(false);
  const isMobile = useIsMobile();
  const pop = isMobile
    ? { position: "fixed", left: 12, right: 12, bottom: 12, zIndex: 200 }
    : { position: "absolute", top: 24, right: -8, zIndex: 40, width: 300 };
  return (
    <span style={{ position: "relative", display: "inline-flex", marginLeft: 8, verticalAlign: "middle" }}>
      <button onClick={() => setOpen((o) => !o)} aria-label="What is this?"
        style={{ width: 17, height: 17, borderRadius: 999, border: "1px solid var(--border-strong)",
                 background: open ? "var(--accent)" : "var(--surface-inset)", color: open ? "#fff" : "var(--text-tertiary)",
                 font: "var(--w-semibold) 10px/1 var(--font-mono)", cursor: "pointer", padding: 0 }}>?</button>
      {open && isMobile ? (
        <span onClick={() => setOpen(false)} style={{ position: "fixed", inset: 0, zIndex: 199, background: "rgba(0,0,0,.35)" }} />
      ) : null}
      {open ? (
        <span onClick={() => setOpen(false)}
              style={{ ...pop, display: "block", background: "var(--surface-card)", border: "1px solid var(--border-strong)",
                       borderRadius: "var(--radius-md)", padding: isMobile ? "16px 18px" : "12px 14px",
                       font: "var(--w-regular) " + (isMobile ? "var(--t-sm)" : "var(--t-xs)") + "/1.6 var(--font-sans)",
                       color: "var(--text-secondary)", boxShadow: "0 10px 30px rgba(0,0,0,.45)" }}>{text}</span>
      ) : null}
    </span>
  );
}

const SL_HELP = {
  robust: "This strategy always runs the settings that made the most over ALL recorded trades — every market since June, replayed nightly through the real engine (thousands of combinations, all with a stop loss). If a different combination beats your card by at least 5% of its all-time P&L, a proposal appears here with a button; nothing changes without your tap. Smaller leads are noise and are ignored. Expect that a few times a year.",
  regime: "This strategy runs the best settings over the LAST 20 trading days — but it does NOT chase the nightly leaderboard. It holds its settings until the Regime change checker below confirms the market itself changed (3 of 5 signals, two nights in a row). Then a proposal appears in the checker's banner; your tap applies it. After a change: at least 5 quiet trading days.",
  checker: "Five signals, each comparing the recent days against their long-run normal: how far alerts bounce, how often they dip deep first, your recent losing trades, the account's pullback, and which profit-taking style is winning. 3 of 5 turned, two nights in a row = the market changed → the banner turns orange with a proposal for the Regime strategy. It never touches Robust and never changes anything by itself.",
  risk: "How a setting BEHAVES, not just what it adds up to. The equity curve with its drawdown underneath, how deep a bad stretch can really get (the same trades reshuffled 2,000 times — one observed ordering badly understates the risk), every trade from biggest win to biggest loss, and the dip-buy landscape: how deep and how often. Every figure is shown in pairs — the winning side next to the losing one.",
  stats: "Everything measured, one tab each: Data coverage (every alerted entry vs its recorded tape — follow the recording), Entry stats (weekday, entry hour, trade # of the day, news days, alert wording — average P&L per trade under each live strategy), By month, Sizing simulations (what money-management rules like half-after-a-loss would have done on your real trades), and Run history (every analysis and every change).",
};

function StrategyLabPage() {
  const NT = window.NitroTraderDesignSystem_95e598;
  const isMobile = useIsMobile();
  const [run, setRun] = React.useState(undefined);
  const [hist, setHist] = React.useState([]);
  const [liveRows, setLiveRows] = React.useState({});
  const [props, setProps] = React.useState([]);
  const [applied, setApplied] = React.useState({});
  const [applying, setApplying] = React.useState(false);
  const [showDiff, setShowDiff] = React.useState(false);
  const [cmpOpen, setCmpOpen] = React.useState({});
  const [statsOpen, setStatsOpen] = React.useState(false);
  const [statsTab, setStatsTab] = React.useState("coverage");
  const [riskOpen, setRiskOpen] = React.useState(true);
  const [riskRole, setRiskRole] = React.useState("regime");
  const [riskWhich, setRiskWhich] = React.useState("own");
  const [cov, setCov] = React.useState(null);
  const [covSrc, setCovSrc] = React.useState("all");

  const load = React.useCallback(async () => {
    try {
      const c = window.NT_CLIENT;
      const [r1, r2, r3, r4, r5] = await Promise.all([
        c.from("lab_runs").select("ran_at,pool_n,grid_n,roles,signals,flip,regime,segments,top")
          .order("ran_at", { ascending: false }).limit(1),
        c.from("lab_runs").select("ran_at,pool_n,roles,flip").order("ran_at", { ascending: false }).limit(30),
        c.from("strategies").select("id,name,broker_account,lab_role,stop_loss_pct,breakeven_at_pct,take_profit_pct,take_half_at_pct,trailing_tiers,add_on_loss_pct,add_max_count,add_size_mode")
          .not("lab_role", "is", null),
        c.from("lab_proposals").select("id,strategy_id,role,config,label,stats,created_at").eq("status", "pending"),
        c.from("lab_proposals").select("strategy_id,decided_at").eq("status", "applied").order("decided_at", { ascending: false }).limit(20),
      ]);
      setRun((r1.data && r1.data[0]) || null);
      setHist(r2.data || []);
      const m = {}; (r3.data || []).forEach((x) => { m[String(x.id)] = x; });
      setLiveRows(m);
      setProps(r4.data || []);
      const ap = {}; (r5.data || []).forEach((x) => { if (!ap[String(x.strategy_id)]) ap[String(x.strategy_id)] = x.decided_at; });
      setApplied(ap);
    } catch (e) { setRun(null); }
  }, []);
  React.useEffect(() => { load(); const t = setInterval(load, 300000); return () => clearInterval(t); }, [load]);
  React.useEffect(() => { if (window.lucide) window.lucide.createIcons(); });

  /* Data coverage — ported unchanged from the Weekly Lab (the "visual database") */
  React.useEffect(() => {
    const c = window.NT_CLIENT;
    if (!c) { setCov([]); return; }
    (async () => {
      try {
        const [alR, poolR, srcR] = await Promise.all([
          c.from("alerts").select("ts,ticker,source_id,raw").eq("type", "ENTRY").order("ts", { ascending: false }).limit(1000),
          c.from("replay_pool_cache").select("ticker,strike,side,entry_ts,ticks,entry_price,peak_price,low_price,tape_first,tape_last"),
          c.from("sources").select("id,name,type,category"),
        ]);
        const pool = poolR.data || [];
        const srcName = {}, srcCat = {};
        (srcR.data || []).forEach((s2) => { srcName[s2.id] = s2.name || s2.type || ("source " + s2.id); srcCat[s2.id] = s2.category; });
        const optionsOnly = (alR.data || []).filter((a) => a.source_id == null || srcCat[a.source_id] == null || srcCat[a.source_id] === "options");
        const rows = optionsOnly.map((a) => {
          const t0 = new Date(a.ts).getTime();
          let best = null;
          pool.forEach((p2) => {
            if (p2.ticker !== a.ticker) return;
            const dt = new Date(p2.entry_ts).getTime() - t0;
            if (dt < -3 * 60000 || dt > 15 * 60000) return;
            if (!best || Math.abs(dt) < Math.abs(best.dt)) best = { ...p2, dt };
          });
          const ep = best ? Number(best.entry_price) : 0;
          const pk = (best && best.peak_price != null && ep) ? (Number(best.peak_price) / ep - 1) * 100 : null;
          const dd = (best && best.low_price != null && ep) ? (Number(best.low_price) / ep - 1) * 100 : null;
          const mins = (best && best.tape_first && best.tape_last)
            ? Math.round((new Date(best.tape_last) - new Date(best.tape_first)) / 60000) : null;
          return { ts: a.ts, ticker: a.ticker, source: srcName[a.source_id] || (a.source_id == null ? "—" : "src " + a.source_id),
                   contract: best ? (String(Number(best.strike)) + (best.side || "")) : null,
                   ticks: best ? Number(best.ticks) : 0, taped: !!best, peak: pk, dd: dd, mins: mins };
        });
        setCov(rows);
      } catch (e) { setCov([]); }
    })();
  }, []);

  const mono = { font: "var(--w-regular) var(--t-sm)/1 var(--font-mono)", fontVariantNumeric: "tabular-nums" };
  const lbl = { font: "var(--w-semibold) var(--t-2xs)/1 var(--font-mono)", letterSpacing: "var(--ls-wide)", textTransform: "uppercase", color: "var(--text-tertiary)" };
  const money = (v) => <span style={{ ...mono, color: v > 0 ? "var(--profit)" : v < 0 ? "var(--loss)" : "var(--text-secondary)" }}>{SL_money(v)}</span>;
  const linkBtn = { background: "none", border: "none", cursor: "pointer", padding: 0, color: "var(--accent)", font: "var(--w-medium) var(--t-xs)/1 var(--font-sans)" };

  if (run === undefined) {
    return <div style={{ color: "var(--text-tertiary)", font: "var(--w-regular) var(--t-sm)/1 var(--font-sans)" }}>Loading…</div>;
  }
  const head = (
    <PageHead title="Strategy Lab"
      subtitle={run
        ? "every settings combination × every recorded trade, replayed nightly through the real engine · " +
          (run.grid_n || 0).toLocaleString() + " combinations · " + run.pool_n + " trades · last run " +
          new Date(run.ran_at).toLocaleString("nl-NL", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" })
        : "every settings combination × every recorded trade, replayed nightly through the real engine"} />
  );
  if (!run || !run.roles) {
    return (
      <div style={{ display: "flex", flexDirection: "column", gap: "var(--gap-grid)" }}>
        {head}
        <NT.Card padding={20}>
          <div style={{ padding: "40px 20px", textAlign: "center", color: "var(--text-tertiary)", font: "var(--w-regular) var(--t-sm)/1.6 var(--font-sans)" }}>
            The first analysis under the Robust / Regime design lands with the next nightly run (23:15). Both strategy cards, the Regime change checker and all stats appear here then.
          </div>
        </NT.Card>
      </div>
    );
  }

  const acctTail = (bid) => {
    const b = ((window.NT_DATA || {}).brokerAccounts || []).find((x) => x.id === bid);
    if (!b) return bid ? "account " + bid : "";
    const ref = b.account_ref ? " ···" + String(b.account_ref).slice(-4) : "";
    return (b.label || "Schwab") + ref;
  };
  const pendingFor = (sid) => props.find((p) => String(p.strategy_id) === String(sid));
  const signals = run.signals || [];
  const turned = signals.filter((s) => s.turned).length;

  const diffTable = (yourW, otherW, otherName, ownStats, otherStats, yoursName) => (
    <div style={{ marginTop: 12, border: "1px solid var(--border)", borderRadius: "var(--radius-md)", overflow: "hidden", background: "var(--surface-card)" }}>
      <table style={{ width: "100%", borderCollapse: "collapse" }}>
        <thead><tr>{["setting", yoursName || "yours", otherName].map((h) => (
          <th key={h} style={{ ...lbl, textAlign: "left", padding: "9px 12px", background: "var(--surface-inset)" }}>{h}</th>))}</tr></thead>
        <tbody>
          {Object.keys(yourW).map((k) => {
            const differs = yourW[k] !== otherW[k];
            const td = { padding: "9px 12px", borderTop: "1px solid var(--border)", font: "var(--w-regular) var(--t-xs)/1.45 var(--font-sans)", color: differs ? "var(--text-primary)" : "var(--text-secondary)" };
            return (
              <tr key={k} style={differs ? { background: "color-mix(in srgb, var(--accent) 10%, transparent)" } : null}>
                <td style={{ ...td, color: "var(--text-tertiary)", width: "30%" }}>{k}</td>
                <td style={td}>{yourW[k]}</td><td style={td}>{otherW[k]}</td>
              </tr>);
          })}
          {ownStats && otherStats ? (
            <tr>
              <td style={{ padding: "9px 12px", borderTop: "1px solid var(--border)", font: "var(--w-regular) var(--t-xs)/1.4 var(--font-sans)", color: "var(--text-tertiary)" }}>Would have made</td>
              <td style={{ padding: "9px 12px", borderTop: "1px solid var(--border)" }}>{money(ownStats.pnl)} <span style={{ font: "var(--w-regular) var(--t-2xs)/1 var(--font-sans)", color: "var(--text-tertiary)" }}>all · {money(ownStats.new20d)} 20d</span></td>
              <td style={{ padding: "9px 12px", borderTop: "1px solid var(--border)" }}>{money(otherStats.pnl)} <span style={{ font: "var(--w-regular) var(--t-2xs)/1 var(--font-sans)", color: "var(--text-tertiary)" }}>all · {money(otherStats.new20d)} 20d</span></td>
            </tr>) : null}
        </tbody>
      </table>
    </div>
  );

  const decide = async (prop, name, apply) => {
    if (apply) {
      const ok = await window.NT_CONFIRM(
        "Apply to " + name + ":\n\n" + prop.label + "\n\n" +
        "Measured: " + SL_money((prop.stats || {}).new20d) + " last 20 days · " + SL_money((prop.stats || {}).pnl) +
        " over all trades · worst trade " + SL_money((prop.stats || {}).worst) + ".",
        { title: "Change live strategy settings?", confirmText: "Apply" });
      if (!ok) return;
    }
    setApplying(true);
    try {
      if (apply) {
        const r = await window.NT_CLIENT.from("strategies").update(SL_payload(prop.config)).eq("id", Number(prop.strategy_id));
        if (r.error) throw r.error;
      }
      await window.NT_CLIENT.from("lab_proposals")
        .update({ status: apply ? "applied" : "skipped", decided_at: new Date().toISOString() })
        .eq("id", prop.id);
      if (apply) await SL_requestRescore();
      await load();
      if (apply) await window.NT_ALERT("Applied — the card now runs: " + prop.label + ". The numbers below refresh in a moment.", { title: "Done" });
    } catch (e) {
      await window.NT_ALERT("Could not save: " + (e.message || e), { title: "Failed" });
    }
    setApplying(false);
  };

  /* ---------- one role card ---------- */
  const roleCard = (role) => {
    const rr = (run.roles || {})[role];
    if (!rr) return null;
    const sid = String(rr.strategy_id);
    const row = liveRows[sid];
    const curCfg = SL_rowCfg(row);
    const edited = curCfg && rr.own && !SL_sameCfg(curCfg, rr.own);
    const own = rr.own || {};
    const best = rr.best || {};
    const pill = role === "robust" ? "BEST OVER ALL TRADES" : "BEST OVER LAST 20 DAYS";
    const pending = role === "robust" ? pendingFor(sid) : null;
    const refit = applied[sid];
    const open = !!cmpOpen[role];
    return (
      <NT.Card key={role} padding={0}>
        <div style={{ padding: isMobile ? "16px 16px" : "20px 22px" }}>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
            <div style={{ font: "var(--w-semibold) 17px/1.2 var(--font-sans)", color: "var(--text-primary)" }}>
              {role === "robust" ? "Robust" : "Regime"}
            </div>
            <span style={{ display: "inline-flex", alignItems: "center", flex: "none" }}>
              <span style={{ padding: "6px 12px", borderRadius: 999, background: "var(--profit-bg)", color: "var(--profit)", font: "var(--w-semibold) var(--t-2xs)/1 var(--font-mono)", letterSpacing: "var(--ls-wide)" }}>
                {pill}
              </span>
              <SL_Help text={SL_HELP[role]} />
            </span>
          </div>
          <div style={{ font: "var(--w-light) 38px/1.1 var(--font-mono)", fontVariantNumeric: "tabular-nums", color: (best.pnl || 0) >= 0 ? "var(--profit)" : "var(--loss)", margin: "18px 0 4px" }}>{SL_money(best.pnl)}</div>
          <div style={{ font: "var(--w-regular) var(--t-2xs)/1.4 var(--font-sans)", color: "var(--text-tertiary)", marginBottom: 16 }}>
            {(role === "robust"
              ? "the best settings over all trades (and green in the last 20 days)"
              : "the best settings over the last 20 trading days — picked ON those same days, in hindsight, so this number flatters itself; only the days AFTER a switch tell you if it holds")
              + " — SIMULATED on all " + run.pool_n + " recorded trades: what these settings would have made, not what was banked (that lives on Trades)"}
          </div>
          <div style={{ marginBottom: 14 }}>
            {SL_chips(best).map((c2, i) => (
              <span key={i} style={{ display: "inline-block", padding: "6px 11px", borderRadius: 999, background: "var(--surface-inset)", font: "var(--w-regular) var(--t-2xs)/1 var(--font-sans)", color: "var(--text-secondary)", margin: "0 6px 6px 0", whiteSpace: "nowrap" }}>{c2}</span>
            ))}
          </div>
          <div style={{ display: "flex", gap: isMobile ? 20 : 30, flexWrap: "wrap", paddingTop: 14, borderTop: "1px solid var(--border)" }}>
            <span><div style={lbl}>win</div><div style={{ ...mono, marginTop: 6 }}>{best.win}%</div></span>
            <span><div style={lbl}>avg / trade</div><div style={{ ...mono, marginTop: 6 }}>{money(best.avg)}</div></span>
            <span><div style={lbl}>worst trade</div><div style={{ ...mono, marginTop: 6 }}>{money(best.worst)}</div></span>
            <span><div style={lbl}>last 20 days</div><div style={{ ...mono, marginTop: 6 }}>{money(best.new20d)}</div></span>
          </div>
          {pending ? (
            <div style={{ marginTop: 14, padding: "12px 14px", borderRadius: "var(--radius-md)", background: "color-mix(in srgb, #d9a53f 10%, transparent)" }}>
              <div style={{ font: "var(--w-semibold) var(--t-xs)/1.5 var(--font-sans)", color: "#d9a53f" }}>
                {(function () {
                  // Show the engine's own reason VERBATIM. This block used to rebuild a
                  // worst-trade story for every risk-limit note — on 30 Sep that put
                  // "can cost -310, your limit is -502" on screen while the REAL trigger
                  // was the drawdown limit. The note is the source of truth; never
                  // reconstruct a check the engine didn't print.
                  const why = ((pending.stats || {}).note) || "";
                  if (why) return why.charAt(0).toUpperCase() + why.slice(1) + ". Proposed: ";
                  const e = (pending.stats && pending.stats.edge_pct != null) ? pending.stats.edge_pct : rr.edge_pct;
                  return "Better-shaped setting" + (e == null ? "" : " (" + (e > 0 ? "+" : "") + e + "% result)") + ": ";
                })()}{pending.label}
              </div>
              <div style={{ display: "flex", gap: 12, marginTop: 10, alignItems: "center", flexWrap: "wrap" }}>
                <button disabled={applying} onClick={() => decide(pending, rr.name, true)}
                  style={{ background: "#d9a53f", color: "#1a1405", border: "none", borderRadius: "var(--radius-sm)", padding: "9px 14px", font: "var(--w-semibold) var(--t-xs)/1 var(--font-sans)", cursor: "pointer" }}>Adjust Robust →</button>
                <button disabled={applying} onClick={() => decide(pending, rr.name, false)} style={{ ...linkBtn, color: "var(--text-tertiary)" }}>skip until next update</button>
              </div>
            </div>
          ) : null}
          <div style={{ marginTop: 14, display: "flex", justifyContent: "space-between", alignItems: "center", gap: 10 }}>
            <span style={{ font: "var(--w-regular) var(--t-2xs)/1.4 var(--font-sans)", color: "var(--text-tertiary)" }}>
              {(rr.matches_best && !edited
                  ? ((row && row.name) || rr.name) + " runs exactly this ✓ · "
                  : ((row && row.name) || rr.name) + (edited ? " was edited — rescored tonight · " : " runs different settings — "))
                + (rr.matches_best && !edited ? ""
                   : role === "robust"
                     ? (pending ? "review the proposal below · "
                        : (rr.edge_pct != null ? "best is " + (rr.edge_pct >= 0 ? "+" : "") + rr.edge_pct + "% ahead — " : "")
                          + "a proposal appears at 5%+ · ")
                     : "a proposal appears in the Regime change checker on a confirmed regime change · ")
                + (rr.account === "fronttest" ? "paper card — measures every trade, places no real orders"
                   : rr.account === "draft" ? "archived card — the Lab still measures it nightly on every recorded trade"
                   : acctTail(rr.broker_account))}
            </span>
            <button onClick={() => setCmpOpen((o) => ({ ...o, [role]: !o[role] }))} style={{ ...linkBtn, flex: "none" }}>compare {open ? "▴" : "▾"}</button>
          </div>
          {open ? diffTable(SL_words(curCfg || rr.own), SL_words(rr.best), role === "robust" ? "the all-time best" : "the 20-day best", own, rr.best, ((row && row.name) || rr.name)) : null}
        </div>
      </NT.Card>
    );
  };

  /* ---------- banner ---------- */
  const regRR = (run.roles || {}).regime;
  const regPending = regRR ? pendingFor(String(regRR.strategy_id)) : null;
  const turnedNames = signals.filter((s) => s.turned).map((s) => s.label).join(", ");
  const banner = regPending ? (
    <div style={{ marginTop: 14, background: "color-mix(in srgb, #d9a53f 10%, transparent)", borderRadius: "var(--radius-lg)", padding: "16px 18px", display: "flex", alignItems: "center", gap: 16, flexWrap: "wrap" }}>
      <span style={{ flex: "none", fontSize: 16, color: "#d9a53f" }}>⚠</span>
      <div style={{ flex: 1, minWidth: 240, font: "var(--w-regular) var(--t-xs)/1.55 var(--font-sans)", color: "var(--text-secondary)" }}>
        {/* The banner states the REAL reason for the proposal. There are two now: a
            confirmed regime change, or settings that sit outside the risk limits. It used
            to tell the regime story always, even at 1 of 5 signals — Gianni saw
            "the market changed" on a night when nothing had changed. */}
        {(function () {
          const why = ((regPending.stats || {}).note) || "";
          const risk = /risk limit|risicogrens/i.test(why);
          if (risk) {
            // the engine's note IS the reason (it names which limit broke — worst
            // trade or realistic drawdown); never rebuild a different story here
            return (<React.Fragment>
              <b style={{ color: "#d9a53f" }}>Riskier than your own limit.</b> The market has not changed
              ({turned} of 5 signals) — but {why.replace(/^your settings break your own risk limit:\s*/i, "")}.
              The proposal below stays inside your limits. </React.Fragment>);
          }
          return (<React.Fragment>
            <b style={{ color: "#d9a53f" }}>Consider switching.</b> {turned} of 5 signals say the market changed ({turnedNames}) — two nights in a row.
            Green across the board = keep what you have. </React.Fragment>);
        })()}
        <button style={linkBtn} onClick={() => setShowDiff((v) => !v)}>show changed settings {showDiff ? "▴" : "▾"}</button>
      </div>
      <div style={{ flex: "none", display: "flex", flexDirection: "column", gap: 8, alignItems: "center" }}>
        <button disabled={applying} onClick={() => decide(regPending, regRR.name, true)}
          style={{ background: "#d9a53f", color: "#1a1405", border: "none", borderRadius: "var(--radius-md)", padding: "11px 18px", font: "var(--w-semibold) var(--t-sm)/1 var(--font-sans)", cursor: "pointer" }}>Adjust Regime →</button>
        <button disabled={applying} onClick={() => decide(regPending, regRR.name, false)} style={{ ...linkBtn, color: "var(--text-tertiary)" }}>skip until next update</button>
      </div>
      {showDiff ? (
        <div style={{ flexBasis: "100%" }}>
          {diffTable(SL_words(SL_rowCfg(liveRows[String(regPending.strategy_id)])), SL_words(regPending.config), "proposed", null, null, ((liveRows[String(regPending.strategy_id)] || {}).name || regRR.name))}
        </div>) : null}
    </div>
  ) : (
    <div style={{ marginTop: 14, background: run.flip ? "color-mix(in srgb, #d9a53f 10%, transparent)" : "color-mix(in srgb, var(--profit) 10%, transparent)", borderRadius: "var(--radius-lg)", padding: "13px 18px", font: "var(--w-medium) var(--t-xs)/1.55 var(--font-sans)", color: run.flip ? "#d9a53f" : "var(--profit)" }}>
      {run.flip
        ? turned + " of 5 signals turned (" + turnedNames + ") — first night. A second night in a row confirms the change and brings a proposal here."
        : turned + " of 5 signals turned → below the 3-of-5 line → market unchanged → the Regime strategy keeps its settings."}
    </div>
  );

  /* ---------- stats tabs ---------- */
  const seg = run.segments || {};
  const buckets = (seg.buckets || {});
  const ROLES = ["robust", "regime"];
  const avgTable = (title, dim, keyOrder) => {
    const b = buckets[dim] || {};
    let keys = Object.keys(b);
    keys = keyOrder ? keyOrder.filter((k) => keys.indexOf(k) >= 0) : keys.sort();
    return (
      <NT.Card padding={0} key={dim}>
        <div style={{ padding: "13px 16px 9px", font: "var(--w-semibold) var(--t-sm)/1 var(--font-sans)", color: "var(--text-primary)" }}>{title}</div>
        <div style={{ overflowX: "auto" }}>
          <table style={{ width: "100%", borderCollapse: "collapse" }}>
            <thead><tr>
              <th style={{ ...lbl, textAlign: "left", padding: "8px 16px", borderBottom: "1px solid var(--border)" }}>&nbsp;</th>
              <th style={{ ...lbl, textAlign: "right", padding: "8px 12px", borderBottom: "1px solid var(--border)" }}>trades</th>
              {ROLES.map((r2) => (
                <th key={r2} style={{ ...lbl, textAlign: "right", padding: "8px 16px", borderBottom: "1px solid var(--border)" }}>{r2} avg</th>))}
            </tr></thead>
            <tbody>{keys.map((k) => (
              <tr key={k}>
                <td style={{ padding: "9px 16px", borderTop: "1px solid var(--border)", font: "var(--w-regular) var(--t-xs)/1.4 var(--font-sans)", color: "var(--text-primary)", whiteSpace: "nowrap" }}>{k}</td>
                <td style={{ padding: "9px 12px", borderTop: "1px solid var(--border)", textAlign: "right", ...mono, color: "var(--text-tertiary)" }}>{b[k].n}</td>
                {ROLES.map((r2) => (
                  <td key={r2} style={{ padding: "9px 16px", borderTop: "1px solid var(--border)", textAlign: "right" }}>{money(((b[k].cards || {})[r2] || {}).avg)}</td>))}
              </tr>))}
            </tbody>
          </table>
        </div>
      </NT.Card>
    );
  };

  const covTable = () => {
    if (cov == null) return <NT.Card padding={20}><div style={{ padding: "20px 0", textAlign: "center", color: "var(--text-tertiary)", font: "var(--w-regular) var(--t-sm)/1 var(--font-sans)" }}>Loading coverage…</div></NT.Card>;
    const channels = Array.from(new Set(cov.map((r2) => r2.source)));
    const shown = covSrc === "all" ? cov : cov.filter((r2) => r2.source === covSrc);
    const total = shown.length, taped = shown.filter((r2) => r2.taped).length;
    const CUT = "2026-08-24";
    const recent = shown.filter((r2) => r2.ts >= CUT);
    const recentTaped = recent.filter((r2) => r2.taped).length;
    const dOf = (iso) => { try { return new Intl.DateTimeFormat("en-GB", { timeZone: "Europe/Amsterdam", day: "2-digit", month: "short" }).format(new Date(iso)); } catch (e) { return "—"; } };
    const tmOf = (iso) => { try { return new Intl.DateTimeFormat("nl-NL", { timeZone: "Europe/Amsterdam", hour: "2-digit", minute: "2-digit", hour12: false }).format(new Date(iso)); } catch (e) { return "—"; } };
    const th2 = { ...lbl, textAlign: "right", padding: "8px 12px", borderBottom: "1px solid var(--border)" };
    const thL2 = { ...th2, textAlign: "left" };
    const td2 = { padding: "8px 12px", borderTop: "1px solid var(--border)", textAlign: "right", font: "var(--w-regular) var(--t-xs)/1 var(--font-mono)", fontVariantNumeric: "tabular-nums" };
    const tdL2 = { ...td2, textAlign: "left", font: "var(--w-regular) var(--t-xs)/1.3 var(--font-sans)" };
    const pct2 = (v) => (v > 0 ? "+" : "") + Math.round(v) + "%";
    return (
      <NT.Card padding={0}>
        <div style={{ padding: "13px 16px 9px", display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
          <span style={{ font: "var(--w-semibold) var(--t-sm)/1 var(--font-sans)", color: "var(--text-primary)" }}>Data coverage — every alerted entry vs recorded tape</span>
          <span style={{ display: "inline-flex", alignItems: "center", gap: 12 }}>
            <span style={{ font: "var(--w-regular) var(--t-2xs)/1 var(--font-sans)", color: "var(--text-tertiary)" }}>live from the database · {taped} of {total} alerts taped · since 24 Aug {recentTaped}/{recent.length}</span>
            {channels.length > 1 ? <NT_Select value={covSrc} onChange={setCovSrc}
              options={[{ value: "all", label: "All channels" }].concat(channels.map((c2) => ({ value: c2, label: c2 })))} /> : null}
          </span>
        </div>
        <div style={{ overflowX: "auto", maxHeight: 430, overflowY: "auto" }}>
          <table style={{ width: "100%", borderCollapse: "collapse", minWidth: 780 }}>
            <thead><tr><th style={thL2}>date</th><th style={thL2}>time</th><th style={thL2}>source</th><th style={thL2}>ticker</th><th style={thL2}>contract</th>
              <th style={th2}>peak</th><th style={th2}>drawdown</th><th style={th2}>tape</th><th style={th2}>ticks</th><th style={{ ...thL2, paddingLeft: 18 }}>data</th></tr></thead>
            <tbody>{shown.map((r2, i) => (
              <tr key={i}>
                <td style={{ ...tdL2, color: "var(--text-secondary)" }}>{dOf(r2.ts)}</td>
                <td style={{ ...tdL2, color: "var(--text-secondary)" }}>{tmOf(r2.ts)}</td>
                <td style={{ ...tdL2, color: "var(--text-secondary)" }}>{r2.source}</td>
                <td style={{ ...tdL2, color: "var(--text-primary)" }}>{r2.ticker}</td>
                <td style={{ ...tdL2, color: "var(--text-secondary)" }}>{r2.contract || "—"}</td>
                <td style={{ ...td2, color: r2.peak == null ? "var(--text-tertiary)" : "var(--profit)" }}>{r2.peak == null ? "—" : pct2(r2.peak)}</td>
                <td style={{ ...td2, color: r2.dd == null ? "var(--text-tertiary)" : "var(--loss)" }}>{r2.dd == null ? "—" : pct2(r2.dd)}</td>
                <td style={{ ...td2, color: "var(--text-secondary)" }}>{r2.mins == null ? "—" : r2.mins + "m"}</td>
                <td style={{ ...td2, color: r2.taped ? "var(--text-secondary)" : "var(--text-tertiary)" }}>{r2.taped ? r2.ticks.toLocaleString("en-US") : "—"}</td>
                <td style={{ ...tdL2, paddingLeft: 18 }}><span style={{ font: "var(--w-semibold) var(--t-2xs)/1 var(--font-sans)", letterSpacing: "var(--ls-wide)", color: r2.taped ? "var(--profit)" : "var(--loss)" }}>{r2.taped ? "TAPED" : "NO TAPE"}</span></td>
              </tr>))}
            </tbody>
          </table>
        </div>
      </NT.Card>
    );
  };

  const simsTable = () => (
    <NT.Card padding={0}>
      <div style={{ padding: "13px 16px 9px", font: "var(--w-semibold) var(--t-sm)/1 var(--font-sans)", color: "var(--text-primary)" }}>Sizing simulations — same trades, size adapted</div>
      <div style={{ overflowX: "auto" }}>
        <table style={{ width: "100%", borderCollapse: "collapse" }}>
          <thead><tr><th style={{ ...lbl, textAlign: "left", padding: "8px 16px", borderBottom: "1px solid var(--border)" }}>rule</th>
            <th style={{ ...lbl, textAlign: "right", padding: "8px 16px", borderBottom: "1px solid var(--border)" }}>would have made</th></tr></thead>
          <tbody>{(seg.sims || []).map((s2, i) => (
            <tr key={i}>
              <td style={{ padding: "9px 16px", borderTop: "1px solid var(--border)", font: "var(--w-regular) var(--t-xs)/1.4 var(--font-sans)", color: i === 0 ? "var(--text-primary)" : "var(--text-secondary)" }}>{s2.name}</td>
              <td style={{ padding: "9px 16px", borderTop: "1px solid var(--border)", textAlign: "right" }}>{money(s2.pnl)}</td>
            </tr>))}
          </tbody>
        </table>
      </div>
      {seg.streak_note ? <div style={{ padding: "10px 16px 14px", font: "var(--w-regular) var(--t-2xs)/1.5 var(--font-sans)", color: "var(--text-tertiary)" }}>{seg.streak_note}</div> : null}
    </NT.Card>
  );

  const histTable = () => (
    <NT.Card padding={0}>
      <div style={{ padding: "13px 16px 9px", font: "var(--w-semibold) var(--t-sm)/1 var(--font-sans)", color: "var(--text-primary)" }}>Run history</div>
      <div style={{ overflowX: "auto" }}>
        <table style={{ width: "100%", borderCollapse: "collapse" }}>
          <thead><tr>{["run", "trades", "robust", "regime", "market"].map((h) => (
            <th key={h} style={{ ...lbl, textAlign: "left", padding: "8px 16px", borderBottom: "1px solid var(--border)" }}>{h}</th>))}</tr></thead>
          <tbody>{hist.filter((r2) => r2.roles).map((r2, i) => {
            const st = (role) => {
              const x = (r2.roles || {})[role];
              return !x ? "—" : (x.matches_best ? "on the best" : "differs");
            };
            return (
              <tr key={i}>
                <td style={{ padding: "9px 16px", borderTop: "1px solid var(--border)", ...mono, color: "var(--text-secondary)", whiteSpace: "nowrap" }}>
                  {new Date(r2.ran_at).toLocaleString("nl-NL", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" })}</td>
                <td style={{ padding: "9px 16px", borderTop: "1px solid var(--border)", ...mono, color: "var(--text-tertiary)" }}>{r2.pool_n}</td>
                <td style={{ padding: "9px 16px", borderTop: "1px solid var(--border)", font: "var(--w-regular) var(--t-xs)/1 var(--font-sans)", color: st("robust") === "on the best" ? "var(--profit)" : "var(--text-secondary)" }}>{st("robust")}</td>
                <td style={{ padding: "9px 16px", borderTop: "1px solid var(--border)", font: "var(--w-regular) var(--t-xs)/1 var(--font-sans)", color: st("regime") === "on the best" ? "var(--profit)" : "var(--text-secondary)" }}>{st("regime")}</td>
                <td style={{ padding: "9px 16px", borderTop: "1px solid var(--border)", font: "var(--w-regular) var(--t-xs)/1 var(--font-sans)", color: r2.flip ? "#d9a53f" : "var(--text-tertiary)" }}>{r2.flip ? "signals turned" : "unchanged"}</td>
              </tr>);
          })}
          </tbody>
        </table>
      </div>
    </NT.Card>
  );

  const TABS = [["coverage", "Data coverage"], ["entry", "Entry stats"], ["month", "By month"], ["sizing", "Sizing simulations"], ["history", "Run history"]];

  /* Risk & shape — the charts (18 Sep). One role at a time: a curve belongs to ONE set
     of settings, so the role and own/best switches sit above it. */
  const riskPanel = () => {
    const roles = run.roles || {};
    const keys = Object.keys(roles);
    if (!keys.length) return <div style={{ color: "var(--text-tertiary)", font: "var(--w-regular) var(--t-sm)/1 var(--font-sans)", padding: 20 }}>No analysis yet.</div>;
    const rk = roles[riskRole] ? riskRole : keys[0];
    const rr = roles[rk];
    const src = riskWhich === "best" ? rr.best : rr.own;
    const acct = rr.acct_value || (run.risk || {}).account;
    const nm = (rr.name || rk);
    const pair = (a, b) => (
      <div style={{ display: "flex", justifyContent: "space-between", gap: 10, padding: "7px 0", borderTop: "1px solid var(--border)" }}>
        <span style={{ font: "var(--w-regular) var(--t-sm)/1.3 var(--font-sans)", color: "var(--text-tertiary)" }}>{a.k}</span>
        {/* t-sm, niet t-2xs: waarden die als kale tekst binnenkomen (streaks, drawdown)
            renderden kleiner dan de rijen die via money() lopen — money() zet zelf t-sm
            (Gianni, 21 sep). */}
        <span style={{ display: "flex", gap: 14 }}>
          <span style={{ font: "var(--w-regular) var(--t-sm)/1.3 var(--font-mono)", color: "var(--profit)", fontVariantNumeric: "tabular-nums" }}>{a.v}</span>
          <span style={{ font: "var(--w-regular) var(--t-sm)/1.3 var(--font-mono)", color: "var(--loss)", fontVariantNumeric: "tabular-nums", minWidth: 74, textAlign: "right" }}>{b.v}</span>
        </span>
      </div>
    );
    return (
      <div style={{ display: "flex", flexDirection: "column", gap: "var(--gap-grid)" }}>
        <div style={{ display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }}>
          {keys.map((k) => (
            <button key={k} onClick={() => setRiskRole(k)} style={{ padding: "7px 12px", borderRadius: "var(--radius-sm)", border: "none", cursor: "pointer",
              background: rk === k ? "var(--surface-inset)" : "transparent", color: rk === k ? "var(--text-primary)" : "var(--text-tertiary)",
              font: "var(--w-medium) var(--t-2xs)/1 var(--font-sans)" }}>{roles[k].name || k}</button>))}
          <span style={{ width: 1, height: 18, background: "var(--border)", margin: "0 4px" }} />
          {[["own", "this card"], ["best", "best combination"]].map(([k, l]) => (
            <button key={k} onClick={() => setRiskWhich(k)} style={{ padding: "7px 12px", borderRadius: "var(--radius-sm)", border: "none", cursor: "pointer",
              background: riskWhich === k ? "var(--surface-inset)" : "transparent", color: riskWhich === k ? "var(--text-primary)" : "var(--text-tertiary)",
              font: "var(--w-medium) var(--t-2xs)/1 var(--font-sans)" }}>{l}</button>))}
        </div>

        {/* Verse cijfers of niet? Na een toegepast voorstel of een eigen wijziging
            herrekent de heartbeat binnen een paar minuten alleen DEZE kaart; tot dan
            staan hier nog de getallen van de nachtelijke analyse (Gianni, 21 sep). */}
        {(function () {
          const live = SL_rowCfg(liveRows[String(rr.strategy_id)]);
          const stale = riskWhich === "own" && live && rr.own && !SL_sameCfg(live, rr.own);
          const fresh = riskWhich === "own" && rr.own && rr.own.rescored_at;
          if (!stale && !fresh) return null;
          return (
            <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "9px 12px", borderRadius: "var(--radius-sm)",
              background: stale ? "color-mix(in srgb, #d9a53f 10%, transparent)" : "var(--surface-inset)",
              font: "var(--w-regular) var(--t-2xs)/1.5 var(--font-sans)", color: "var(--text-secondary)" }}>
              <span style={{ color: stale ? "#d9a53f" : "var(--profit)", flex: "none" }}>{stale ? "\u25cf" : "\u2713"}</span>
              {stale
                ? "You just changed these settings — the numbers below are still from the last analysis. They refresh within a few minutes."
                : "Recalculated after your change, not from last night's analysis."}
            </div>);
        })()}

        {/* WHICH settings the block below is about — above the card, because the chips
            belong to the CHOICE, not to the numbers (Gianni, 18 Sep). */}
        <div style={{ marginTop: -4 }}>
          {SL_chips(src).map((c, i) => (
            <span key={i} style={{ display: "inline-block", padding: "6px 11px", borderRadius: 999,
              background: "var(--surface-inset)", font: "var(--w-regular) var(--t-2xs)/1 var(--font-sans)",
              color: "var(--text-secondary)", margin: "0 6px 6px 0", whiteSpace: "nowrap" }}>{c}</span>))}
        </div>

        {/* Op een breed scherm staan het cijferblok en de charts als gelijke kaarten
            naast elkaar — een blok van volle breedte met de charts eronder was veel
            te wijd (Gianni, 21 sep). Op mobiel gewoon gestapeld. */}
        <div style={{ display: "grid", gridTemplateColumns: isMobile ? "1fr" : "repeat(3, minmax(0, 1fr))", gap: "var(--gap-grid)", alignItems: "stretch" }}>

        {/* beide kanten, altijd naast elkaar (staande regel sinds 18 Sep) */}
        <div style={{ background: "var(--surface)", border: "1px solid var(--border)", borderRadius: "var(--radius-lg)", padding: "14px 18px" }}>
          <div style={{ display: "flex", justifyContent: "space-between", gap: 10, paddingBottom: 8 }}>
            <span style={{ font: "var(--w-semibold) var(--t-sm)/1.2 var(--font-sans)", color: "var(--text-primary)" }}>
              {nm} · {riskWhich === "best" ? "best combination" : "these settings"}
            </span>
            <span style={{ display: "flex", gap: 14 }}>
              <span style={{ font: "var(--w-medium) var(--t-sm)/1 var(--font-sans)", color: "var(--profit)" }}>WIN</span>
              <span style={{ font: "var(--w-medium) var(--t-sm)/1 var(--font-sans)", color: "var(--loss)", minWidth: 74, textAlign: "right" }}>LOSS</span>
            </span>
          </div>
          {pair({ k: "average per trade", v: money(src.avg_win) }, { v: money(src.avg_loss) })}
          {pair({ k: "biggest", v: money(src.best) }, { v: money(src.worst) })}
          {pair({ k: "best run / worst drawdown", v: money(src.best_run) }, { v: money(src.mdd) })}
          {/* Het percentage hoort bij het LABEL, niet achter het bedrag — een suffix
              duwde −$902 uit de LOSS-kolom en brak de uitlijning (Gianni, 21 sep). */}
          {src.bootstrap ? pair(
            { k: "realistic worst drawdown" + (acct ? " · " + Math.round(Math.abs(src.bootstrap.p95) / acct * 100) + "% of account" : ""), v: "" },
            { v: SL_money(src.bootstrap.p95) }) : null}
          {pair({ k: "longest streak", v: (src.win_streak || 0) + " trades" }, { v: (src.loss_streak || 0) + " trades" })}
          <div style={{ display: "flex", justifyContent: "space-between", gap: 10, padding: "9px 0 0", borderTop: "1px solid var(--border)", marginTop: 2 }}>
            <span style={{ font: "var(--w-regular) var(--t-sm)/1.3 var(--font-sans)", color: "var(--text-tertiary)" }}>total · win rate · return per unit of pain</span>
            <span style={{ font: "var(--w-regular) var(--t-sm)/1.3 var(--font-mono)", color: "var(--text-primary)", fontVariantNumeric: "tabular-nums" }}>
              {money(src.pnl)} · {src.win}% · {src.ratio ? src.ratio + "×" : "—"}
            </span>
          </div>
        </div>

          {window.LabEquityChart ? <LabEquityChart curve={src.curve} worstLabel={SL_money(src.mdd)} height={240} /> : null}
          {window.LabTradesChart ? <LabTradesChart curve={src.curve} height={240} /> : null}
          {window.LabLandscapeChart && run.landscape ? <LabLandscapeChart landscape={run.landscape} acct={acct} height={240} /> : null}
        </div>
        {top8Table()}
      </div>
    );
  };

  /* De ranglijst: niet één winnaar maar het plateau eromheen */
  const top8Table = () => {
    const rows = run.top8 || [];
    if (!rows.length) return null;
    const th = { padding: "8px 10px", textAlign: "right", font: "var(--w-medium) var(--t-2xs)/1 var(--font-sans)", color: "var(--text-tertiary)", whiteSpace: "nowrap" };
    const td = { padding: "8px 10px", textAlign: "right", font: "var(--w-regular) var(--t-2xs)/1 var(--font-mono)", color: "var(--text-primary)", fontVariantNumeric: "tabular-nums", borderTop: "1px solid var(--border)", whiteSpace: "nowrap" };
    return (
      <div style={{ background: "var(--surface)", border: "1px solid var(--border)", borderRadius: "var(--radius-lg)", padding: "16px 18px" }}>
        <div style={{ font: "var(--w-semibold) var(--t-sm)/1.2 var(--font-sans)", color: "var(--text-primary)" }}>The eight best-shaped combinations</div>
        <div style={{ font: "var(--w-regular) var(--t-2xs)/1.45 var(--font-sans)", color: "var(--text-tertiary)", marginTop: 4, marginBottom: 10 }}>
          Ranked by return per unit of pain, within your limits. If they sit close together, the result does not hang on one lucky setting.
        </div>
        <div style={{ overflowX: "auto" }}>
          <table style={{ width: "100%", borderCollapse: "collapse", minWidth: 640 }}>
            <thead><tr>
              <th style={{ ...th, textAlign: "left" }}>setting</th>
              <th style={th}>total</th><th style={th}>win</th>
              <th style={th}>avg win</th><th style={th}>avg loss</th>
              <th style={th}>best run</th><th style={th}>worst drawdown</th><th style={th}>per pain</th>
            </tr></thead>
            <tbody>
              {rows.map((r, i) => (
                <tr key={i}>
                  <td style={{ ...td, textAlign: "left", font: "var(--w-regular) var(--t-2xs)/1.3 var(--font-sans)" }}>{r.label}</td>
                  <td style={{ ...td, color: "var(--profit)" }}>{money(r.pnl)}</td>
                  <td style={td}>{r.win}%</td>
                  <td style={{ ...td, color: "var(--profit)" }}>{money(r.avg_win)}</td>
                  <td style={{ ...td, color: "var(--loss)" }}>{money(r.avg_loss)}</td>
                  <td style={{ ...td, color: "var(--profit)" }}>{money(r.best_run)}</td>
                  <td style={{ ...td, color: "var(--loss)" }}>{money(r.mdd)}</td>
                  <td style={{ ...td, font: "var(--w-medium) var(--t-2xs)/1 var(--font-mono)" }}>{r.ratio}×</td>
                </tr>))}
            </tbody>
          </table>
        </div>
      </div>
    );
  };
  const WD = ["Mon", "Tue", "Wed", "Thu", "Fri"];

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
      {head}

      <div style={{ height: 22 }} />
      <div style={{ display: "grid", gridTemplateColumns: isMobile ? "1fr" : "1fr 1fr", gap: "var(--gap-grid)" }}>
        <div style={{ minWidth: 0 }}>{roleCard("robust")}</div>
        <div style={{ minWidth: 0 }}>{roleCard("regime")}</div>
      </div>

      <div style={{ font: "var(--w-semibold) 17px/1.2 var(--font-sans)", color: "var(--text-primary)", margin: "30px 0 14px" }}>
        Regime change checker<SL_Help text={SL_HELP.checker} />
      </div>
      <div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
        {signals.map((s2) => (
          <div key={s2.key} style={{ flex: 1, minWidth: 130, textAlign: "center", padding: "16px 10px", borderRadius: "var(--radius-md)", background: "var(--surface-inset)" }}>
            <div style={lbl}><span style={{ display: "inline-block", width: 7, height: 7, borderRadius: 99, marginRight: 6, verticalAlign: 1, background: s2.turned ? "#d9a53f" : "var(--profit)" }} />{s2.label}</div>
            <div style={{ font: "var(--w-light) 20px/1 var(--font-mono)", fontVariantNumeric: "tabular-nums", marginTop: 10, color: "var(--text-primary)" }}>{s2.value}</div>
            <div style={{ font: "var(--w-regular) var(--t-2xs)/1.3 var(--font-sans)", color: "var(--text-tertiary)", marginTop: 6 }}>{s2.caption}</div>
          </div>))}
      </div>
      {banner}

      <div style={{ font: "var(--w-semibold) 17px/1.2 var(--font-sans)", color: "var(--text-primary)", margin: "34px 0 14px" }}>
        Risk &amp; shape<SL_Help text={SL_HELP.risk} />
        <button onClick={() => setRiskOpen((v) => !v)} style={{ ...linkBtn, marginLeft: 10 }}>{riskOpen ? "hide \u25b4" : "show \u25be"}</button>
      </div>
      {riskOpen ? riskPanel() : null}

      <div style={{ font: "var(--w-semibold) 17px/1.2 var(--font-sans)", color: "var(--text-primary)", margin: "34px 0 14px" }}>
        Stats<SL_Help text={SL_HELP.stats} />
        <button onClick={() => setStatsOpen((v) => !v)} style={{ ...linkBtn, marginLeft: 10 }}>{statsOpen ? "hide ▴" : "show ▾"}</button>
      </div>
      {statsOpen ? (
        <div style={{ display: "flex", flexDirection: "column", gap: "var(--gap-grid)" }}>
          <div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
            {TABS.map(([k, lab2]) => (
              <button key={k} onClick={() => setStatsTab(k)}
                style={{ padding: "9px 14px", borderRadius: "var(--radius-sm)", border: "none", cursor: "pointer",
                         background: statsTab === k ? "var(--surface-inset)" : "transparent",
                         color: statsTab === k ? "var(--text-primary)" : "var(--text-tertiary)",
                         font: "var(--w-medium) var(--t-xs)/1 var(--font-sans)" }}>{lab2}</button>))}
          </div>
          {statsTab === "coverage" ? covTable() : null}
          {statsTab === "entry" ? (
            <div style={{ display: "grid", gridTemplateColumns: isMobile ? "1fr" : "1fr 1fr", gap: "var(--gap-grid)" }}>
              <div style={{ minWidth: 0 }}>{avgTable("Per weekday — average P&L per trade", "weekday", WD)}</div>
              <div style={{ minWidth: 0 }}>{avgTable("By entry hour (ET)", "hour")}</div>
              <div style={{ minWidth: 0 }}>{avgTable("Trade # of the day", "nth", ["1st of the day", "2nd", "3rd or later"])}</div>
              <div style={{ minWidth: 0 }}>{avgTable("News days", "news_day")}</div>
              <div style={{ minWidth: 0 }}>{avgTable("Alert wording", "tone")}</div>
            </div>) : null}
          {statsTab === "month" ? <div style={{ maxWidth: 560 }}>{avgTable("By month — average P&L per trade", "month")}</div> : null}
          {statsTab === "sizing" ? <div style={{ maxWidth: 640 }}>{simsTable()}</div> : null}
          {statsTab === "history" ? histTable() : null}
        </div>
      ) : null}
    </div>
  );
}

Object.assign(window, { StrategyLabPage });
