/* ============================================================================
   MyTraderOS — /app SPA shell (Wave 0 · L0 MVP)
   React inline + Babel standalone (no build step) · vendor local (ห้าม CDN)
   โครง: Router (History API) · SessionGuard (mock) · AppShell (TabBar+sidebar+header)
        · states กลาง reusable · i18n loader · เชื่อม mock API layer
   เนื้อหน้าจริงมาใน wave ถัดไป — ทุก route = placeholder ที่ใช้ states กลางได้จริง
   ============================================================================ */
const { useState, useEffect, useRef, useCallback } = React;

/* ---------------------------------------------------------------- i18n ---- */
function flatten(obj, prefix, out) {
  out = out || {}; prefix = prefix || "";
  for (const k in obj) {
    const v = obj[k], key = prefix ? prefix + "." + k : k;
    if (v && typeof v === "object" && !Array.isArray(v)) flatten(v, key, out);
    else out[key] = v;
  }
  return out;
}
function getLang() {
  try { return localStorage.getItem("mtos_lang") || "th"; } catch (e) { return "th"; }
}
async function loadCatalog(lang) {
  const res = await fetch("i18n/" + lang + ".json", { cache: "no-store" });
  if (!res.ok) throw new Error("catalog " + lang + " " + res.status);
  return flatten(await res.json());
}

/* --------------------------------------------------------- fmt helpers ---- */
/* แสดงผลเท่านั้น — ค่ามาจาก API (client ห้ามคำนวณเลขเทรด) */
function fmtMoney(cents, currency) {
  if (cents === null || cents === undefined) return "—";
  const sym = currency === "THB" ? "฿" : "$";
  const v = cents / 100;
  const sign = v > 0 ? "+" : v < 0 ? "−" : "";
  const abs = Math.abs(v).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
  return sign + sym + abs;
}
function fmtPct(x) { return x === null || x === undefined ? "—" : (x * 100).toFixed(1) + "%"; }
function fmtNum(x, d) { return x === null || x === undefined ? "—" : Number(x).toFixed(d === undefined ? 2 : d); }
function fmtTime(iso) {
  if (!iso) return "—";
  try {
    return new Date(iso).toLocaleString("th-TH", { timeZone: "Asia/Bangkok", day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
  } catch (e) { return iso; }
}

/* ------------------------------------------------------------- router ---- */
function navigate(to, replace) {
  if (replace) history.replaceState({}, "", to); else history.pushState({}, "", to);
  window.dispatchEvent(new Event("mtos:nav"));
}
function useRoute() {
  const [loc, setLoc] = useState(window.location.pathname + window.location.search);
  useEffect(() => {
    const on = () => setLoc(window.location.pathname + window.location.search);
    window.addEventListener("popstate", on);
    window.addEventListener("mtos:nav", on);
    return () => { window.removeEventListener("popstate", on); window.removeEventListener("mtos:nav", on); };
  }, []);
  return loc;
}

/* breakpoint hook (desktop ≥1024px) — ใช้เลือก behavior calendar (day card vs sheet) */
function useIsDesktop() {
  const mq = () => (typeof window.matchMedia === "function" ? window.matchMedia("(min-width:1024px)").matches : window.innerWidth >= 1024);
  const [d, setD] = useState(mq());
  useEffect(() => {
    const on = () => setD(mq());
    let m;
    if (typeof window.matchMedia === "function") { m = window.matchMedia("(min-width:1024px)"); (m.addEventListener ? m.addEventListener("change", on) : m.addListener(on)); }
    window.addEventListener("resize", on);
    return () => { if (m) { m.removeEventListener ? m.removeEventListener("change", on) : m.removeListener(on); } window.removeEventListener("resize", on); };
  }, []);
  return d;
}

/* ------------------------------------------------- query + date helpers -- */
function getQuery() { return new URLSearchParams(window.location.search); }
function patchQuery(patch, replace) {
  const sp = new URLSearchParams(window.location.search);
  for (const k in patch) {
    const v = patch[k];
    if (v === null || v === undefined || v === "") sp.delete(k); else sp.set(k, v);
  }
  const qs = sp.toString();
  navigate(window.location.pathname + (qs ? "?" + qs : ""), replace);
}
function pad2(n) { return String(n).padStart(2, "0"); }
function ymd(d) { return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()); }
function ymNow() { const n = new Date(); return n.getFullYear() + "-" + pad2(n.getMonth() + 1); }
function parseYMD(s) {
  if (!/^\d{4}-\d{2}-\d{2}$/.test(s || "")) return null;
  const p = s.split("-").map(Number);
  const d = new Date(p[0], p[1] - 1, p[2]);
  if (d.getFullYear() !== p[0] || d.getMonth() !== p[1] - 1 || d.getDate() !== p[2]) return null; // 02-30 ฯลฯ
  return d;
}
function parseYM(s) {
  if (!/^\d{4}-\d{2}$/.test(s || "")) return null;
  const p = s.split("-").map(Number);
  if (p[1] < 1 || p[1] > 12) return null;
  return { y: p[0], m: p[1] };
}
/* เงินย่อ (heatmap/calendar cell) — แสดงผลเท่านั้น ค่าจาก API */
function fmtMoneyShort(cents, currency) {
  if (cents === null || cents === undefined) return "—";
  const sym = currency === "THB" ? "฿" : "$";
  const v = cents / 100, a = Math.abs(v);
  const sign = v > 0 ? "+" : v < 0 ? "−" : "";
  const s = a >= 1000 ? (a / 1000).toFixed(a >= 10000 ? 0 : 1) + "k" : a.toFixed(0);
  return sign + sym + s;
}

const ROUTES = [
  { re: /^\/$/, name: "landing", pub: true },
  { re: /^\/login$/, name: "login", pub: true },
  { re: /^\/register$/, name: "register", pub: true },
  { re: /^\/app\/onboarding$/, name: "onboarding" },
  { re: /^\/app$/, name: "dashboard" },
  { re: /^\/app\/calendar$/, name: "calendar" },
  { re: /^\/app\/trades$/, name: "trades" },
  { re: /^\/app\/trades\/([^/]+)$/, name: "trade_detail", keys: ["id"] },
  { re: /^\/app\/insights$/, name: "insights" },
  { re: /^\/app\/import$/, name: "import" },
  { re: /^\/app\/connect$/, name: "connect" },
  { re: /^\/app\/unlock$/, name: "unlock" },
  { re: /^\/app\/settings$/, name: "settings" },
];
function matchRoute(pathname) {
  for (const r of ROUTES) {
    const m = pathname.match(r.re);
    if (m) {
      const params = {};
      (r.keys || []).forEach((k, i) => { params[k] = m[i + 1]; });
      return { name: r.name, pub: !!r.pub, params: params };
    }
  }
  return { name: "not_found", pub: false, params: {} };
}

/* -------------------------------------------------------- mock session --- */
const Session = {
  active() { try { return localStorage.getItem("mtos_session") === "1"; } catch (e) { return false; } },
  login() { try { localStorage.setItem("mtos_session", "1"); localStorage.setItem("mtos_token", "mock.jwt.token"); } catch (e) {} },
  startDemo() { try { localStorage.setItem("mtos_session", "1"); localStorage.setItem("mtos_demo", "1"); localStorage.setItem("mtos_token", "mock.jwt.token"); } catch (e) {} },
  isDemo() { try { return localStorage.getItem("mtos_demo") === "1"; } catch (e) { return false; } },
  logout() { try { localStorage.removeItem("mtos_session"); localStorage.removeItem("mtos_token"); localStorage.removeItem("mtos_demo"); localStorage.removeItem("mtos_user"); } catch (e) {} },
};
/* dev-only UI toggle (state switcher ฯลฯ) — เปิดด้วย localStorage mtos_dev=1 */
function isDevMode() { try { return localStorage.getItem("mtos_dev") === "1"; } catch (e) { return false; } }

/* ===========================================================================
   Central state components (reuse ทุกหน้า — ห้ามหน้าไหนประดิษฐ์เอง)
   =========================================================================== */
function Skeleton({ variant }) {
  const rows = variant === "tiles" ? 4 : variant === "list" ? 5 : 3;
  const box = { background: "linear-gradient(90deg,var(--surface-2),var(--surface-3),var(--surface-2))", backgroundSize: "200% 100%", animation: "mtos-shimmer 1.2s linear infinite", borderRadius: "var(--radius)" };
  if (variant === "tiles") {
    return <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(150px,1fr))", gap: "var(--sp-3)" }}>
      {Array.from({ length: 4 }).map((_, i) => <div key={i} style={Object.assign({ height: 92 }, box)} />)}
    </div>;
  }
  return <div style={{ display: "flex", flexDirection: "column", gap: "var(--sp-3)" }}>
    {Array.from({ length: rows }).map((_, i) => <div key={i} style={Object.assign({ height: 56 }, box)} />)}
  </div>;
}

function EmptyState({ t, title, body, cta, onCta }) {
  return <div style={{ textAlign: "center", padding: "var(--sp-8) var(--sp-4)", color: "var(--ink-dim)" }}>
    <div style={{ fontSize: 34, opacity: .5, marginBottom: "var(--sp-3)" }}>◍</div>
    <div className="disp" style={{ fontSize: "var(--fs-h3)", color: "var(--ink)", marginBottom: "var(--sp-2)" }}>{title || t("states.empty_title")}</div>
    <div style={{ fontSize: "var(--fs-body)", maxWidth: 340, margin: "0 auto var(--sp-5)" }}>{body || t("states.empty_body")}</div>
    {onCta && <button className="btn-brand" onClick={onCta}>{cta || t("states.empty_cta")}</button>}
  </div>;
}

function ErrorState({ t, code, onRetry }) {
  return <div style={{ textAlign: "center", padding: "var(--sp-8) var(--sp-4)", color: "var(--ink-dim)" }}>
    <div style={{ fontSize: 30, color: "var(--loss)", marginBottom: "var(--sp-3)" }}>⚠</div>
    <div className="disp" style={{ fontSize: "var(--fs-h3)", color: "var(--ink)", marginBottom: "var(--sp-2)" }}>{t("states.error_title")}</div>
    <div style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)", marginBottom: "var(--sp-5)" }}>{t("states.error_repeat", { code: code || "—" })}</div>
    <button className="btn-secondary" onClick={onRetry}>{t("common.retry")}</button>
  </div>;
}

function FirstSyncProgress({ t, minutes, progress }) {
  const pct = Math.round((progress || 0) * 100);
  return <div style={{ padding: "var(--sp-5)", background: "var(--surface-1)", border: "1px solid var(--hairline)", borderRadius: "var(--radius)" }}>
    <div className="disp" style={{ fontSize: "var(--fs-h3)", marginBottom: "var(--sp-2)" }}>{t("states.first_sync_title")}</div>
    <div style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)", marginBottom: "var(--sp-3)" }}>{t("states.first_sync_eta", { minutes: minutes || 5 })}</div>
    <div style={{ height: 8, borderRadius: 999, background: "var(--surface-3)", overflow: "hidden" }}>
      <div style={{ height: "100%", width: pct + "%", background: "var(--brand-grad)", transition: "width var(--dur-slow) var(--ease)" }} />
    </div>
  </div>;
}

function LockedSampleCard({ t, children }) {
  return <div style={{ position: "relative", border: "1px solid var(--brand-edge)", borderRadius: "var(--radius)", padding: "var(--sp-4)", background: "var(--surface-1)" }}>
    <span style={{ position: "absolute", top: 10, right: 10, fontSize: "var(--fs-micro)", padding: "2px 8px", borderRadius: "var(--radius-sm)", background: "var(--zebra)", color: "var(--ink-mute)" }}>{t("common.sample")}</span>
    <div style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)", marginBottom: "var(--sp-3)" }}>{t("states.locked_sample_hint")}</div>
    {children}
  </div>;
}

function Toast({ msg, onDone }) {
  useEffect(() => { if (!msg) return; const id = setTimeout(onDone, 3000); return () => clearTimeout(id); }, [msg]);
  if (!msg) return null;
  return <div role="status" aria-live="polite" style={{ position: "fixed", left: "50%", transform: "translateX(-50%)", bottom: "calc(var(--tap-min) + var(--sp-5))", zIndex: "var(--z-toast)", background: "var(--surface-3)", color: "var(--ink)", padding: "var(--sp-3) var(--sp-5)", borderRadius: "var(--radius-full)", boxShadow: "var(--shadow-2)", fontSize: "var(--fs-caption)", maxWidth: "90vw" }}>
    <span style={{ color: "var(--profit)", marginRight: 8 }}>✓</span>{msg}
  </div>;
}

function BottomSheet({ open, title, onClose, children }) {
  useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open]);
  if (!open) return null;
  return <div style={{ position: "fixed", inset: 0, zIndex: "var(--z-sheet)" }}>
    <div onClick={onClose} style={{ position: "absolute", inset: 0, background: "var(--scrim)", animation: "mtos-fade var(--dur-base) var(--ease)" }} />
    <div role="dialog" aria-modal="true" aria-label={title} style={{ position: "absolute", left: 0, right: 0, bottom: 0, maxHeight: "90dvh", overflowY: "auto", background: "var(--surface-2)", borderTopLeftRadius: "var(--radius-lg)", borderTopRightRadius: "var(--radius-lg)", boxShadow: "var(--shadow-sheet)", padding: "var(--sp-4)", paddingBottom: "calc(var(--sp-6) + env(safe-area-inset-bottom))", animation: "mtos-slideup var(--dur-slow) var(--ease)" }}>
      <div style={{ width: 32, height: 4, borderRadius: 999, background: "var(--ink-mute)", margin: "0 auto var(--sp-4)" }} />
      {title && <div className="disp" style={{ fontSize: "var(--fs-h2)", marginBottom: "var(--sp-3)" }}>{title}</div>}
      {children}
    </div>
  </div>;
}

function FAB({ onClick, label }) {
  return <button aria-label={label} onClick={onClick} style={{ width: 56, height: 56, borderRadius: "var(--radius-full)", border: "none", background: "var(--brand-grad)", color: "#fff", fontSize: 26, lineHeight: "1", cursor: "pointer", boxShadow: "var(--shadow-brand)", display: "flex", alignItems: "center", justifyContent: "center" }}>+</button>;
}

/* --------------------------------------------------- small UI helpers ---- */
function MoneyText({ cents, currency }) {
  const v = cents === null || cents === undefined ? 0 : cents;
  const color = v > 0 ? "var(--profit)" : v < 0 ? "var(--loss)" : "var(--ink)";
  return <span className="num" style={{ color: color }}>{fmtMoney(cents, currency)}</span>;
}
function SyncWatermark({ t, meta }) {
  if (!meta || !meta.synced_through) return null;
  return <div style={{ fontSize: "var(--fs-micro)", color: "var(--ink-mute)", marginTop: "var(--sp-3)" }}>{t("states.watermark", { time: fmtTime(meta.synced_through) })}</div>;
}
/* ธง "ตัวอย่าง" (locked/sample state) — มุมขวาบนการ์ด */
function SampleBadge({ t }) {
  return <span style={{ position: "absolute", top: 8, right: 8, fontSize: "var(--fs-micro)", padding: "2px 8px", borderRadius: "var(--radius-sm)", background: "var(--brand-soft)", color: "var(--brand-bright)", fontWeight: 700, zIndex: 1 }}>{t("common.sample")}</span>;
}

/* dropdown เล็ก reuse ทั้ง account + range chip */
function Dropdown({ label, dot, open, onToggle, children }) {
  return <div style={{ position: "relative" }}>
    <button className="btn-secondary" onClick={onToggle} style={{ height: 36, minHeight: 36, padding: "0 var(--sp-3)", borderRadius: "var(--radius-full)", fontSize: "var(--fs-caption)", display: "inline-flex", alignItems: "center", gap: 6, whiteSpace: "nowrap" }}>
      {dot && <span style={{ width: 7, height: 7, borderRadius: "50%", background: "var(--brand-bright)" }} />}{label}<span style={{ opacity: .6 }}>▾</span>
    </button>
    {open && <React.Fragment>
      <div onClick={onToggle} style={{ position: "fixed", inset: 0, zIndex: "var(--z-dropdown)" }} />
      <div role="menu" style={{ position: "absolute", top: 42, right: 0, minWidth: 180, zIndex: "calc(var(--z-dropdown) + 1)", background: "var(--surface-3)", border: "1px solid var(--hairline)", borderRadius: "var(--radius)", boxShadow: "var(--shadow-2)", padding: 6, overflow: "hidden" }}>{children}</div>
    </React.Fragment>}
  </div>;
}
function MenuItem({ selected, onClick, children }) {
  return <button onClick={onClick} role="menuitem" style={{ display: "block", width: "100%", textAlign: "left", minHeight: 40, padding: "0 var(--sp-3)", border: "none", borderRadius: "var(--radius-sm)", cursor: "pointer", background: selected ? "var(--surface-2)" : "transparent", color: selected ? "var(--brand-bright)" : "var(--ink-dim)", fontWeight: selected ? 700 : 500, fontSize: "var(--fs-caption)" }}>{children}</button>;
}

/* generic user glyph (avatar) — ไม่มีรูปจริง (demo/normal) วาดด้วย SVG บนพื้น gradient ม่วง */
function UserGlyph({ size }) {
  const s = size || 20;
  return <svg width={s} height={s} viewBox="0 0 24 24" fill="#fff" aria-hidden="true" style={{ display: "block" }}>
    <circle cx="12" cy="8" r="3.6" />
    <path d="M4.5 19.5c0-3.6 3.4-5.6 7.5-5.6s7.5 2 7.5 5.6z" />
  </svg>;
}

/* AccountMenu — avatar ขวาบน header → dropdown: ตั้งค่า + ออกจากระบบ (แทน TH/EN + logout icon เดิม) */
function AccountMenu({ t, onLogout }) {
  const [open, setOpen] = useState(false);
  const isDemo = Session.isDemo();
  const goSettings = () => { setOpen(false); navigate("/app/settings"); };
  const doLogout = () => { setOpen(false); onLogout(); };
  const row = { display: "flex", alignItems: "center", gap: 10, width: "100%", textAlign: "left", minHeight: 44, padding: "0 10px", border: "none", borderRadius: "var(--radius-sm)", cursor: "pointer", background: "transparent", color: "var(--ink-dim)", fontWeight: 600, fontSize: "var(--fs-caption)" };
  return <div style={{ position: "relative", flexShrink: 0 }}>
    <button data-testid="avatar-btn" onClick={() => setOpen((o) => !o)} aria-haspopup="menu" aria-expanded={open} aria-label={t("menu.aria")} title={t("menu.aria")}
      style={{ width: 44, height: 44, minWidth: 44, padding: 0, border: "none", background: "transparent", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}>
      <span style={{ width: 38, height: 38, borderRadius: "50%", background: "var(--brand-grad)", display: "flex", alignItems: "center", justifyContent: "center", boxShadow: "0 0 0 1.5px var(--chrome-border)" }}><UserGlyph /></span>
    </button>
    {open && <React.Fragment>
      <div onClick={() => setOpen(false)} style={{ position: "fixed", inset: 0, zIndex: "var(--z-dropdown)" }} />
      <div role="menu" data-testid="avatar-menu" style={{ position: "absolute", top: 50, right: 0, minWidth: 214, zIndex: "calc(var(--z-dropdown) + 1)", background: "var(--surface-3)", border: "1px solid var(--hairline)", borderRadius: "var(--radius)", boxShadow: "var(--shadow-2)", padding: 6, overflow: "hidden" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 10px 10px", borderBottom: "1px solid var(--hairline)", marginBottom: 4 }}>
          <span style={{ width: 34, height: 34, borderRadius: "50%", background: "var(--brand-grad)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}><UserGlyph size={18} /></span>
          <div style={{ minWidth: 0 }}>
            <div style={{ fontWeight: 700, fontSize: "var(--fs-caption)", color: "var(--chrome-ink)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{isDemo ? t("menu.demo_user") : t("menu.user")}</div>
            {isDemo && <div style={{ fontSize: "var(--fs-micro)", color: "var(--ink-mute)" }}>{t("demo.badge_suffix")}</div>}
          </div>
        </div>
        <button data-testid="menu-settings" role="menuitem" onClick={goSettings} style={row}><span style={{ width: 18, textAlign: "center", fontSize: 15 }}>⚙</span>{t("app.nav.settings")}</button>
        <button data-testid="menu-logout" role="menuitem" onClick={doLogout} style={row}><span style={{ width: 18, textAlign: "center", fontSize: 15 }}>↩</span>{t("auth.logout")}</button>
      </div>
    </React.Fragment>}
  </div>;
}

/* AccountFilterChip — เปลี่ยน scope บัญชี -> เขียน URL ?account_id= -> หน้า re-fetch */
function AccountFilterChip({ t }) {
  const [accts, setAccts] = useState(null);
  const [open, setOpen] = useState(false);
  const sel = getQuery().get("account_id") || "";
  useEffect(() => { window.MTOS_API.get("/accounts").then((r) => setAccts(r.data)).catch(() => setAccts([])); }, []);
  const cur = (accts || []).find((a) => a.acc_id === sel);
  const label = sel && cur ? cur.nickname : t("filters.account_all");
  const pick = (id) => { setOpen(false); patchQuery({ account_id: id || null }); };
  return <Dropdown label={label} dot open={open} onToggle={() => setOpen((o) => !o)}>
    <MenuItem selected={!sel} onClick={() => pick("")}>{t("filters.account_all")}</MenuItem>
    {(accts || []).map((a) => <MenuItem key={a.acc_id} selected={sel === a.acc_id} onClick={() => pick(a.acc_id)}>{a.nickname} · {a.broker} ••{a.last4}</MenuItem>)}
  </Dropdown>;
}

/* DateRangeChip — เปลี่ยนช่วงวัน -> URL ?range= -> re-fetch (default 30d = ไม่มี param) */
function DateRangeChip({ t }) {
  const [open, setOpen] = useState(false);
  const sel = getQuery().get("range") || "30d";
  const opts = [["7d", t("filters.range_7d")], ["30d", t("filters.range_30d")], ["90d", t("filters.range_90d")], ["all", t("filters.range_all")]];
  const cur = opts.find((o) => o[0] === sel) || opts[1];
  const pick = (v) => { setOpen(false); patchQuery({ range: v === "30d" ? null : v }); };
  return <Dropdown label={cur[1]} open={open} onToggle={() => setOpen((o) => !o)}>
    {opts.map((o) => <MenuItem key={o[0]} selected={sel === o[0]} onClick={() => pick(o[0])}>{o[1]}</MenuItem>)}
  </Dropdown>;
}

/* filter row บนหน้า (มือถือ — desktop มี chip บน header แล้ว) */
function PageFilterBar({ t }) {
  return <div className="only-mobile" style={{ gap: 8, marginBottom: "var(--sp-4)", flexWrap: "wrap" }}>
    <DateRangeChip t={t} /><AccountFilterChip t={t} />
  </div>;
}

/* ===========================================================================
   Nav config + Shell
   =========================================================================== */
const NAV_GROUPS = (t) => [
  { label: t("app.group.record"), items: [
    { name: "dashboard", to: "/app", label: t("app.nav.dashboard"), icon: "▦" },
    { name: "calendar", to: "/app/calendar", label: t("app.nav.calendar"), icon: "◱" },
    { name: "trades", to: "/app/trades", label: t("app.nav.trades"), icon: "≣" },
    { name: "import", to: "/app/import", label: t("app.nav.import"), icon: "⇪" },
  ] },
  { label: t("app.group.analyze"), items: [
    { name: "insights", to: "/app/insights", label: t("app.nav.insights"), icon: "◎" },
  ] },
];
const TABS = (t) => [
  { name: "dashboard", to: "/app", label: t("app.nav.dashboard"), icon: "▦" },
  { name: "calendar", to: "/app/calendar", label: t("app.nav.calendar"), icon: "◱" },
  { name: "__fab" },
  { name: "trades", to: "/app/trades", label: t("app.nav.trades"), icon: "≣" },
  { name: "settings", to: "/app/settings", label: t("app.nav.settings"), icon: "⚙" },
];

function Logo() {
  return <div style={{ display: "flex", alignItems: "center", gap: 9 }}>
    <div className="disp" style={{ width: 30, height: 30, borderRadius: 8, background: "var(--brand-grad-icon)", display: "flex", alignItems: "center", justifyContent: "center", fontWeight: 800, color: "#fff", fontSize: 15 }}>M</div>
    <span className="disp" style={{ fontWeight: 800, fontSize: 19, letterSpacing: "-0.5px" }}>MyTrader<span style={{ background: "linear-gradient(90deg,#a78bfa,#e879c9)", WebkitBackgroundClip: "text", backgroundClip: "text", color: "transparent" }}>OS</span></span>
  </div>;
}

function LangToggle({ lang, onSet }) {
  const cell = (code) => ({ padding: "6px 12px", cursor: "pointer", fontWeight: 700, fontSize: 12, transition: "all var(--dur-fast)", color: lang === code ? "#fff" : "var(--ink-mute)", background: lang === code ? "var(--brand-grad)" : "transparent" });
  return <div style={{ display: "inline-flex", border: "1.5px solid var(--surface-3)", borderRadius: "var(--radius-full)", overflow: "hidden" }}>
    <span style={cell("th")} onClick={() => onSet("th")}>TH</span>
    <span style={cell("en")} onClick={() => onSet("en")}>EN</span>
  </div>;
}

function AppShell({ t, active, lang, onSetLang, onLogout, onQuickAdd, children }) {
  const groups = NAV_GROUPS(t);
  const tabs = TABS(t);
  const go = (to) => (e) => { e.preventDefault(); navigate(to); };

  return <div style={{ minHeight: "100dvh", background: "var(--bg-grad)" }}>
    {/* header (chrome มืด — mtos-chrome ตั้ง token subtree ให้ลูกทุกตัวเป็นโทนมืด) */}
    <header className="mtos-chrome" style={{ position: "sticky", top: 0, zIndex: "var(--z-nav)", background: "rgba(18,13,40,.92)", backdropFilter: "blur(12px)", borderBottom: "1px solid var(--chrome-border)", color: "var(--chrome-ink)" }}>
      <div style={{ maxWidth: 1440, margin: "0 auto", display: "flex", alignItems: "center", justifyContent: "space-between", padding: "12px 16px", gap: 12 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0, flexShrink: 1 }}>
          <a href="/app" onClick={go("/app")} style={{ flexShrink: 0 }}><Logo /></a>
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 8, flexShrink: 0 }}>
          <div className="only-desktop"><div style={{ display: "flex", gap: 8 }}><DateRangeChip t={t} /><AccountFilterChip t={t} /></div></div>
          {Session.isDemo() && <span data-testid="demo-badge" title={t("demo.badge")} style={{ display: "inline-flex", alignItems: "center", flex: "0 0 auto", padding: "3px 9px", borderRadius: "var(--radius-full)", background: "var(--brand-grad)", color: "#fff", fontSize: 11, fontWeight: 800, letterSpacing: ".3px", whiteSpace: "nowrap" }}>
            <span>{t("demo.badge_short")}</span>
            <span className="hdr-hide-sm" style={{ marginLeft: 4 }}>· {t("demo.badge_suffix")}</span>
          </span>}
          <AccountMenu t={t} onLogout={onLogout} />
        </div>
      </div>
    </header>

    <div style={{ maxWidth: 1440, margin: "0 auto", display: "flex" }}>
      {/* sidebar desktop */}
      <nav className="only-desktop" aria-label="sidebar" style={{ width: 232, flexShrink: 0, padding: "var(--sp-5) var(--sp-3)", borderRight: "1px solid var(--hairline)", minHeight: "calc(100dvh - 55px)", background: "var(--sidebar-bg)" }}>
        {groups.map((g) => <div key={g.label} style={{ marginBottom: "var(--sp-5)" }}>
          <div style={{ fontSize: "var(--fs-micro)", letterSpacing: "1px", textTransform: "uppercase", color: "var(--ink-mute)", padding: "0 var(--sp-3)", marginBottom: "var(--sp-2)" }}>{g.label}</div>
          {g.items.map((it) => <SideItem key={it.name} it={it} active={active === it.name} onClick={go(it.to)} />)}
        </div>)}
        <div style={{ borderTop: "1px solid var(--hairline)", margin: "var(--sp-4) var(--sp-3)" }} />
        <SideItem it={{ label: t("app.nav.unlock"), icon: "★" }} brand active={active === "unlock"} onClick={go("/app/unlock")} />
        <SideItem it={{ label: t("app.nav.settings"), icon: "⚙" }} active={active === "settings"} onClick={go("/app/settings")} />
      </nav>

      {/* content */}
      <main style={{ flex: 1, minWidth: 0, padding: "var(--sp-5) var(--sp-4) calc(var(--tap-min) + var(--sp-8))" }}>{children}</main>
    </div>

    {/* FAB desktop (bottom-right) */}
    <div className="only-desktop" style={{ position: "fixed", right: "var(--sp-6)", bottom: "var(--sp-6)", zIndex: "var(--z-fab)" }}>
      <FAB onClick={onQuickAdd} label={t("app.nav.add")} />
    </div>

    {/* tabbar mobile (light · ขาว + border + active ม่วง ตาม mockup บนพื้นสว่าง) */}
    <nav className="only-mobile" aria-label="tabbar" style={{ position: "fixed", left: 0, right: 0, bottom: 0, zIndex: "var(--z-nav)", height: "calc(var(--tap-min) + 16px + env(safe-area-inset-bottom))", paddingBottom: "env(safe-area-inset-bottom)", background: "rgba(255,255,255,.96)", backdropFilter: "blur(12px)", borderTop: "1px solid var(--hairline)", boxShadow: "0 -2px 12px rgba(20,16,38,.06)", display: "flex", alignItems: "center", justifyContent: "space-around" }}>
      {tabs.map((tb, i) => tb.name === "__fab"
        ? <div key="fab" style={{ marginTop: -22 }}><FAB onClick={onQuickAdd} label={t("app.nav.add")} /></div>
        : <a key={tb.name} href={tb.to} onClick={go(tb.to)} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 2, minHeight: "var(--tap-min)", justifyContent: "center", color: active === tb.name ? "var(--brand-strong)" : "var(--ink-mute)", fontSize: 20 }}>
            <span>{tb.icon}</span>
            <span style={{ fontSize: 10, fontWeight: 600 }}>{tb.label}</span>
          </a>)}
    </nav>
  </div>;
}

function SideItem({ it, active, brand, onClick }) {
  const color = active ? "var(--brand-strong)" : brand ? "var(--brand)" : "var(--ink-dim)";
  return <a href="#" onClick={onClick} style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px var(--sp-3)", borderRadius: "var(--radius-sm)", minHeight: 40, color: color, fontWeight: active ? 700 : 500, background: active ? "var(--brand-soft)" : "transparent", borderLeft: active ? "3px solid var(--brand-strong)" : "3px solid transparent" }}>
    <span style={{ width: 20, textAlign: "center" }}>{it.icon}</span>{it.label}
  </a>;
}

/* ===========================================================================
   Page scaffold + state demo (ทุก route ใช้ได้จริง)
   =========================================================================== */
function StateSwitcher({ t, value, onChange }) {
  if (!isDevMode()) return null;   // ซ่อนแถบ dev ในการใช้งานปกติ · เปิดด้วย localStorage mtos_dev=1
  const opts = [
    ["ready", t("demo.state_ready")], ["loading", t("demo.state_loading")],
    ["empty", t("demo.state_empty")], ["error", t("demo.state_error")], ["locked", t("demo.state_locked")],
  ];
  return <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginBottom: "var(--sp-4)" }}>
    <span style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)", alignSelf: "center", marginRight: 4 }}>{t("demo.state_switcher")}:</span>
    {opts.map(([k, lbl]) => <button key={k} onClick={() => onChange(k)} className={value === k ? "chip-on" : "chip"}>{lbl}</button>)}
  </div>;
}

function PageHeader({ t, page }) {
  return <div style={{ marginBottom: "var(--sp-5)" }}>
    <h1 style={{ fontSize: "var(--fs-h1)", marginBottom: 4 }}>{t("pages." + page + ".title")}</h1>
    <div style={{ color: "var(--ink-dim)", fontSize: "var(--fs-body)" }}>{t("pages." + page + ".subtitle")}</div>
  </div>;
}

/* generic placeholder page ที่ demo states กลางครบ */
function GenericPage({ t, page, toast }) {
  const [st, setSt] = useState("ready");
  return <div>
    <PageHeader t={t} page={page} />
    <StateSwitcher t={t} value={st} onChange={setSt} />
    <div style={{ marginTop: "var(--sp-2)" }}>
      {st === "loading" && <Skeleton variant="list" />}
      {st === "empty" && <EmptyState t={t} onCta={() => toast(t("states.toast_saved"))} />}
      {st === "error" && <ErrorState t={t} code="mock_error" onRetry={() => setSt("ready")} />}
      {st === "locked" && <div style={{ display: "flex", flexDirection: "column", gap: "var(--sp-4)" }}>
        <FirstSyncProgress t={t} minutes={6} progress={0.35} />
        <LockedSampleCard t={t}><Skeleton variant="tiles" /></LockedSampleCard>
      </div>}
      {st === "ready" && <div style={{ padding: "var(--sp-6)", border: "1px dashed var(--hairline)", borderRadius: "var(--radius)", color: "var(--ink-mute)", textAlign: "center" }}>
        <div style={{ marginBottom: "var(--sp-2)" }}>{t("demo.placeholder_note")}</div>
        <div style={{ fontSize: "var(--fs-caption)" }}>{t("demo.central_states_note")}</div>
      </div>}
    </div>
  </div>;
}

/* ===========================================================================
   Dashboard cards (Wave 1) — StatTile · EquityCurve · MiniHeatmap · FeesPanel
   ทุกเลขมาจาก API · client format เท่านั้น · pf_flag/null render ตาม flag
   =========================================================================== */
function StatTile({ label, value, sub, onClick, sample, t }) {
  const clickable = !!onClick;
  return <div className="card" role={clickable ? "button" : undefined} tabIndex={clickable ? 0 : undefined}
    onClick={onClick} onKeyDown={clickable ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick(); } } : undefined}
    style={{ position: "relative", cursor: clickable ? "pointer" : "default", minHeight: 92 }}>
    {sample && <SampleBadge t={t} />}
    <div style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)", marginBottom: 6 }}>{label}</div>
    <div className="disp num" style={{ fontSize: "var(--fs-h2)" }}>{value}</div>
    {sub && <div className="num" style={{ fontSize: "var(--fs-micro)", color: "var(--ink-mute)", marginTop: 4 }}>{sub}</div>}
  </div>;
}

/* สร้าง 5 tile จาก summary object (ใช้ทั้ง real + sample) */
function buildTiles(t, s) {
  const rExp = s.expectancy_r == null ? null : (s.expectancy_r >= 0 ? "+" : "−") + Math.abs(s.expectancy_r).toFixed(2) + "R";
  return [
    { key: "net", label: t("analytics.net_pnl"), value: <MoneyText cents={s.net_pnl_cents} currency={s.currency} />, sub: t("analytics.trades_line", { count: s.trades_closed, wins: s.wins, losses: s.losses, be: s.breakeven }) },
    { key: "wr", label: t("analytics.win_rate"), value: <span className="num">{fmtPct(s.win_rate)}</span>, sub: t("analytics.win_rate_explain"), dim: "win_rate" },
    { key: "pf", label: t("analytics.profit_factor"), value: <span className="num">{s.pf_flag === "no_losses" ? "∞" : s.pf_flag === "no_trades" ? "—" : fmtNum(s.profit_factor)}</span>, dim: "profit_factor" },
    { key: "exp", label: t("analytics.expectancy"), value: <span className="num"><MoneyText cents={s.expectancy_cents} currency={s.currency} /> <span style={{ color: "var(--ink-mute)", fontSize: "var(--fs-micro)" }}>{t("analytics.per_trade")}</span></span>, sub: rExp, dim: "expectancy" },
    { key: "awl", label: t("analytics.avg_wl"), value: <span className="num">{fmtNum(s.payoff_ratio)}</span>, sub: fmtMoney(s.avg_win_cents, s.currency) + " / " + fmtMoney(s.avg_loss_cents, s.currency), dim: "payoff" },
  ];
}
function StatTileGrid({ t, tiles, sample }) {
  return <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(160px,1fr))", gap: "var(--sp-3)" }}>
    {tiles.map((ti) => <StatTile key={ti.key} t={t} label={ti.label} value={ti.value} sub={ti.sub} sample={sample}
      onClick={ti.dim ? () => navigate("/app/insights?dimension=" + ti.dim) : undefined} />)}
  </div>;
}

/* ---------------------------------------------------------------------------
   Dashboard KPI cards (TradeZella-style · Wave dashkpi)
   5 การ์ด gauge/donut/pills — reuse PFDonut · WinGauge · KpiPill (จาก Trades KPI)
   ทุกเลขจาก /analytics/summary (สะท้อน account/range) · client วาด/format เท่านั้น
   --------------------------------------------------------------------------- */
function InfoIcon({ tip }) {
  return <span className="dash-info" title={tip} aria-label={tip} role="img">i</span>;
}
function DashboardKPICards({ t, s, sample }) {
  const gw = s.gross_win_cents || 0, gl = Math.abs(s.gross_loss_cents || 0);
  const pfText = s.pf_flag === "no_losses" ? "∞" : s.pf_flag === "no_trades" ? "—" : fmtNum(s.profit_factor);
  const aw = s.avg_win_cents || 0, al = Math.abs(s.avg_loss_cents || 0);
  const payoff = (s.avg_win_cents != null && al) ? aw / al : null;
  const barTot = aw + al || 1;
  const netColor = s.net_pnl_cents > 0 ? "var(--profit)" : s.net_pnl_cents < 0 ? "var(--loss)" : "var(--ink)";
  const lbl = { margin: 0 };
  return <div>
    {sample && <div style={{ marginBottom: "var(--sp-2)" }}><span className="dash-sample-chip">{t("common.sample")}</span></div>}
    <div className="kpi-strip">
      {/* 1 · Net P&L + count badge */}
      <div className="kpi-card">
        <div className="dash-kpi-head">
          <span className="kpi-label" style={lbl}>{t("analytics.net_pnl")}</span>
          <InfoIcon tip={t("analytics.info_net")} />
          <span className="dash-count num">{s.trades_closed}</span>
        </div>
        <div className="kpi-value num" style={{ color: netColor }}>{fmtMoney(s.net_pnl_cents, s.currency)}</div>
      </div>
      {/* 2 · Trade win % + half-gauge + pills */}
      <div className="kpi-card">
        <div className="dash-kpi-head"><span className="kpi-label" style={lbl}>{t("analytics.win_rate")}</span><InfoIcon tip={t("analytics.info_winrate")} /></div>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
          <div className="kpi-value num">{fmtPct(s.win_rate)}</div>
          <WinGauge rate={s.win_rate} />
        </div>
        <div style={{ display: "flex", gap: 5, marginTop: 8, flexWrap: "wrap" }}>
          <KpiPill kind="W" n={s.wins} label={t("filters.outcome_win")} />
          <KpiPill kind="BE" n={s.breakeven} label={t("filters.outcome_be")} />
          <KpiPill kind="L" n={s.losses} label={t("filters.outcome_loss")} />
        </div>
      </div>
      {/* 3 · Profit factor + donut */}
      <div className="kpi-card">
        <div className="dash-kpi-head"><span className="kpi-label" style={lbl}>{t("analytics.profit_factor")}</span><InfoIcon tip={t("analytics.info_pf")} /></div>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
          <div className="kpi-value num">{pfText}</div>
          <PFDonut win={gw} loss={gl} />
        </div>
      </div>
      {/* 4 · Day win % + half-gauge + pills (วันชนะ/เสมอ/แพ้) */}
      <div className="kpi-card">
        <div className="dash-kpi-head"><span className="kpi-label" style={lbl}>{t("analytics.day_win_rate")}</span><InfoIcon tip={t("analytics.info_daywin")} /></div>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
          <div className="kpi-value num">{fmtPct(s.day_win_rate)}</div>
          <WinGauge rate={s.day_win_rate} />
        </div>
        <div style={{ display: "flex", gap: 5, marginTop: 8, flexWrap: "wrap" }}>
          <KpiPill kind="W" n={s.day_wins} label={t("analytics.win_days")} />
          <KpiPill kind="BE" n={s.day_breakeven} label={t("analytics.be_days")} />
          <KpiPill kind="L" n={s.day_losses} label={t("analytics.loss_days")} />
        </div>
      </div>
      {/* 5 · Avg win/loss + proportion bar + $ */}
      <div className="kpi-card">
        <div className="dash-kpi-head"><span className="kpi-label" style={lbl}>{t("analytics.avg_wl")}</span><InfoIcon tip={t("analytics.info_avgwl")} /></div>
        <div className="kpi-value num">{payoff == null ? "—" : fmtNum(payoff)}</div>
        <div style={{ display: "flex", height: 7, borderRadius: 999, overflow: "hidden", marginTop: 10, background: "var(--surface-3)" }}>
          <div style={{ width: (aw / barTot * 100) + "%", background: "var(--profit)" }} />
          <div style={{ width: (al / barTot * 100) + "%", background: "var(--loss)" }} />
        </div>
        <div style={{ display: "flex", justifyContent: "space-between", marginTop: 5 }}>
          <span className="num" style={{ fontSize: "var(--fs-micro)", color: "var(--profit)", fontWeight: 700 }}>{s.avg_win_cents == null ? "—" : fmtMoney(s.avg_win_cents, s.currency)}</span>
          <span className="num" style={{ fontSize: "var(--fs-micro)", color: "var(--loss)", fontWeight: 700 }}>{s.avg_loss_cents == null ? "—" : fmtMoney(s.avg_loss_cents, s.currency)}</span>
        </div>
      </div>
    </div>
  </div>;
}

/* Expectancy — การ์ดเสริมใต้ KPI strip (ดีไซน์ใหม่ไม่มีใน 5 การ์ด แต่เลขมีค่า) */
function ExpectancyCard({ t, s, sample }) {
  const rExp = s.expectancy_r == null ? null : (s.expectancy_r >= 0 ? "+" : "−") + Math.abs(s.expectancy_r).toFixed(2) + "R";
  return <div className="card" role="button" tabIndex={0}
    onClick={() => navigate("/app/insights?dimension=expectancy")}
    onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); navigate("/app/insights?dimension=expectancy"); } }}
    style={{ position: "relative", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "space-between", gap: "var(--sp-3)", flexWrap: "wrap" }}>
    {sample && <SampleBadge t={t} />}
    <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
      <span className="kpi-label" style={{ margin: 0 }}>{t("analytics.expectancy")}</span>
      <InfoIcon tip={t("analytics.info_expectancy")} />
    </div>
    <div style={{ display: "flex", alignItems: "baseline", gap: 8 }}>
      <span className="disp num" style={{ fontSize: "var(--fs-h3)" }}><MoneyText cents={s.expectancy_cents} currency={s.currency} /></span>
      <span style={{ fontSize: "var(--fs-micro)", color: "var(--ink-mute)" }}>{t("analytics.per_trade")}</span>
      {rExp && <span className="num" style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)" }}>{rExp}</span>}
    </div>
  </div>;
}

/* FeesPanel — commission/swap/fee + รวม (จาก summary.fees) */
function FeesPanel({ t, fees, currency, sample }) {
  if (!fees) return null;
  const rows = [
    [t("analytics.fees_commission"), fees.commission_cents],
    [t("analytics.fees_swap"), fees.swap_cents],
    [t("analytics.fees_fee"), fees.fee_cents],
  ];
  return <div className="card" style={{ position: "relative" }}>
    {sample && <SampleBadge t={t} />}
    <div style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)", marginBottom: "var(--sp-3)" }}>{t("analytics.fees_title")}</div>
    {rows.map(([lbl, v], i) => <div key={i} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "6px 0", fontSize: "var(--fs-caption)" }}>
      <span style={{ color: "var(--ink-dim)" }}>{lbl}</span><MoneyText cents={v} currency={currency} />
    </div>)}
    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", paddingTop: "var(--sp-2)", marginTop: "var(--sp-2)", borderTop: "1px solid var(--hairline)", fontWeight: 700 }}>
      <span>{t("analytics.fees_total")}</span><MoneyText cents={fees.total_cents} currency={currency} />
    </div>
  </div>;
}

/* EquityCurve — ลายเซ็นแบรนด์ (จุดเดียวที่ม่วงแตะพื้นที่ข้อมูล ในฐานะ identity ไม่ใช่ semantic) */
function EquityCurve({ t, granularity, onGran }) {
  const [pts, setPts] = useState(null);
  const [loading, setLoading] = useState(true);
  const q = getQuery();
  const acct = q.get("account_id") || "";
  const range = q.get("range") || "30d";
  useEffect(() => {
    setLoading(true);
    const sp = new URLSearchParams(analyticsQS(acct, range).replace(/^\?/, ""));
    sp.set("granularity", granularity || "trade");
    window.MTOS_API.get("/analytics/equity-curve?" + sp.toString())
      .then((r) => { setPts(r.data.points || []); setLoading(false); })
      .catch(() => { setPts([]); setLoading(false); });
  }, [granularity, acct, range]);

  const toggle = <div className="only-desktop">
    <div style={{ display: "inline-flex", border: "1px solid var(--hairline)", borderRadius: "var(--radius-full)", overflow: "hidden" }}>
      {[["trade", t("analytics.gran_trade")], ["day", t("analytics.gran_day")]].map(([g, lbl]) => <button key={g} onClick={() => onGran(g)}
        style={{ minHeight: 30, padding: "0 12px", border: "none", cursor: "pointer", fontSize: "var(--fs-micro)", fontWeight: 700, background: granularity === g ? "var(--brand-soft)" : "transparent", color: granularity === g ? "var(--brand-bright)" : "var(--ink-mute)" }}>{lbl}</button>)}
    </div>
  </div>;

  const header = <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "var(--sp-3)" }}>
    <span style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)" }}>{t("analytics.equity_curve_label")}</span>{toggle}
  </div>;

  let body;
  if (loading) body = <div style={{ height: 200 }}><Skeleton /></div>;
  else if (!pts || pts.length < 2) body = <div style={{ height: 200, display: "flex", alignItems: "center", justifyContent: "center" }}><EmptyState t={t} title={t("analytics.not_enough_curve")} body="" /></div>;
  else {
    const w = 600, h = 200, pad = 8;
    const ys = pts.map((p) => p.cum_net_pnl_cents);
    const min = Math.min.apply(null, ys.concat([0])), max = Math.max.apply(null, ys.concat([0]));
    const span = max - min || 1;
    const x = (i) => pad + i * (w - pad * 2) / (pts.length - 1);
    const y = (v) => h - pad - (v - min) * (h - pad * 2) / span;
    const line = pts.map((p, i) => (i ? "L" : "M") + x(i).toFixed(1) + " " + y(p.cum_net_pnl_cents).toFixed(1)).join(" ");
    const area = line + " L" + x(pts.length - 1).toFixed(1) + " " + (h - pad) + " L" + x(0).toFixed(1) + " " + (h - pad) + " Z";
    const y0 = y(0);
    // x-axis: 4 label เดือนย่อ จากจุดจริง
    const months = t("calendar.months_short");
    const labelAt = (p) => { const s = p.t || p.trade_date || ""; const d = parseYMD((s.split("T")[0]) || ""); return d ? months[d.getMonth()] : ""; };
    const nLbl = 4, xlabels = [];
    for (let k = 0; k < nLbl; k++) { const idx = Math.round(k * (pts.length - 1) / (nLbl - 1)); xlabels.push([x(idx), labelAt(pts[idx])]); }
    body = <div>
      <svg viewBox={"0 0 " + w + " " + h} preserveAspectRatio="none" style={{ width: "100%", height: 200, display: "block" }}>
        <defs><linearGradient id="eqgrad" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor="var(--brand-bright)" stopOpacity=".28" />
          <stop offset="85%" stopColor="var(--brand-bright)" stopOpacity="0" />
        </linearGradient></defs>
        <line x1={pad} x2={w - pad} y1={y0} y2={y0} stroke="var(--hairline)" strokeWidth="1" vectorEffect="non-scaling-stroke" />
        <path d={area} fill="url(#eqgrad)" />
        <path d={line} fill="none" stroke="var(--brand-bright)" strokeWidth="2" vectorEffect="non-scaling-stroke" strokeLinejoin="round" />
      </svg>
      <div style={{ display: "flex", justifyContent: "space-between", marginTop: 4 }}>
        {xlabels.map((l, i) => <span key={i} style={{ fontSize: "var(--fs-micro)", color: "var(--ink-subtle)" }}>{l[1]}</span>)}
      </div>
    </div>;
  }
  return <div className="card">{header}{body}</div>;
}

/* MiniHeatmap (C8) — เดือนปัจจุบัน · สีตาม sign · แตะวัน -> /app/calendar?day= */
function MiniHeatmap({ t }) {
  const [res, setRes] = useState(null);
  const [err, setErr] = useState(false);
  useEffect(() => { window.MTOS_API.get("/analytics/calendar?month=" + ymNow()).then(setRes).catch(() => setErr(true)); }, []);
  if (err) return null;
  if (!res) return <div className="card"><div style={{ height: 150 }}><Skeleton /></div></div>;
  const ym = parseYM(res.data.month) || parseYM(ymNow());
  const dmap = {}; (res.data.days || []).forEach((d) => { dmap[d.trade_date] = d; });
  const first = new Date(ym.y, ym.m - 1, 1);
  const lead = (first.getDay() + 6) % 7;
  const dim = new Date(ym.y, ym.m, 0).getDate();
  const cells = []; for (let i = 0; i < lead; i++) cells.push(null); for (let d = 1; d <= dim; d++) cells.push(d);
  const weekdays = t("calendar.weekdays");
  const monthName = t("calendar.months")[ym.m - 1] + " " + ym.y;
  // TradeZella-style: พื้นเขียว/แดงอ่อน + ตัวเลข P&L เด่นกลางช่อง · เลขวันเล็กมุมบน
  const cellBg = (d) => { if (!d) return "var(--zebra)"; const n = d.net_pnl_cents; const c = n > 0 ? "var(--profit)" : n < 0 ? "var(--loss)" : "var(--surface-3)"; return "color-mix(in srgb, " + c + " 15%, transparent)"; };
  const cellInk = (d) => { const n = d.net_pnl_cents; return n > 0 ? "var(--profit)" : n < 0 ? "var(--loss)" : "var(--ink-mute)"; };
  return <div className="card">
    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "var(--sp-3)" }}>
      <span style={{ fontSize: "var(--fs-caption)", color: "var(--ink-dim)", fontWeight: 600 }}>{monthName}</span>
      <a href={"/app/calendar?month=" + res.data.month} onClick={(e) => { e.preventDefault(); navigate("/app/calendar?month=" + res.data.month); }} style={{ fontSize: "var(--fs-caption)", color: "var(--brand-bright)", fontWeight: 600 }}>{t("analytics.view_calendar")} ›</a>
    </div>
    <div style={{ display: "grid", gridTemplateColumns: "repeat(7,1fr)", gap: 4 }}>
      {weekdays.map((w, i) => <div key={"h" + i} style={{ textAlign: "center", fontSize: "var(--fs-micro)", color: "var(--ink-subtle)" }}>{w}</div>)}
      {cells.map((num, i) => {
        if (!num) return <div key={i} />;
        const date = res.data.month + "-" + pad2(num);
        const d = dmap[date];
        const aria = d ? num + " " + t("calendar.months_short")[ym.m - 1] + " " + fmtMoneyShort(d.net_pnl_cents, res.meta.currency) + ", " + d.trades_closed_count + " ไม้" : String(num);
        return <button key={i} aria-label={aria} onClick={() => navigate("/app/calendar?day=" + date)}
          style={{ aspectRatio: "1", minWidth: 0, minHeight: 34, border: "none", cursor: "pointer", borderRadius: 5, background: cellBg(d), display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 1, padding: "2px 1px", overflow: "hidden" }}>
          <span style={{ fontSize: 9, fontWeight: 600, color: d ? "var(--ink-mute)" : "var(--ink-subtle)", lineHeight: 1 }}>{num}</span>
          {d && <span className="num" style={{ fontSize: 10, fontWeight: 800, color: cellInk(d), lineHeight: 1, whiteSpace: "nowrap" }}>{fmtMoneyShort(d.net_pnl_cents, res.meta.currency)}</span>}
        </button>;
      })}
    </div>
    <div style={{ marginTop: "var(--sp-3)", fontSize: "var(--fs-caption)" }}>
      <span style={{ color: "var(--ink-mute)" }}>{t("analytics.month_total")} </span><MoneyText cents={res.meta.month_total_cents} currency={res.meta.currency} />
    </div>
  </div>;
}

/* Dashboard เต็ม (Wave 1) — 5 tile · fees · equity · heatmap + ทุก state */
function DashboardPage({ t, toast }) {
  const [devState, setDevState] = useState("ready");   // dev state switcher
  const [gran, setGran] = useState("trade");
  const [phase, setPhase] = useState("loading");       // loading|ready|empty|error|locked
  const [summary, setSummary] = useState(null);
  const [meta, setMeta] = useState(null);
  const [errCode, setErrCode] = useState(null);
  const [lockInfo, setLockInfo] = useState(null);
  const [sample, setSample] = useState(null);
  const [accts, setAccts] = useState(null);
  const q = getQuery();
  const acct = q.get("account_id") || "";
  const range = q.get("range") || "30d";
  const noAccounts = accts && accts.length === 0;

  useEffect(() => { window.MTOS_API.get("/accounts").then((r) => setAccts(r.data)).catch(() => setAccts([])); }, []);

  const load = useCallback((sim) => {
    setPhase("loading");
    if (sim === "loading") return;
    if (sim === "empty") { setTimeout(() => setPhase("empty"), 120); return; }
    const opts = sim === "error" ? { simulate: "error" } : sim === "locked" ? { simulate: "locked" } : {};
    window.MTOS_API.get("/analytics/summary" + analyticsQS(acct, range), opts)
      .then((r) => { setSummary(r.data); setMeta(r.meta); setPhase(r.data.trades_closed === 0 ? "empty" : "ready"); })
      .catch((e) => {
        if (e.status === 409) {
          const det = (e.envelope && e.envelope.error && e.envelope.error.details) || { minutes: 6, progress: 0.35 };
          setLockInfo(det);
          window.MTOS_API.get("/analytics/sample").then((sr) => { setSample(sr.data); setPhase("locked"); }).catch(() => { setErrCode("sample_fail"); setPhase("error"); });
        } else { setErrCode((e.envelope && e.envelope.error && e.envelope.error.code) || "error"); setPhase("error"); }
      });
  }, [acct, range]);

  useEffect(() => { load(devState === "ready" ? null : devState); }, [devState, load]);

  return <div>
    <PageHeader t={t} page="dashboard" />
    <StateSwitcher t={t} value={devState} onChange={setDevState} />
    <PageFilterBar t={t} />

    {phase === "loading" && <div style={{ display: "flex", flexDirection: "column", gap: "var(--sp-4)" }}>
      <Skeleton variant="tiles" /><div className="card"><div style={{ height: 200 }}><Skeleton /></div></div>
    </div>}

    {phase === "error" && <ErrorState t={t} code={errCode} onRetry={() => load(null)} />}

    {phase === "empty" && <EmptyState t={t} title={t("analytics.empty_range")} body={t("states.empty_body")} cta={noAccounts ? t("states.connect_cta") : t("states.empty_cta")} onCta={() => noAccounts ? navigate("/app/connect") : patchQuery({ quickadd: "1" })} />}

    {phase === "locked" && sample && <div style={{ display: "flex", flexDirection: "column", gap: "var(--sp-4)" }}>
      <FirstSyncProgress t={t} minutes={lockInfo && lockInfo.minutes} progress={lockInfo && lockInfo.progress} />
      <div style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)" }}>{t("analytics.first_sync_note")}</div>
      <DashboardKPICards t={t} s={sample} sample />
      <ExpectancyCard t={t} s={sample} sample />
      <FeesPanel t={t} fees={sample.fees} currency={sample.currency} sample />
    </div>}

    {phase === "ready" && summary && <div style={{ display: "flex", flexDirection: "column", gap: "var(--sp-4)" }}>
      <DashboardKPICards t={t} s={summary} />
      <ExpectancyCard t={t} s={summary} />
      <EquityCurve t={t} granularity={gran} onGran={setGran} />
      <div style={{ display: "grid", gridTemplateColumns: "1fr", gap: "var(--sp-4)" }} className="dash-2col">
        <MiniHeatmap t={t} />
        <FeesPanel t={t} fees={summary.fees} currency={summary.currency} />
      </div>
      <SyncWatermark t={t} meta={meta} />
    </div>}
  </div>;
}

/* ===========================================================================
   Calendar (Wave 1) — MonthHeader · MonthGrid · WeeklySubtotal · DayDetailSheet
   =========================================================================== */
function MonthHeader({ t, ym, data }) {
  const ms = data.month_summary || {};
  const name = t("calendar.months")[ym.m - 1] + " " + ym.y;
  const go = (delta) => {
    let m = ym.m + delta, y = ym.y;
    if (m < 1) { m = 12; y--; } if (m > 12) { m = 1; y++; }
    patchQuery({ month: y + "-" + pad2(m), day: null });
  };
  const btn = { width: 40, height: 40, borderRadius: "var(--radius-sm)", border: "1px solid var(--hairline)", background: "transparent", color: "var(--ink)", cursor: "pointer", fontSize: 18 };
  return <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: "var(--sp-3)", marginBottom: "var(--sp-4)" }}>
    <div style={{ display: "flex", alignItems: "center", gap: "var(--sp-3)" }}>
      <button aria-label={t("calendar.prev_month")} onClick={() => go(-1)} style={btn}>‹</button>
      <div className="disp" style={{ fontSize: "var(--fs-h2)", minWidth: 150 }}>{name}</div>
      <button aria-label={t("calendar.next_month")} onClick={() => go(1)} style={btn}>›</button>
    </div>
    <div style={{ display: "flex", gap: "var(--sp-5)", alignItems: "center", flexWrap: "wrap" }}>
      <div><div style={{ fontSize: "var(--fs-micro)", color: "var(--ink-mute)" }}>{t("calendar.net_month")}</div><div className="disp num" style={{ fontSize: "var(--fs-h3)" }}><MoneyText cents={ms.net_pnl_cents} currency="USD" /></div></div>
      <div><div style={{ fontSize: "var(--fs-micro)", color: "var(--ink-mute)" }}>{t("analytics.win_rate")}</div><div className="num" style={{ fontSize: "var(--fs-h3)" }}>{fmtPct(ms.win_rate)}</div></div>
      <div><div style={{ fontSize: "var(--fs-micro)", color: "var(--ink-mute)" }}>{t("calendar.trade_count", { n: "" }).replace("{n}", "").trim() || "ไม้"}</div><div className="num" style={{ fontSize: "var(--fs-h3)" }}>{ms.trades_closed_count}</div></div>
    </div>
  </div>;
}

function MonthGrid({ t, ym, data, sample }) {
  const dmap = {}; (data.days || []).forEach((d) => { dmap[d.trade_date] = d; });
  const wmap = {}; (data.weekly || []).forEach((w) => { wmap[w.week_start] = w; });
  const first = new Date(ym.y, ym.m - 1, 1);
  const lead = (first.getDay() + 6) % 7;
  const dim = new Date(ym.y, ym.m, 0).getDate();
  const cells = []; for (let i = 0; i < lead; i++) cells.push(null); for (let d = 1; d <= dim; d++) cells.push(d);
  while (cells.length % 7) cells.push(null);
  const weeks = []; for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
  const mondayOf = (wi) => { const dd = new Date(ym.y, ym.m - 1, 1 + wi * 7 - lead); return ymd(dd); };
  const weekdays = t("calendar.weekdays");
  const monShort = t("calendar.months_short")[ym.m - 1];
  const today = ymd(new Date());

  const openDay = (date) => patchQuery({ day: date, month: data.month });
  const cellColor = (d) => { const n = d.net_pnl_cents; return n > 0 ? "var(--profit)" : n < 0 ? "var(--loss)" : "var(--surface-3)"; };
  const dotStr = (n) => n <= 3 ? "•".repeat(n) : "×" + n;

  const Cell = ({ num, week }) => {
    if (!num) return <div style={{ minHeight: 44 }} />;
    const date = data.month + "-" + pad2(num);
    const d = dmap[date];
    const isToday = date === today;
    return <button aria-label={d ? num + " " + monShort + " " + fmtMoneyShort(d.net_pnl_cents, "USD") + ", " + d.trades_closed_count + " ไม้" : String(num)}
      onClick={() => openDay(date)}
      style={{ position: "relative", minHeight: 44, border: isToday ? "1px solid var(--brand-edge)" : "1px solid var(--hairline)", borderRadius: "var(--radius-sm)", cursor: "pointer", padding: "4px 5px", textAlign: "left", background: d ? "color-mix(in srgb, " + (d.net_pnl_cents > 0 ? "var(--profit)" : d.net_pnl_cents < 0 ? "var(--loss)" : "var(--surface-3)") + " 12%, transparent)" : "transparent", color: "var(--ink)", display: "flex", flexDirection: "column", justifyContent: "space-between" }}>
      <span style={{ fontSize: 11, color: d ? "var(--ink-dim)" : "var(--ink-subtle)", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
        {num}{d && d.has_daily_note ? <span style={{ width: 5, height: 5, borderRadius: "50%", background: "var(--brand-bright)" }} /> : null}
      </span>
      {d && <span className="num" style={{ fontSize: 11, fontWeight: 700, color: cellColor(d) }}>{fmtMoneyShort(d.net_pnl_cents, "USD")}</span>}
      {d && <span style={{ fontSize: 8, color: "var(--ink-subtle)", lineHeight: 1 }}>{dotStr(d.trades_closed_count)}</span>}
    </button>;
  };

  return <div>
    {/* weekday header */}
    <div style={{ display: "grid", gridTemplateColumns: "repeat(7,1fr)", gap: 5, marginBottom: 5 }}>
      {weekdays.map((w, i) => <div key={i} style={{ textAlign: "center", fontSize: "var(--fs-micro)", color: "var(--ink-mute)" }}>{w}</div>)}
    </div>
    {weeks.map((wk, wi) => <div key={wi} style={{ display: "grid", gridTemplateColumns: "repeat(7,1fr)", gap: 5, marginBottom: 5 }}>
      {wk.map((num, ci) => <Cell key={ci} num={num} week={wi} />)}
    </div>)}
    {/* WeeklySubtotal — desktop = ท้ายเป็นบล็อค · mobile = list ท้าย scroll */}
    <div style={{ marginTop: "var(--sp-4)", borderTop: "1px solid var(--hairline)", paddingTop: "var(--sp-3)" }}>
      <div style={{ fontSize: "var(--fs-micro)", color: "var(--ink-mute)", marginBottom: 6 }}>{t("calendar.weekly_subtotal")}</div>
      {weeks.map((wk, wi) => {
        const w = wmap[mondayOf(wi)];
        if (!w) return null;
        return <div key={wi} style={{ display: "flex", justifyContent: "space-between", fontSize: "var(--fs-caption)", padding: "3px 0" }}>
          <span style={{ color: "var(--ink-dim)" }}>{t("calendar.week_label")} {wi + 1} · {t("calendar.traded_days", { n: w.traded_days_count })}</span>
          <MoneyText cents={w.net_pnl_cents} currency="USD" />
        </div>;
      })}
    </div>
  </div>;
}

function SessionStrip({ t, sessions }) {
  if (!sessions || !sessions.length) return null;
  const order = ["asia", "london", "newyork", "off"];
  const sorted = sessions.slice().sort((a, b) => order.indexOf(a.key) - order.indexOf(b.key));
  return <div>
    <div style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)", marginBottom: "var(--sp-2)" }}>{t("calendar.day_sessions")}</div>
    <div style={{ display: "flex", flexWrap: "wrap", gap: "var(--sp-2)" }}>
      {sorted.map((s) => <div key={s.key} style={{ flex: "1 1 120px", border: "1px solid var(--hairline)", borderRadius: "var(--radius-sm)", padding: "var(--sp-2) var(--sp-3)" }}>
        <div style={{ fontSize: "var(--fs-micro)", color: "var(--ink-mute)" }}>{t("session." + s.key)} · {t("calendar.trade_count", { n: s.trades })}</div>
        <div className="num" style={{ fontSize: "var(--fs-caption)", fontWeight: 700 }}><MoneyText cents={s.net_pnl_cents} currency="USD" /></div>
      </div>)}
    </div>
  </div>;
}

/* DayDetailSheet — mobile bottom-sheet 85% · desktop side panel ขวา */
function DayDetailSheet({ t, date, sample, onClose, toast }) {
  const [d, setD] = useState(null);
  const [note, setNote] = useState("");
  const [noteState, setNoteState] = useState("");   // "" | saving | saved
  const timer = useRef(null);
  useEffect(() => {
    setD(null); setNoteState("");
    window.MTOS_API.get("/calendar/days/" + date).then((r) => { setD(r.data); setNote((r.data.daily_note && r.data.daily_note.body) || ""); }).catch(() => setD({ error: true }));
  }, [date]);
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  const onNote = (e) => {
    const v = e.target.value; setNote(v); setNoteState("saving");
    if (timer.current) clearTimeout(timer.current);
    timer.current = setTimeout(() => {
      window.MTOS_API.put("/daily-notes/" + date, { body: v }).then(() => setNoteState("saved")).catch(() => setNoteState(""));
    }, 600);
  };

  const summary = d && d.summary;
  const content = <div>
    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: "var(--sp-3)" }}>
      <div>
        <div className="disp" style={{ fontSize: "var(--fs-h2)" }}>{date}</div>
        {sample && <span style={{ fontSize: "var(--fs-micro)", color: "var(--brand-bright)", fontWeight: 700 }}>{t("common.sample")}</span>}
      </div>
      <button className="btn-ghost" onClick={onClose} aria-label={t("common.close")} style={{ fontSize: 22, minHeight: 40 }}>×</button>
    </div>
    {!d && <div style={{ height: 200 }}><Skeleton /></div>}
    {d && d.error && <ErrorState t={t} code="day_load" onRetry={() => { }} />}
    {d && !d.error && <div style={{ display: "flex", flexDirection: "column", gap: "var(--sp-4)" }}>
      {/* สรุปวัน */}
      <div>
        <div style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)", marginBottom: "var(--sp-2)" }}>{t("calendar.day_summary")}</div>
        {summary ? <div className="num" style={{ display: "flex", gap: "var(--sp-5)", flexWrap: "wrap" }}>
          <div><div className="disp" style={{ fontSize: "var(--fs-h2)" }}><MoneyText cents={summary.net_pnl_cents} currency="USD" /></div></div>
          <div style={{ fontSize: "var(--fs-caption)", color: "var(--ink-dim)", alignSelf: "center" }}>{t("analytics.trades_line", { count: summary.trades_closed_count, wins: summary.wins_count, losses: summary.losses_count, be: summary.be_count })}</div>
        </div> : <div style={{ color: "var(--ink-mute)", fontSize: "var(--fs-caption)" }}>{t("calendar.no_trades_day")}</div>}
      </div>
      <SessionStrip t={t} sessions={d.sessions} />
      {/* รายการไม้ */}
      {d.trades && d.trades.length > 0 && <div>
        <div style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)", marginBottom: "var(--sp-2)" }}>{t("calendar.day_trades")}</div>
        <div style={{ display: "flex", flexDirection: "column", gap: "var(--sp-2)" }}>
          {d.trades.map((tr) => <button key={tr.trade_id} onClick={() => navigate("/app/trades/" + tr.trade_id)}
            style={{ display: "flex", justifyContent: "space-between", alignItems: "center", width: "100%", textAlign: "left", border: "1px solid var(--hairline)", borderRadius: "var(--radius-sm)", background: "var(--surface-2)", cursor: "pointer", padding: "var(--sp-2) var(--sp-3)", minHeight: 48 }}>
            <span><span className="disp" style={{ fontSize: "var(--fs-caption)" }}>{tr.symbol}</span> <span style={{ fontSize: "var(--fs-micro)", color: tr.direction === "long" ? "var(--profit)" : "var(--loss)" }}>{tr.direction}</span> <span style={{ fontSize: "var(--fs-micro)", color: "var(--ink-mute)" }}>{(tr.volume_lots_x100 / 100).toFixed(2)} lot · {t("session." + tr.session)}</span></span>
            <MoneyText cents={tr.realized_net_pnl_cents} currency="USD" />
          </button>)}
        </div>
      </div>}
      {/* daily note + autosave */}
      <div>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "var(--sp-2)" }}>
          <span style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)" }}>{t("calendar.day_note")}</span>
          <span style={{ fontSize: "var(--fs-micro)", color: noteState === "saved" ? "var(--profit)" : "var(--ink-subtle)" }}>{noteState === "saving" ? t("calendar.note_saving") : noteState === "saved" ? t("calendar.note_saved") : ""}</span>
        </div>
        <textarea value={note} onChange={onNote} placeholder={t("calendar.note_placeholder")}
          style={{ width: "100%", minHeight: 80, resize: "vertical", background: "var(--surface-2)", border: "1px solid var(--hairline)", borderRadius: "var(--radius-sm)", color: "var(--ink)", padding: "var(--sp-3)", fontFamily: "var(--font-ui)", fontSize: "var(--fs-body)" }} />
      </div>
    </div>}
  </div>;

  return ReactDOM.createPortal(<div style={{ position: "fixed", inset: 0, zIndex: "var(--z-modal)" }}>
    <div onClick={onClose} style={{ position: "absolute", inset: 0, background: "var(--scrim)", animation: "mtos-fade var(--dur-base) var(--ease)" }} />
    {/* mobile bottom-sheet (wrapper คุม display ผ่าน class · panel เป็น block ปกติ) */}
    <div className="only-mobile">
      <div role="dialog" aria-modal="true" aria-label={date} style={{ position: "absolute", left: 0, right: 0, bottom: 0, maxHeight: "85dvh", overflowY: "auto", background: "var(--surface-1)", borderTopLeftRadius: "var(--radius-lg)", borderTopRightRadius: "var(--radius-lg)", boxShadow: "var(--shadow-sheet)", padding: "var(--sp-4)", paddingBottom: "calc(var(--sp-6) + env(safe-area-inset-bottom))", animation: "mtos-slideup var(--dur-slow) var(--ease)" }}>
        <div style={{ width: 32, height: 4, borderRadius: 999, background: "var(--ink-mute)", margin: "0 auto var(--sp-3)" }} />
        {content}
      </div>
    </div>
    {/* desktop side panel ขวา */}
    <div className="only-desktop">
      <div role="dialog" aria-modal="true" aria-label={date} style={{ position: "absolute", top: 0, right: 0, bottom: 0, width: 440, maxWidth: "92vw", overflowY: "auto", background: "var(--surface-1)", borderLeft: "1px solid var(--hairline)", boxShadow: "var(--shadow-sheet)", padding: "var(--sp-5)", animation: "mtos-fade var(--dur-base) var(--ease)" }}>
        {content}
      </div>
    </div>
  </div>, document.body);
}

/* ===========================================================================
   Wave 2.5: Calendar desktop — day cards (expandable) + sticky mini-calendar
   เฉพาะ ≥1024px · มือถือคงเดิม (MonthGrid + DayDetailSheet)
   =========================================================================== */
function fmtDayLabel(date, t) {
  const d = parseYMD(date); if (!d) return date;
  return d.getDate() + " " + t("calendar.months_short")[d.getMonth()] + " " + d.getFullYear();
}
/* mini equity ของวัน (intraday cumulative · line + area) */
function DayMiniChart({ t, points }) {
  if (!points || points.length < 2) return <div style={{ height: 120, display: "flex", alignItems: "center", justifyContent: "center", color: "var(--ink-subtle)", fontSize: "var(--fs-micro)" }}>{t("calendar.no_intraday")}</div>;
  const w = 300, h = 120, pad = 8;
  const ys = points.map((p) => p.cum_net_pnl_cents);
  const min = Math.min.apply(null, ys.concat([0])), max = Math.max.apply(null, ys.concat([0]));
  const span = max - min || 1;
  const x = (i) => pad + i * (w - pad * 2) / (points.length - 1);
  const y = (v) => h - pad - (v - min) * (h - pad * 2) / span;
  const up = ys[ys.length - 1] >= 0;
  const col = up ? "var(--profit)" : "var(--loss)";
  const line = points.map((p, i) => (i ? "L" : "M") + x(i).toFixed(1) + " " + y(p.cum_net_pnl_cents).toFixed(1)).join(" ");
  const area = line + " L" + x(points.length - 1).toFixed(1) + " " + (h - pad) + " L" + x(0).toFixed(1) + " " + (h - pad) + " Z";
  const y0 = y(0);
  return <svg viewBox={"0 0 " + w + " " + h} preserveAspectRatio="none" style={{ width: "100%", height: 120, display: "block" }}>
    <defs><linearGradient id={"dg" + up} x1="0" y1="0" x2="0" y2="1">
      <stop offset="0%" stopColor={col} stopOpacity=".22" /><stop offset="90%" stopColor={col} stopOpacity="0" />
    </linearGradient></defs>
    <line x1={pad} x2={w - pad} y1={y0} y2={y0} stroke="var(--hairline)" strokeWidth="1" vectorEffect="non-scaling-stroke" />
    <path d={area} fill={"url(#dg" + up + ")"} />
    <path d={line} fill="none" stroke={col} strokeWidth="2" vectorEffect="non-scaling-stroke" strokeLinejoin="round" />
  </svg>;
}
/* daily note editor (desktop day card) — autosave debounce → PUT /daily-notes */
function DayNoteEditor({ t, date, initial }) {
  const [note, setNote] = useState(initial || "");
  const [state, setState] = useState("");
  const timer = useRef(null);
  useEffect(() => { setNote(initial || ""); setState(""); }, [date]);
  const onChange = (e) => {
    const v = e.target.value; setNote(v); setState("saving");
    if (timer.current) clearTimeout(timer.current);
    timer.current = setTimeout(() => { window.MTOS_API.put("/daily-notes/" + date, { body: v }).then(() => setState("saved")).catch(() => setState("")); }, 600);
  };
  return <div>
    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "var(--sp-2)" }}>
      <span style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)" }}>{t("calendar.day_note")}</span>
      <span style={{ fontSize: "var(--fs-micro)", color: state === "saved" ? "var(--profit)" : "var(--ink-subtle)" }}>{state === "saving" ? t("calendar.note_saving") : state === "saved" ? t("calendar.note_saved") : ""}</span>
    </div>
    <textarea value={note} onChange={onChange} placeholder={t("calendar.note_placeholder")}
      style={{ width: "100%", minHeight: 72, resize: "vertical", background: "var(--surface-2)", border: "1px solid var(--hairline)", borderRadius: "var(--radius-sm)", color: "var(--ink)", padding: "var(--sp-3)", fontFamily: "var(--font-ui)", fontSize: "var(--fs-body)" }} />
  </div>;
}
/* DayCard — collapsed: caret+วันที่+Net P&L · expand: mini chart + 7 stats + trades table + note */
function DayCard({ t, date, agg, currency, expanded, onToggle }) {
  const [d, setD] = useState(null);
  const [err, setErr] = useState(false);
  const ref = useRef(null);
  useEffect(() => {
    if (!expanded) return;
    if (d || err) return;
    setErr(false);
    window.MTOS_API.get("/calendar/days/" + date).then((r) => setD(r.data)).catch(() => setErr(true));
  }, [expanded, date]);
  useEffect(() => { if (expanded && ref.current) { try { ref.current.scrollIntoView({ behavior: "smooth", block: "nearest" }); } catch (e) {} } }, [expanded]);

  const net = agg ? agg.net_pnl_cents : (d && d.summary ? d.summary.net_pnl_cents : null);
  const noTrades = agg == null && (!d || !d.summary);
  const sm = d && d.summary;
  const winsLosses = sm ? (sm.wins_count + " / " + sm.losses_count) : "—";
  const statCells = sm ? [
    [t("calendar.stat.total_trades"), <span className="num">{sm.trades_closed_count}</span>],
    [t("calendar.stat.gross"), <MoneyText cents={sm.gross_pnl_cents} currency={currency} />],
    [t("calendar.stat.winners_losers"), <span className="num">{winsLosses}</span>],
    [t("calendar.stat.commissions"), <MoneyText cents={sm.commission_cents} currency={currency} />],
    [t("calendar.stat.win_rate"), <span className="num">{fmtPct(sm.win_rate)}</span>],
    [t("calendar.stat.volume"), <span className="num">{(sm.volume_lots_x100 / 100).toFixed(2)}</span>],
    [t("calendar.stat.pf"), <span className="num">{sm.profit_factor == null ? "∞" : fmtNum(sm.profit_factor)}</span>],
  ] : [];

  return <div className="daycard" ref={ref}>
    <button className="daycard-head" aria-expanded={expanded} onClick={onToggle}>
      <span className={"daycard-caret" + (expanded ? " open" : "")}>▶</span>
      <span className="disp" style={{ fontSize: "var(--fs-h3)", fontWeight: 700, minWidth: 130 }}>{fmtDayLabel(date, t)}</span>
      {agg && agg.has_daily_note ? <span style={{ width: 6, height: 6, borderRadius: "50%", background: "var(--brand-strong)", flex: "0 0 auto" }} /> : null}
      <span style={{ flex: 1 }} />
      {net != null && <span className="num disp" style={{ fontSize: "var(--fs-h3)", fontWeight: 700, color: net > 0 ? "var(--profit)" : net < 0 ? "var(--loss)" : "var(--ink)" }}>{fmtMoney(net, currency)}</span>}
    </button>
    {expanded && <div className="daycard-body">
      {err && <ErrorState t={t} code="day_load" onRetry={() => { setErr(false); setD(null); }} />}
      {!err && !d && <div style={{ height: 160 }}><Skeleton /></div>}
      {!err && d && noTrades && <div style={{ textAlign: "center", padding: "var(--sp-5)", color: "var(--ink-mute)" }}>
        <div style={{ marginBottom: "var(--sp-3)" }}>{t("calendar.no_trades_day")}</div>
        <DayNoteEditor t={t} date={date} initial={d.daily_note ? d.daily_note.body : ""} />
      </div>}
      {!err && d && !noTrades && <div style={{ display: "flex", flexDirection: "column", gap: "var(--sp-4)" }}>
        <div className="daycard-split">
          <div><div style={{ fontSize: "var(--fs-micro)", color: "var(--ink-mute)", marginBottom: 4 }}>{t("calendar.day_curve")}</div><DayMiniChart t={t} points={d.intraday_points} /></div>
          <div className="daystat-grid">
            {statCells.map((c, i) => <div key={i}><div className="daystat-label">{c[0]}</div><div className="daystat-value">{c[1]}</div></div>)}
          </div>
        </div>
        {d.trades && d.trades.length > 0 && <div>
          <table className="trades-table">
            <thead><tr><th>{t("trades.col_time")}</th><th>{t("trades.col_symbol")}</th><th>{t("trades.col_direction")}</th><th style={{ textAlign: "right" }}>{t("trades.col_pnl")}</th><th style={{ textAlign: "right" }}>{t("trades.col_r")}</th></tr></thead>
            <tbody>{d.trades.map((tr) => <tr key={tr.trade_id} onClick={() => navigate("/app/trades/" + tr.trade_id)}>
              <td className="num" style={{ color: "var(--ink-mute)" }}>{fmtTime(tr.closed_at)}</td>
              <td><span className="disp" style={{ fontWeight: 700 }}>{tr.symbol}</span></td>
              <td><DirText t={t} dir={tr.direction} /></td>
              <td style={{ textAlign: "right" }}><MoneyText cents={tr.realized_net_pnl_cents} currency={currency} /></td>
              <td className="num" style={{ textAlign: "right" }}>{fmtR(tr.r_multiple)}</td>
            </tr>)}</tbody>
          </table>
        </div>}
        <DayNoteEditor t={t} date={date} initial={d.daily_note ? d.daily_note.body : ""} />
      </div>}
    </div>}
  </div>;
}
/* mini calendar sticky (desktop) — grid สีตาม sign · คลิกวันที่มีเทรด → expand card */
function CalMiniCal({ t, ym, month, dmap, selected, onPick }) {
  const first = new Date(ym.y, ym.m - 1, 1);
  const lead = (first.getDay() + 6) % 7;
  const dim = new Date(ym.y, ym.m, 0).getDate();
  const cells = []; for (let i = 0; i < lead; i++) cells.push(null); for (let dd = 1; dd <= dim; dd++) cells.push(dd);
  const weekdays = t("calendar.weekdays");
  return <div className="card">
    <div style={{ display: "grid", gridTemplateColumns: "repeat(7,1fr)", gap: 4, marginBottom: 6 }}>
      {weekdays.map((w, i) => <div key={i} style={{ textAlign: "center", fontSize: 10, fontWeight: 700, color: "var(--brand-strong)" }}>{w}</div>)}
    </div>
    <div style={{ display: "grid", gridTemplateColumns: "repeat(7,1fr)", gap: 4 }}>
      {cells.map((num, i) => {
        if (!num) return <div key={i} />;
        const date = month + "-" + pad2(num);
        const dd = dmap[date];
        const isSel = date === selected;
        const net = dd ? dd.net_pnl_cents : null;
        const dot = net == null ? null : net > 0 ? "var(--profit)" : net < 0 ? "var(--loss)" : "var(--ink-subtle)";
        return <button key={i} disabled={!dd} onClick={() => dd && onPick(date)} aria-label={date}
          style={{ aspectRatio: "1", minWidth: 0, border: isSel ? "1px solid var(--brand-edge)" : "1px solid transparent", background: isSel ? "var(--brand-soft)" : "transparent", borderRadius: 8, cursor: dd ? "pointer" : "default", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 2, padding: 0, color: dd ? "var(--ink)" : "var(--ink-faint)", fontSize: 12, fontWeight: isSel ? 700 : 500 }}>
          <span>{num}</span>
          {dot && <span style={{ width: 5, height: 5, borderRadius: "50%", background: dot }} />}
        </button>;
      })}
    </div>
  </div>;
}
function CalendarDesktop({ t, ym, data, sample, dayParam, toast }) {
  const month = data.month;
  const dmap = {}; (data.days || []).forEach((d) => { dmap[d.trade_date] = d; });
  const currency = "USD";
  const [expanded, setExpanded] = useState(dayParam && dmap[dayParam] ? dayParam : null);
  // sync กับ ?day= (back/forward · deep-link)
  useEffect(() => { setExpanded(dayParam && dmap[dayParam] ? dayParam : (dayParam && !dmap[dayParam] ? dayParam : null)); }, [dayParam, month]);
  const toggle = (date) => { const next = expanded === date ? null : date; setExpanded(next); patchQuery({ day: next || null, month: month }); };
  const pick = (date) => { setExpanded(date); patchQuery({ day: date, month: month }); };

  // วันเรียงล่าสุดก่อน · ถ้า deep-link วันว่าง (ไม่มีเทรด) แทรกการ์ดว่างบนสุด
  const dates = (data.days || []).map((d) => d.trade_date).sort().reverse();
  const emptySelected = expanded && !dmap[expanded];
  return <div className="cal-desktop">
    <div className="cal-daylist">
      {emptySelected && <DayCard t={t} date={expanded} agg={null} currency={currency} expanded onToggle={() => toggle(expanded)} />}
      {dates.map((date) => <DayCard key={date} t={t} date={date} agg={dmap[date]} currency={currency} expanded={expanded === date} onToggle={() => toggle(date)} />)}
    </div>
    <aside className="cal-minical">
      <CalMiniCal t={t} ym={ym} month={month} dmap={dmap} selected={expanded} onPick={pick} />
    </aside>
  </div>;
}

function CalendarPage({ t, toast }) {
  const isDesktop = useIsDesktop();
  const [devState, setDevState] = useState("ready");
  const [phase, setPhase] = useState("loading");   // loading|ready|empty|error|locked
  const [data, setData] = useState(null);
  const [meta, setMeta] = useState(null);
  const [errCode, setErrCode] = useState(null);
  const [accts, setAccts] = useState(null);
  const q = getQuery();
  const monthParam = q.get("month");
  const dayParam = q.get("day");
  const acct = q.get("account_id") || "";
  const noAccounts = accts && accts.length === 0;
  useEffect(() => { window.MTOS_API.get("/accounts").then((r) => setAccts(r.data)).catch(() => setAccts([])); }, []);

  // strip ?day= ที่ผิด format / เป็นอนาคต (E10)
  const dayDate = parseYMD(dayParam);
  const todayMid = new Date(); todayMid.setHours(0, 0, 0, 0);
  const dayValid = !!(dayDate && dayDate <= todayMid);
  useEffect(() => { if (dayParam && !dayValid) patchQuery({ day: null }, true); }, [dayParam, dayValid]);
  const sheetOpen = !!(dayParam && dayValid);

  const load = useCallback((sim, mparam) => {
    setPhase("loading");
    if (sim === "loading") return;
    if (sim === "empty") { setTimeout(() => setPhase("empty"), 120); return; }
    if (sim === "locked") {
      window.MTOS_API.get("/calendar/sample").then((r) => { setData(r.data); setMeta(r.meta); setPhase("locked"); }).catch(() => { setErrCode("error"); setPhase("error"); });
      return;
    }
    const ym = parseYM(mparam) ? mparam : ymNow();
    window.MTOS_API.get("/calendar/" + ym, sim === "error" ? { simulate: "error" } : {})
      .then((r) => { setData(r.data); setMeta(r.meta); setPhase(r.data.days && r.data.days.length ? "ready" : "empty"); })
      .catch((e) => { setErrCode((e.envelope && e.envelope.error && e.envelope.error.code) || "error"); setPhase("error"); });
  }, []);

  useEffect(() => { load(devState === "ready" ? null : devState, monthParam); }, [devState, monthParam, acct, load]);

  const ym = data ? (parseYM(data.month) || parseYM(ymNow())) : null;

  return <div>
    <PageHeader t={t} page="calendar" />
    <StateSwitcher t={t} value={devState} onChange={setDevState} />
    <PageFilterBar t={t} />

    {phase === "loading" && <div className="card"><div style={{ height: 320 }}><Skeleton /></div></div>}
    {phase === "error" && <ErrorState t={t} code={errCode} onRetry={() => load(null, monthParam)} />}
    {phase === "empty" && <EmptyState t={t} title={t("calendar.empty_month")} body={t("calendar.empty_month_body")} cta={noAccounts ? t("states.connect_cta") : t("states.empty_cta")} onCta={() => noAccounts ? navigate("/app/connect") : patchQuery({ quickadd: "1" })} />}

    {(phase === "ready" || phase === "locked") && data && ym && <div>
      {phase === "locked" && <div style={{ marginBottom: "var(--sp-4)" }}><FirstSyncProgress t={t} minutes={6} progress={0.35} /></div>}
      {isDesktop && phase === "ready"
        ? <div>
            <div className="card" style={{ marginBottom: "var(--sp-4)" }}><MonthHeader t={t} ym={ym} data={data} /></div>
            <CalendarDesktop t={t} ym={ym} data={data} sample={false} dayParam={dayValid ? dayParam : null} toast={toast} />
          </div>
        : <div className="card" style={{ position: "relative" }}>
            {phase === "locked" && <SampleBadge t={t} />}
            <MonthHeader t={t} ym={ym} data={data} />
            <MonthGrid t={t} ym={ym} data={data} sample={phase === "locked"} />
          </div>}
      <SyncWatermark t={t} meta={meta} />
    </div>}

    {/* sheet ใช้กับ MonthGrid (มือถือทุก phase · desktop เฉพาะ locked) · desktop ready → expand การ์ด (กัน portal หลุด) */}
    {sheetOpen && !(isDesktop && phase === "ready") && <DayDetailSheet t={t} date={dayParam} sample={phase === "locked"} onClose={() => patchQuery({ day: null })} toast={toast} />}
  </div>;
}

/* ===========================================================================
   Public pages (login/register — mock)
   =========================================================================== */
function AuthPage({ t, mode, onLogin }) {
  const params = new URLSearchParams(window.location.search);
  const next = params.get("next") || "/app";
  const doLogin = () => { onLogin(); navigate(next, true); };
  return <div style={{ minHeight: "100dvh", display: "flex", alignItems: "center", justifyContent: "center", padding: "var(--sp-5)", background: "var(--bg-grad)" }}>
    <div className="card" style={{ width: "100%", maxWidth: 380 }}>
      <div style={{ marginBottom: "var(--sp-5)" }}><Logo /></div>
      <h1 style={{ fontSize: "var(--fs-h1)", marginBottom: "var(--sp-4)" }}>{t(mode === "register" ? "auth.register_title" : "auth.login_title")}</h1>
      <label className="lbl">{t("auth.email")}</label>
      <input className="inp" type="email" autoComplete="username" placeholder="you@email.com" />
      <label className="lbl">{t("auth.password")}</label>
      <input className="inp" type="password" autoComplete="current-password" />
      <button className="btn-primary" style={{ width: "100%", marginTop: "var(--sp-4)" }} onClick={doLogin}>{t("auth.mock_login")}</button>
      <div style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)", marginTop: "var(--sp-3)", textAlign: "center" }}>{t("auth.demo_note")}</div>
      <div style={{ textAlign: "center", marginTop: "var(--sp-4)" }}>
        <a href={mode === "register" ? "/login" : "/register"} onClick={(e) => { e.preventDefault(); navigate(mode === "register" ? "/login" : "/register"); }} style={{ fontSize: "var(--fs-caption)" }}>{t(mode === "register" ? "auth.to_login" : "auth.to_register")}</a>
      </div>
    </div>
  </div>;
}

function NotFoundPage({ t }) {
  return <div style={{ textAlign: "center", padding: "var(--sp-8) var(--sp-4)" }}>
    <div className="disp" style={{ fontSize: 48, color: "var(--brand-bright)", marginBottom: "var(--sp-3)" }}>404</div>
    <div className="disp" style={{ fontSize: "var(--fs-h2)", marginBottom: "var(--sp-2)" }}>{t("errors.not_found_page")}</div>
    <div style={{ color: "var(--ink-dim)", marginBottom: "var(--sp-5)" }}>{t("errors.not_found_page_body")}</div>
    <button className="btn-brand" onClick={() => navigate("/app")}>{t("errors.go_dashboard")}</button>
  </div>;
}

/* ===========================================================================
   LandingPage — route "/" (public · ไม่ต้อง session)
   โครงย่อจาก marketing landing · CTA เดียวพาเข้า demo · light theme · mobile-first
   copy: MT4 = อัปโหลด statement (ห้าม "sync") · ไม่มีราคา hardcode
   =========================================================================== */
function EquityCurveSignature({ t }) {
  // ลายเซ็น equity curve — SVG นิ่ง (0 external request) · แสดงผลอย่างเดียว
  const line = "M0,176 L60,170 L120,180 L180,150 L240,160 L300,118 L360,130 L420,86 L480,96 L540,50 L600,36";
  const area = line + " L600,200 L0,200 Z";
  return <div className="card" style={{ padding: "var(--sp-4)", overflow: "hidden" }}>
    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: "var(--sp-3)" }}>
      <span style={{ fontSize: "var(--fs-caption)", fontWeight: 700, color: "var(--ink-dim)" }}>{t("landing.eq_title")}</span>
      <span style={{ fontSize: "var(--fs-caption)", fontWeight: 800, color: "var(--profit)" }}>{t("landing.eq_delta")}</span>
    </div>
    <svg viewBox="0 0 600 200" preserveAspectRatio="none" role="img" aria-label={t("landing.eq_title")} style={{ width: "100%", height: 140, display: "block" }}>
      <defs>
        <linearGradient id="eqStroke" x1="0" y1="0" x2="1" y2="0">
          <stop offset="0" stopColor="#6d3bf0" /><stop offset="0.55" stopColor="#b02fd0" /><stop offset="1" stopColor="#f0409a" />
        </linearGradient>
        <linearGradient id="eqFill" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stopColor="#6d3bf0" stopOpacity="0.20" /><stop offset="1" stopColor="#6d3bf0" stopOpacity="0" />
        </linearGradient>
      </defs>
      <path d={area} fill="url(#eqFill)" />
      <path d={line} fill="none" stroke="url(#eqStroke)" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
    <div style={{ fontSize: "var(--fs-micro)", color: "var(--ink-mute)", marginTop: "var(--sp-2)" }}>{t("landing.eq_caption")}</div>
  </div>;
}

function LandingPage({ t, lang, onSetLang, onDemo }) {
  const goLogin = (e) => { e.preventDefault(); navigate("/login"); };
  const goRegister = (e) => { e.preventDefault(); navigate("/register"); };
  const feats = [
    { icon: "▦", k: "f1" }, { icon: "◎", k: "f2" }, { icon: "⇪", k: "f3" }, { icon: "◱", k: "f4" },
  ];
  return <div style={{ minHeight: "100dvh", background: "var(--bg-grad)", display: "flex", flexDirection: "column" }}>
    {/* top bar */}
    <header style={{ position: "sticky", top: 0, zIndex: "var(--z-nav)", background: "rgba(246,246,250,.9)", backdropFilter: "blur(10px)", borderBottom: "1px solid var(--hairline)" }}>
      <div style={{ maxWidth: 960, margin: "0 auto", display: "flex", alignItems: "center", justifyContent: "space-between", padding: "12px 16px", gap: 12 }}>
        <Logo />
        <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
          <LangToggle lang={lang} onSet={onSetLang} />
          <a href="/login" onClick={goLogin} style={{ fontSize: "var(--fs-caption)", fontWeight: 600 }}>{t("landing.cta_login")}</a>
        </div>
      </div>
    </header>

    <main style={{ flex: 1, maxWidth: 960, width: "100%", margin: "0 auto", padding: "var(--sp-7) var(--sp-4) var(--sp-8)" }}>
      {/* hero */}
      <section style={{ textAlign: "center", marginBottom: "var(--sp-8)" }}>
        <div style={{ display: "inline-block", fontSize: "var(--fs-micro)", fontWeight: 700, letterSpacing: "1px", textTransform: "uppercase", color: "var(--brand-bright)", background: "var(--brand-soft)", padding: "5px 12px", borderRadius: "var(--radius-full)", marginBottom: "var(--sp-4)" }}>{t("landing.eyebrow")}</div>
        <h1 className="disp" style={{ fontSize: "clamp(30px,7vw,52px)", fontWeight: 800, lineHeight: 1.05, letterSpacing: "-1px", margin: "0 0 var(--sp-4)", color: "var(--ink)" }}>
          {t("landing.hero_h1a")}<br /><span style={{ background: "var(--brand-grad)", WebkitBackgroundClip: "text", backgroundClip: "text", color: "transparent" }}>{t("landing.hero_h1b")}</span>
        </h1>
        <p style={{ fontSize: "clamp(16px,2.5vw,20px)", color: "var(--ink-dim)", maxWidth: 560, margin: "0 auto var(--sp-6)", lineHeight: 1.55 }}>{t("landing.hero_tagline")}</p>
        <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "var(--sp-3)" }}>
          <button className="btn-brand" data-testid="cta-demo" onClick={onDemo} style={{ fontSize: 18, fontWeight: 700, padding: "0 var(--sp-7)", height: 56, borderRadius: "var(--radius-full)", minWidth: 240 }}>{t("landing.cta_demo")}</button>
          <div style={{ display: "flex", gap: "var(--sp-4)", fontSize: "var(--fs-caption)" }}>
            <a href="/login" onClick={goLogin} style={{ fontWeight: 600 }}>{t("landing.cta_login")}</a>
            <span style={{ color: "var(--ink-subtle)" }}>·</span>
            <a href="/register" onClick={goRegister} style={{ fontWeight: 600 }}>{t("landing.cta_register")}</a>
          </div>
          <div style={{ fontSize: "var(--fs-micro)", color: "var(--ink-mute)", marginTop: 2 }}>{t("landing.cta_note")}</div>
        </div>
      </section>

      {/* equity curve signature */}
      <section style={{ maxWidth: 640, margin: "0 auto var(--sp-8)" }}>
        <EquityCurveSignature t={t} />
      </section>

      {/* features */}
      <section>
        <h2 className="disp" style={{ textAlign: "center", fontSize: "clamp(22px,4vw,30px)", fontWeight: 800, letterSpacing: "-0.5px", margin: "0 0 var(--sp-6)", color: "var(--ink)" }}>{t("landing.feat_h2")}</h2>
        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(220px,1fr))", gap: "var(--sp-4)" }}>
          {feats.map((f) => <div key={f.k} className="card">
            <div style={{ width: 40, height: 40, borderRadius: "var(--radius-sm)", background: "var(--brand-soft)", color: "var(--brand-bright)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 20, marginBottom: "var(--sp-3)" }}>{f.icon}</div>
            <div style={{ fontWeight: 700, fontSize: "var(--fs-body)", color: "var(--ink)", marginBottom: 4 }}>{t("landing." + f.k + "_t")}</div>
            <div style={{ fontSize: "var(--fs-caption)", color: "var(--ink-dim)", lineHeight: 1.55 }}>{t("landing." + f.k + "_d")}</div>
          </div>)}
        </div>
      </section>

      {/* bottom CTA */}
      <section style={{ textAlign: "center", marginTop: "var(--sp-8)" }}>
        <button className="btn-brand" onClick={onDemo} style={{ fontSize: 17, fontWeight: 700, padding: "0 var(--sp-7)", height: 52, borderRadius: "var(--radius-full)", minWidth: 220 }}>{t("landing.cta_demo")}</button>
      </section>
    </main>

    <footer style={{ borderTop: "1px solid var(--hairline)", padding: "var(--sp-5) var(--sp-4)", textAlign: "center" }}>
      <div style={{ marginBottom: "var(--sp-2)" }}><Logo /></div>
      <div style={{ fontSize: "var(--fs-micro)", color: "var(--ink-mute)" }}>{t("landing.footer")}</div>
    </footer>
  </div>;
}

/* ===========================================================================
   Wave 2: Journal Core — shared journal components (EmotionPicker · TagPicker · TradeNoteBox)
   ใช้ร่วมทั้ง Trade detail + Quick-add · optimistic write ผ่าน mock API
   =========================================================================== */
const EMOTIONS = ["confident", "calm", "focused", "fomo", "fear", "greed", "revenge", "anxious", "impatient", "neutral"];
const EMOTION_EMOJI = { confident: "💪", calm: "😌", focused: "🎯", fomo: "🏃", fear: "😨", greed: "🤑", revenge: "😤", anxious: "😟", impatient: "⏱️", neutral: "😐" };

/* EmotionPicker — 10 emoji แถวเดียว scroll แนวนอน · 1 tap = บันทึก · แตะซ้ำ = ยกเลิก */
function EmotionPicker({ t, value, onPick }) {
  return <div style={{ display: "flex", gap: 8, overflowX: "auto", paddingBottom: 6, WebkitOverflowScrolling: "touch" }}>
    {EMOTIONS.map((k) => {
      const on = value === k;
      return <button key={k} onClick={() => onPick(on ? null : k)} aria-pressed={on} aria-label={t("journal.emotion." + k)}
        style={{ flex: "0 0 auto", minWidth: 64, minHeight: 64, padding: "6px 8px", borderRadius: "var(--radius)", cursor: "pointer",
          border: on ? "1.5px solid var(--brand-bright)" : "1px solid var(--hairline)",
          background: on ? "var(--brand-soft)" : "var(--surface-2)", color: on ? "var(--brand-bright)" : "var(--ink-dim)",
          display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 3 }}>
        <span style={{ fontSize: 22, lineHeight: 1 }}>{EMOTION_EMOJI[k]}</span>
        <span style={{ fontSize: 10, fontWeight: 600, whiteSpace: "nowrap" }}>{t("journal.emotion." + k)}</span>
      </button>;
    })}
  </div>;
}

/* TradeNoteBox — autosave debounce 800ms → PUT journal · "บันทึกแล้ว ✓" */
function TradeNoteBox({ t, tradeId, initial }) {
  const [note, setNote] = useState(initial || "");
  const [state, setState] = useState("");   // "" | saving | saved
  const timer = useRef(null);
  useEffect(() => { setNote(initial || ""); setState(""); }, [tradeId]);
  const onChange = (e) => {
    const v = e.target.value.slice(0, 10000);
    setNote(v); setState("saving");
    if (timer.current) clearTimeout(timer.current);
    timer.current = setTimeout(() => {
      window.MTOS_API.put("/trades/" + tradeId + "/journal", { note_body: v }).then(() => setState("saved")).catch(() => setState(""));
    }, 800);
  };
  return <div>
    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "var(--sp-2)" }}>
      <span style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)" }}>{t("journal.note_title")}</span>
      <span data-testid="note-state" style={{ fontSize: "var(--fs-micro)", color: state === "saved" ? "var(--profit)" : "var(--ink-subtle)" }}>
        {state === "saving" ? t("journal.note_saving") : state === "saved" ? t("journal.note_saved") + " ✓" : ""}</span>
    </div>
    <textarea value={note} onChange={onChange} placeholder={t("journal.note_placeholder")}
      style={{ width: "100%", minHeight: 96, resize: "vertical", background: "var(--surface-2)", border: "1px solid var(--hairline)", borderRadius: "var(--radius-sm)", color: "var(--ink)", padding: "var(--sp-3)", fontFamily: "var(--font-ui)", fontSize: "var(--fs-body)" }} />
    {note.length > 9000 && <div style={{ textAlign: "right", fontSize: "var(--fs-micro)", color: "var(--ink-subtle)", marginTop: 4 }}>{t("journal.note_counter", { n: note.length })}</div>}
  </div>;
}

/* TagPicker — tag ระบบ (EA/Manual) lock · user tag toggle · สร้างใหม่ inline */
function TagPicker({ t, options, selected, onToggle, onCreate }) {
  const [adding, setAdding] = useState(false);
  const [name, setName] = useState("");
  const sel = {}; (selected || []).forEach((id) => { sel[id] = 1; });
  const create = () => { const n = name.trim(); if (!n) return; onCreate(n); setName(""); setAdding(false); };
  const sysSelected = (options || []).filter((x) => x.is_system && sel[x.public_id]);
  const userTags = (options || []).filter((x) => !x.is_system);
  return <div>
    <div style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)", marginBottom: "var(--sp-2)" }}>{t("journal.tags_title")}</div>
    <div style={{ display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" }}>
      {sysSelected.map((tag) => <span key={tag.public_id} title={t("journal.tag_system")}
        style={{ display: "inline-flex", alignItems: "center", gap: 4, minHeight: 32, padding: "0 var(--sp-3)", borderRadius: "var(--radius-full)", fontSize: "var(--fs-caption)", fontWeight: 600, border: "1px solid var(--hairline)", background: "var(--zebra)", color: "var(--ink-mute)" }}>🔒 {tag.name}</span>)}
      {userTags.map((tag) => { const on = !!sel[tag.public_id];
        return <button key={tag.public_id} onClick={() => onToggle(tag.public_id)} className={on ? "chip-on" : "chip"} style={{ minHeight: 32 }}>{on ? "✓ " : ""}{tag.name}</button>; })}
      {adding
        ? <span style={{ display: "inline-flex", gap: 4, alignItems: "center" }}>
            <input autoFocus value={name} onChange={(e) => setName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") create(); }} placeholder={t("journal.tag_new_placeholder")} className="inp" style={{ height: 36, minHeight: 36, width: 150 }} />
            <button className="btn-brand" onClick={create} style={{ minHeight: 36, padding: "0 var(--sp-3)", fontSize: "var(--fs-caption)" }} disabled={!name.trim()}>{name.trim() ? t("journal.tag_create", { name: name.trim() }) : t("common.save")}</button>
          </span>
        : <button className="chip" onClick={() => setAdding(true)} style={{ minHeight: 32 }}>+ {t("journal.tag_add")}</button>}
    </div>
  </div>;
}

/* badges — EA/Manual · ปิดบางส่วน · สถานะ open */
function OriginBadge({ t, origin }) {
  const ea = origin === "ea";
  return <span style={{ fontSize: "var(--fs-micro)", fontWeight: 700, padding: "1px 7px", borderRadius: "var(--radius-sm)", background: ea ? "var(--brand-soft)" : "var(--zebra)", color: ea ? "var(--brand-bright)" : "var(--ink-mute)" }}>{ea ? t("trades.badge_ea") : t("trades.badge_manual")}</span>;
}
function PartialBadge({ t }) {
  return <span style={{ fontSize: "var(--fs-micro)", fontWeight: 700, padding: "1px 7px", borderRadius: "var(--radius-sm)", background: "rgba(200,163,90,.16)", color: "#c8a35a" }}>{t("trades.partial_badge")}</span>;
}
function DirText({ t, dir }) {
  return <span style={{ fontSize: "var(--fs-micro)", fontWeight: 700, color: dir === "long" ? "var(--profit)" : "var(--loss)" }}>{dir === "long" ? "▲ " + t("filters.direction_long") : "▼ " + t("filters.direction_short")}</span>;
}
function fmtR(r) { return r === null || r === undefined ? "—" : (r >= 0 ? "+" : "−") + Math.abs(r).toFixed(2) + "R"; }

/* ===========================================================================
   Wave 2: Trade log filter (FilterBar · FilterSheet · ActiveFilterChips · ResultSummary · ExcludedNotice)
   ทุก dim → query string · back/forward re-parse (ผ่าน patchQuery + useRoute)
   =========================================================================== */
function filterDims(t, opts) {
  const all = {
    symbol: { label: t("filters.dim_symbol"), values: (opts.symbols || []).map((s) => [s, s]) },
    direction: { label: t("filters.dim_direction"), values: [["long", t("filters.direction_long")], ["short", t("filters.direction_short")]] },
    session: { label: t("filters.dim_session"), values: (opts.sessions || []).map((s) => [s.key, t("session." + s.key)]) },
    outcome: { label: t("filters.dim_outcome"), values: [["win", t("filters.outcome_win")], ["loss", t("filters.outcome_loss")], ["be", t("filters.outcome_be")]] },
    origin: { label: t("filters.dim_origin"), values: [["ea", t("filters.origin_ea")], ["manual", t("filters.origin_manual")]] },
    sort: { label: t("filters.dim_sort"), values: [["-closed_at", t("filters.sort_recent")], ["closed_at", t("filters.sort_oldest")], ["-net_cents", t("filters.sort_pnl_high")], ["net_cents", t("filters.sort_pnl_low")]] },
  };
  const enabled = opts.dims_enabled || Object.keys(all);
  const out = {};
  ["symbol", "direction", "session", "outcome", "origin", "sort"].forEach((k) => { if (enabled.indexOf(k) >= 0) out[k] = all[k]; });
  return out;
}
const FILTER_KEYS = ["symbol", "direction", "session", "outcome", "origin"];   // sort ไม่นับเป็น active chip
function activeCount(q) { let n = 0; FILTER_KEYS.forEach((k) => { if (q[k]) n++; }); return n; }

/* desktop inline chip ต่อ dim */
function DimChip({ t, dimKey, g, sel, onPick }) {
  const [open, setOpen] = useState(false);
  const cur = g.values.find((v) => v[0] === sel);
  return <Dropdown label={cur ? cur[1] : g.label} dot={!!sel && dimKey !== "sort"} open={open} onToggle={() => setOpen((o) => !o)}>
    {dimKey !== "sort" && <MenuItem selected={!sel} onClick={() => { setOpen(false); onPick(null); }}>{t("filters.any")}</MenuItem>}
    {g.values.map((v) => <MenuItem key={v[0]} selected={sel === v[0]} onClick={() => { setOpen(false); onPick(v[0]); }}>{v[1]}</MenuItem>)}
  </Dropdown>;
}

function FilterSheet({ t, open, onClose, dims, q }) {
  const [draft, setDraft] = useState({});
  useEffect(() => {
    if (open) { const d = {}; Object.keys(dims).forEach((k) => { d[k] = q[k] || ""; }); setDraft(d); }
  }, [open]);
  const setD = (k, v) => setDraft((o) => Object.assign({}, o, { [k]: v }));
  const apply = () => { const patch = { cursor: null }; Object.keys(dims).forEach((k) => { patch[k] = draft[k] || null; }); patchQuery(patch); onClose(); };
  const clear = () => { const d = {}; Object.keys(dims).forEach((k) => { d[k] = ""; }); setDraft(d); };
  return <BottomSheet open={open} title={t("filters.sheet_title")} onClose={onClose}>
    {Object.keys(dims).map((dk) => { const g = dims[dk];
      return <div key={dk} style={{ marginBottom: "var(--sp-4)" }}>
        <div style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)", marginBottom: 6 }}>{g.label}</div>
        <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
          {dk !== "sort" && <button className={!draft[dk] ? "chip-on" : "chip"} onClick={() => setD(dk, "")} style={{ minHeight: 40 }}>{t("filters.any")}</button>}
          {g.values.map((v) => <button key={v[0]} className={draft[dk] === v[0] ? "chip-on" : "chip"} onClick={() => setD(dk, v[0])} style={{ minHeight: 40 }}>{v[1]}</button>)}
        </div>
      </div>;
    })}
    <div style={{ display: "flex", gap: "var(--sp-3)", marginTop: "var(--sp-4)" }}>
      <button className="btn-secondary" onClick={clear} style={{ flex: "0 0 auto" }}>{t("filters.clear_all")}</button>
      <button className="btn-brand" onClick={apply} style={{ flex: 1 }}>{t("filters.apply")}</button>
    </div>
  </BottomSheet>;
}

function ActiveFilterChips({ t, q }) {
  const chips = [];
  if (q.symbol) chips.push(["symbol", q.symbol]);
  if (q.direction) chips.push(["direction", t("filters.direction_" + q.direction)]);
  if (q.session) chips.push(["session", t("session." + q.session)]);
  if (q.outcome) chips.push(["outcome", t("filters.outcome_" + q.outcome)]);
  if (q.origin) chips.push(["origin", t("filters.origin_" + q.origin)]);
  if (!chips.length) return null;
  const clearAll = () => patchQuery({ symbol: null, direction: null, session: null, outcome: null, origin: null, cursor: null });
  return <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginBottom: "var(--sp-3)" }}>
    {chips.map(([k, label]) => <span key={k} style={{ display: "inline-flex", alignItems: "center", gap: 6, minHeight: 32, padding: "0 6px 0 var(--sp-3)", borderRadius: "var(--radius-full)", fontSize: "var(--fs-caption)", background: "var(--brand-soft)", color: "var(--brand-bright)", fontWeight: 600 }}>
      {label}<button aria-label={t("common.close")} onClick={() => patchQuery({ [k]: null, cursor: null })} style={{ border: "none", background: "transparent", color: "var(--brand-bright)", cursor: "pointer", fontSize: 15, lineHeight: 1, minHeight: 28, padding: "0 2px" }}>×</button>
    </span>)}
    <button className="btn-ghost" onClick={clearAll} style={{ minHeight: 32, fontSize: "var(--fs-caption)" }}>{t("filters.clear_all")}</button>
  </div>;
}

function TradeCard({ t, tr, onOpen }) {
  const net = tr.net_cents;
  return <button onClick={onOpen} style={{ display: "block", width: "100%", textAlign: "left", border: "1px solid var(--hairline)", borderRadius: "var(--radius)", background: "var(--surface-1)", cursor: "pointer", padding: "var(--sp-3)", minHeight: 64 }}>
    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", minWidth: 0 }}>
        <span className="disp" style={{ fontSize: "var(--fs-body)", fontWeight: 700 }}>{tr.symbol}</span>
        <DirText t={t} dir={tr.direction} />
        <span className="num" style={{ fontSize: "var(--fs-micro)", color: "var(--ink-mute)" }}>{tr.lots.toFixed(2)} lot</span>
      </div>
      <div style={{ textAlign: "right", flexShrink: 0 }}>
        {tr.status === "open" ? <span style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)" }}>{t("trades.status_open")}</span> : <span className="num" style={{ fontWeight: 700 }}><MoneyText cents={net} currency={tr.currency} /></span>}
      </div>
    </div>
    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 6, gap: 8 }}>
      <div style={{ display: "flex", gap: 6, alignItems: "center", flexWrap: "wrap" }}>
        <OriginBadge t={t} origin={tr.origin} />
        {tr.is_partial && <PartialBadge t={t} />}
        <span className="num" style={{ fontSize: "var(--fs-micro)", color: "var(--ink-subtle)" }}>{t("session." + tr.session)}</span>
      </div>
      <span className="num" style={{ fontSize: "var(--fs-micro)", color: "var(--ink-mute)" }}>{fmtR(tr.r_multiple)}</span>
    </div>
  </button>;
}

/* ===========================================================================
   Wave 2.5: Trades KPI strip — 4 ใบเหนือ list · เลขจาก /trades/summary (สะท้อน filter)
   ทุกค่าจาก mock (client format/วาดเท่านั้น) · re-fetch เมื่อ filter/qkey เปลี่ยน
   =========================================================================== */
/* donut 2 arc (PF) — เขียว=gross win · แดง=gross loss */
function PFDonut({ win, loss }) {
  const tot = win + loss || 1;
  const r = 15, c = 2 * Math.PI * r, gw = c * (win / tot);
  return <svg viewBox="0 0 40 40" style={{ width: 44, height: 44, flex: "0 0 auto" }} aria-hidden="true">
    <circle cx="20" cy="20" r={r} fill="none" stroke="var(--loss)" strokeWidth="6" opacity=".85" />
    <circle cx="20" cy="20" r={r} fill="none" stroke="var(--profit)" strokeWidth="6"
      strokeDasharray={gw + " " + (c - gw)} strokeDashoffset={c * 0.25} transform="rotate(-90 20 20)" strokeLinecap="butt" />
  </svg>;
}
/* half-gauge (win%) — semicircle · เขียว fill ตาม rate */
function WinGauge({ rate }) {
  const r = rate == null ? 0 : Math.max(0, Math.min(1, rate));
  const R = 22, cx = 26, cy = 26;
  const p = (frac) => { const ang = Math.PI - Math.PI * frac; return [cx + R * Math.cos(ang), cy - R * Math.sin(ang)]; };
  const s = p(0), e = p(r), bgEnd = p(1);
  return <svg viewBox="0 0 52 30" style={{ width: 56, height: 32, flex: "0 0 auto" }} aria-hidden="true">
    <path d={"M" + s[0] + "," + s[1] + " A" + R + "," + R + " 0 0 1 " + bgEnd[0] + "," + bgEnd[1]} fill="none" stroke="var(--surface-3)" strokeWidth="5" strokeLinecap="round" />
    {r > 0 && <path d={"M" + s[0] + "," + s[1] + " A" + R + "," + R + " 0 0 1 " + e[0] + "," + e[1]} fill="none" stroke="var(--profit)" strokeWidth="5" strokeLinecap="round" />}
  </svg>;
}
function KpiPill({ n, kind, label }) {
  const map = { W: "var(--profit)", BE: "#3b82f6", L: "var(--loss)" };
  const bg = { W: "#e7f8f0", BE: "#eef1fe", L: "#fdecef" };
  return <span className="kpi-pill num" style={{ background: bg[kind], color: map[kind] }} title={label}>{kind} {n}</span>;
}
function TradesKPIStrip({ t, qkey }) {
  const [s, setS] = useState(null);
  const [phase, setPhase] = useState("loading");   // loading | ready | error
  useEffect(() => {
    let dead = false; setPhase("loading");
    const sp = new URLSearchParams();
    ["symbol", "direction", "session", "outcome", "origin", "status", "account_id"].forEach((k) => { const v = getQuery().get(k); if (v) sp.set(k, v); });
    const range = getQuery().get("range") || "30d";
    const df = rangeToDateFrom(range); if (df) sp.set("date_from", df);
    window.MTOS_API.get("/trades/summary?" + sp.toString())
      .then((r) => { if (!dead) { setS(r.data); setPhase("ready"); } })
      .catch(() => { if (!dead) setPhase("error"); });
    return () => { dead = true; };
  }, [qkey]);

  if (phase === "error") return null;
  if (phase === "loading" || !s) return <div className="kpi-strip">
    {[0, 1, 2, 3].map((i) => <div key={i} className="kpi-card"><div style={{ height: 64 }}><Skeleton /></div></div>)}
  </div>;

  const gw = s.gross_win_cents || 0, gl = Math.abs(s.gross_loss_cents || 0);
  const pfText = s.pf_flag === "no_losses" ? "∞" : s.pf_flag === "no_trades" ? "—" : fmtNum(s.profit_factor);
  const aw = s.avg_win_cents || 0, al = Math.abs(s.avg_loss_cents || 0);
  const payoff = al ? aw / al : null;
  const barTot = aw + al || 1;
  const netColor = s.net_pnl_cents > 0 ? "var(--profit)" : s.net_pnl_cents < 0 ? "var(--loss)" : "var(--ink)";

  return <div className="kpi-strip">
    {/* 1 · Net cumulative P&L */}
    <div className="kpi-card">
      <div className="kpi-label">{t("trades.kpi.net")}</div>
      <div className="kpi-value num" style={{ color: netColor }}>{fmtMoney(s.net_pnl_cents, s.currency)}</div>
      <div className="num" style={{ fontSize: "var(--fs-micro)", color: "var(--ink-mute)", marginTop: 6 }}>{t("trades.kpi.trade_count", { n: s.trades_closed })}</div>
    </div>
    {/* 2 · Profit factor + donut */}
    <div className="kpi-card">
      <div className="kpi-label">{t("trades.kpi.pf")}</div>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
        <div className="kpi-value num">{pfText}</div>
        <PFDonut win={gw} loss={gl} />
      </div>
    </div>
    {/* 3 · Win % + half gauge + pills */}
    <div className="kpi-card">
      <div className="kpi-label">{t("trades.kpi.winrate")}</div>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
        <div className="kpi-value num">{fmtPct(s.win_rate)}</div>
        <WinGauge rate={s.win_rate} />
      </div>
      <div style={{ display: "flex", gap: 5, marginTop: 8, flexWrap: "wrap" }}>
        <KpiPill kind="W" n={s.wins} label={t("filters.outcome_win")} />
        <KpiPill kind="BE" n={s.breakeven} label={t("filters.outcome_be")} />
        <KpiPill kind="L" n={s.losses} label={t("filters.outcome_loss")} />
      </div>
    </div>
    {/* 4 · Avg win/loss + proportion bar */}
    <div className="kpi-card">
      <div className="kpi-label">{t("trades.kpi.avgwl")}</div>
      <div className="kpi-value num">{payoff == null ? "—" : fmtNum(payoff)}</div>
      <div style={{ display: "flex", height: 7, borderRadius: 999, overflow: "hidden", marginTop: 10, background: "var(--surface-3)" }}>
        <div style={{ width: (aw / barTot * 100) + "%", background: "var(--profit)" }} />
        <div style={{ width: (al / barTot * 100) + "%", background: "var(--loss)" }} />
      </div>
      <div style={{ display: "flex", justifyContent: "space-between", marginTop: 5 }}>
        <span className="num" style={{ fontSize: "var(--fs-micro)", color: "var(--profit)", fontWeight: 700 }}>{s.avg_win_cents == null ? "—" : fmtMoney(s.avg_win_cents, s.currency)}</span>
        <span className="num" style={{ fontSize: "var(--fs-micro)", color: "var(--loss)", fontWeight: 700 }}>{s.avg_loss_cents == null ? "—" : fmtMoney(s.avg_loss_cents, s.currency)}</span>
      </div>
    </div>
  </div>;
}

/* ===========================================================================
   Wave 2: TradesPage — มือถือ การ์ด+infinite scroll · desktop ตาราง+hover row
   =========================================================================== */
function TradesPage({ t, toast, rev }) {
  const [devState, setDevState] = useState("ready");
  const [opts, setOpts] = useState({ symbols: [], sessions: [], dims_enabled: [] });
  const [rows, setRows] = useState([]);
  const [meta, setMeta] = useState(null);
  const [phase, setPhase] = useState("loading");   // loading|ready|empty_none|empty_filter|error|locked
  const [loadingMore, setLoadingMore] = useState(false);
  const [errCode, setErrCode] = useState(null);
  const [sheetOpen, setSheetOpen] = useState(false);
  const sentinel = useRef(null);
  const q = getQuery();
  const acct = q.get("account_id") || "";
  const range = q.get("range") || "30d";
  const qkey = ["symbol", "direction", "session", "outcome", "origin", "sort"].map((k) => k + "=" + (q.get(k) || "")).join("&") + "|acct=" + acct + "|range=" + range;
  const filtersActive = activeCount({ symbol: q.get("symbol"), direction: q.get("direction"), session: q.get("session"), outcome: q.get("outcome"), origin: q.get("origin") }) > 0;

  useEffect(() => { window.MTOS_API.get("/filter-options").then((r) => setOpts(r.data)).catch(() => {}); }, []);

  const buildQuery = useCallback((cursor) => {
    const sp = new URLSearchParams();
    ["symbol", "direction", "session", "outcome", "origin", "sort", "r_min", "r_max", "status"].forEach((k) => { const v = getQuery().get(k); if (v) sp.set(k, v); });
    if (acct) sp.set("account_id", acct);
    const df = rangeToDateFrom(range); if (df) sp.set("date_from", df);
    sp.set("limit", "12");
    if (cursor) sp.set("cursor", cursor);
    return "/trades?" + sp.toString();
  }, [acct, range]);

  const load = useCallback((sim) => {
    setPhase("loading"); setRows([]); setMeta(null);
    if (sim === "loading") return;
    if (sim === "locked") {
      window.MTOS_API.get("/trades?limit=3").then((r) => { setRows(r.data); setMeta(r.meta); setPhase("locked"); }).catch(() => { setErrCode("error"); setPhase("error"); });
      return;
    }
    const opt = sim === "error" ? { simulate: "error" } : sim === "empty" ? { simulate: "empty" } : {};
    window.MTOS_API.get(buildQuery(null), opt).then((r) => {
      setRows(r.data); setMeta(r.meta);
      if (!r.data.length) setPhase(filtersActive ? "empty_filter" : "empty_none");
      else setPhase("ready");
    }).catch((e) => { setErrCode((e.envelope && e.envelope.error && e.envelope.error.code) || "error"); setPhase("error"); });
  }, [buildQuery, filtersActive]);

  useEffect(() => { load(devState === "ready" ? null : devState); }, [devState, qkey, rev]);

  const loadMore = useCallback(() => {
    if (!meta || !meta.next_cursor || loadingMore) return;
    setLoadingMore(true);
    window.MTOS_API.get(buildQuery(meta.next_cursor)).then((r) => {
      setRows((prev) => prev.concat(r.data)); setMeta(r.meta); setLoadingMore(false);
    }).catch(() => setLoadingMore(false));
  }, [meta, loadingMore, buildQuery]);

  // infinite scroll (IntersectionObserver)
  useEffect(() => {
    if (phase !== "ready") return;
    const el = sentinel.current; if (!el) return;
    const io = new IntersectionObserver((ents) => { if (ents[0].isIntersecting) loadMore(); }, { rootMargin: "200px" });
    io.observe(el);
    return () => io.disconnect();
  }, [phase, loadMore]);

  const dims = filterDims(t, opts);
  const openTrade = (id) => navigate("/app/trades/" + id);

  const listMobile = <div className="only-mobile" style={{ flexDirection: "column", gap: "var(--sp-2)" }}>
    {rows.map((tr) => <TradeCard key={tr.trd_id} t={t} tr={tr} onOpen={() => openTrade(tr.trd_id)} />)}
  </div>;
  const listDesktop = <div className="only-desktop"><table className="trades-table"><thead><tr>
    <th>{t("trades.col_symbol")}</th><th>{t("trades.col_direction")}</th><th style={{ textAlign: "right" }}>{t("trades.col_lots")}</th>
    <th style={{ textAlign: "right" }}>{t("trades.col_pnl")}</th><th style={{ textAlign: "right" }}>{t("trades.col_r")}</th><th>{t("trades.col_time")}</th>
  </tr></thead><tbody>
    {rows.map((tr) => <tr key={tr.trd_id} onClick={() => openTrade(tr.trd_id)}>
      <td><span style={{ display: "inline-flex", gap: 6, alignItems: "center" }}><span className="disp" style={{ fontWeight: 700 }}>{tr.symbol}</span><OriginBadge t={t} origin={tr.origin} />{tr.is_partial && <PartialBadge t={t} />}</span></td>
      <td><DirText t={t} dir={tr.direction} /></td>
      <td className="num" style={{ textAlign: "right" }}>{tr.lots.toFixed(2)}</td>
      <td style={{ textAlign: "right" }}>{tr.status === "open" ? <span style={{ color: "var(--ink-mute)" }}>{t("trades.status_open")}</span> : <MoneyText cents={tr.net_cents} currency={tr.currency} />}</td>
      <td className="num" style={{ textAlign: "right" }}>{fmtR(tr.r_multiple)}</td>
      <td className="num" style={{ color: "var(--ink-mute)" }}>{fmtTime(tr.closed_at || tr.opened_at)}</td>
    </tr>)}
  </tbody></table></div>;

  return <div>
    <PageHeader t={t} page="trades" />
    <StateSwitcher t={t} value={devState} onChange={setDevState} />

    {/* FilterBar sticky */}
    <div className="filterbar">
      <PageFilterBar t={t} />
      <div className="only-mobile" style={{ gap: 8 }}>
        <button className="btn-secondary" onClick={() => setSheetOpen(true)} style={{ minHeight: 40, padding: "0 var(--sp-3)", fontSize: "var(--fs-caption)" }}>
          ⚙ {t("trades.filter_btn")}{activeCount({ symbol: q.get("symbol"), direction: q.get("direction"), session: q.get("session"), outcome: q.get("outcome"), origin: q.get("origin") }) > 0 && <span style={{ marginLeft: 6, background: "var(--brand-bright)", color: "#fff", borderRadius: 999, fontSize: 11, padding: "0 6px", fontWeight: 700 }}>{activeCount({ symbol: q.get("symbol"), direction: q.get("direction"), session: q.get("session"), outcome: q.get("outcome"), origin: q.get("origin") })}</span>}
        </button>
      </div>
      <div className="only-desktop">
        <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
          {Object.keys(dims).map((dk) => <DimChip key={dk} t={t} dimKey={dk} g={dims[dk]} sel={q.get(dk) || ""} onPick={(v) => patchQuery({ [dk]: v, cursor: null })} />)}
        </div>
      </div>
    </div>

    <ActiveFilterChips t={t} q={{ symbol: q.get("symbol"), direction: q.get("direction"), session: q.get("session"), outcome: q.get("outcome"), origin: q.get("origin") }} />

    {/* KPI strip — สะท้อน filter slice (ซ่อนตอน error / locked / ยังไม่มีไม้เลย) */}
    {(phase === "ready" || phase === "loading" || phase === "empty_filter") && <TradesKPIStrip t={t} qkey={qkey} />}

    {phase === "loading" && <Skeleton variant="list" />}
    {phase === "error" && <ErrorState t={t} code={errCode} onRetry={() => load(null)} />}
    {phase === "empty_none" && <EmptyState t={t} title={t("trades.empty_none_title")} body={t("trades.empty_none_body")} cta={t("trades.empty_none_cta")} onCta={() => patchQuery({ quickadd: "1" })} />}
    {phase === "empty_filter" && <EmptyState t={t} title={t("trades.empty_filter_title")} body={t("trades.empty_filter_body")} cta={t("trades.clear_filter")} onCta={() => patchQuery({ symbol: null, direction: null, session: null, outcome: null, origin: null, cursor: null })} />}

    {phase === "locked" && <div style={{ display: "flex", flexDirection: "column", gap: "var(--sp-4)" }}>
      <FirstSyncProgress t={t} minutes={6} progress={0.35} />
      <div style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)" }}>{t("trades.sample_hint")}</div>
      <LockedSampleCard t={t}><div style={{ display: "flex", flexDirection: "column", gap: "var(--sp-2)" }}>{rows.map((tr) => <TradeCard key={tr.trd_id} t={t} tr={tr} onOpen={() => {}} />)}</div></LockedSampleCard>
    </div>}

    {phase === "ready" && <div>
      <div data-testid="result-summary" style={{ fontSize: "var(--fs-caption)", color: "var(--ink-dim)", marginBottom: "var(--sp-2)" }}>{meta ? t("trades.result_summary", { count: meta.result_count, accounts: meta.account_count }) : ""}</div>
      {meta && meta.excluded_null_r > 0 && <div data-testid="excluded-notice" style={{ fontSize: "var(--fs-micro)", color: "#c8a35a", marginBottom: "var(--sp-2)" }}>⚠ {t("trades.excluded_notice", { n: meta.excluded_null_r })}</div>}
      {listMobile}
      {listDesktop}
      <div ref={sentinel} style={{ height: 1 }} />
      <div style={{ textAlign: "center", padding: "var(--sp-4)", fontSize: "var(--fs-caption)", color: "var(--ink-mute)" }}>
        {loadingMore ? t("trades.loading_more") : meta && meta.next_cursor ? <button className="btn-secondary" onClick={loadMore} style={{ minHeight: 40 }}>{t("trades.load_more")}</button> : t("trades.end_of_list")}
      </div>
    </div>}

    <FilterSheet t={t} open={sheetOpen} onClose={() => setSheetOpen(false)} dims={dims} q={{ symbol: q.get("symbol"), direction: q.get("direction"), session: q.get("session"), outcome: q.get("outcome"), origin: q.get("origin"), sort: q.get("sort") }} />
  </div>;
}
function rangeToDateFrom(range) {
  if (!range || range === "all") return null;
  const days = range === "7d" ? 7 : range === "90d" ? 90 : 30;
  const now = new Date();
  const base = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
  base.setUTCDate(base.getUTCDate() - days);
  return base.toISOString().slice(0, 10);
}
/* querystring กลางสำหรับ analytics calls — แนบ account_id + date range จาก URL (P0-3) */
function analyticsQS(acct, range) {
  const sp = new URLSearchParams();
  if (acct) sp.set("account_id", acct);
  const df = rangeToDateFrom(range); if (df) sp.set("date_from", df);
  const qs = sp.toString();
  return qs ? "?" + qs : "";
}

/* ===========================================================================
   Wave 2: TradeDetailPage — header + EmotionPicker + TradeNoteBox + TagPicker + legs + AI placeholder
   =========================================================================== */
function TradeDetailPage({ t, id, toast }) {
  const [phase, setPhase] = useState("loading");   // loading|ready|notfound|error
  const [tr, setTr] = useState(null);
  const [journal, setJournal] = useState({ emotion: null, tag_ids: [], note: { body: "" } });
  const [tagOptions, setTagOptions] = useState([]);

  useEffect(() => {
    let dead = false; setPhase("loading");
    window.MTOS_API.get("/trades/" + id).then((r) => {
      if (dead) return; setTr(r.data); setJournal(r.data.journal || { emotion: null, tag_ids: [], note: { body: "" } }); setPhase("ready");
    }).catch((e) => { if (dead) return; setPhase(e.status === 404 ? "notfound" : "error"); });
    window.MTOS_API.get("/tags").then((r) => { if (!dead) setTagOptions(r.data); }).catch(() => {});
    return () => { dead = true; };
  }, [id]);

  const curEmotion = journal && journal.emotion ? journal.emotion.emotion : null;
  const setEmotion = (k) => {
    const prev = journal.emotion;
    setJournal((j) => Object.assign({}, j, { emotion: k ? { emotion: k } : null }));   // optimistic
    window.MTOS_API.put("/trades/" + id + "/journal", { emotion: k }).catch(() => { setJournal((j) => Object.assign({}, j, { emotion: prev })); toast(t("states.error_title")); });
  };
  const toggleTag = (tid) => {
    const cur = journal.tag_ids || [];
    const next = cur.indexOf(tid) >= 0 ? cur.filter((x) => x !== tid) : cur.concat([tid]);
    setJournal((j) => Object.assign({}, j, { tag_ids: next }));
    window.MTOS_API.put("/trades/" + id + "/journal", { tag_ids: next }).catch(() => {});
  };
  const createTag = (name) => {
    window.MTOS_API.post("/tags", { name: name }).then((r) => { setTagOptions((o) => o.concat([r.data])); toggleTag(r.data.public_id); }).catch(() => {});
  };

  if (phase === "loading") return <div><div style={{ height: 120 }}><Skeleton /></div><div style={{ height: 200, marginTop: 16 }}><Skeleton /></div></div>;
  if (phase === "error") return <ErrorState t={t} code="trade_load" onRetry={() => navigate("/app/trades/" + id)} />;
  if (phase === "notfound") return <div style={{ textAlign: "center", padding: "var(--sp-8) var(--sp-4)" }}>
    <div className="disp" style={{ fontSize: 44, color: "var(--brand-bright)", marginBottom: "var(--sp-3)" }}>404</div>
    <div className="disp" style={{ fontSize: "var(--fs-h2)", marginBottom: "var(--sp-2)" }}>{t("journal.not_found_title")}</div>
    <div style={{ color: "var(--ink-dim)", marginBottom: "var(--sp-5)" }}>{t("journal.not_found_body")}</div>
    <button className="btn-brand" onClick={() => navigate("/app/trades")}>{t("journal.back_to_trades")}</button>
  </div>;

  const isOpen = tr.status === "open";
  const header = <div className="a-header card">
    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 12, flexWrap: "wrap" }}>
      <div>
        <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
          <span className="disp" style={{ fontSize: "var(--fs-h1)" }}>{tr.symbol}</span>
          <DirText t={t} dir={tr.direction} />
          <OriginBadge t={t} origin={tr.origin} />
          {tr.is_partial && <PartialBadge t={t} />}
          {isOpen && <span style={{ fontSize: "var(--fs-micro)", color: "var(--ink-mute)" }}>{t("trades.status_open")}</span>}
        </div>
        <div className="num" style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)", marginTop: 4 }}>{tr.lots.toFixed(2)} lot · {t("session." + tr.session)}</div>
      </div>
      <div style={{ textAlign: "right" }}>
        <div className="disp num" style={{ fontSize: "var(--fs-h1)" }}>{isOpen ? "—" : <MoneyText cents={tr.net_cents} currency={tr.currency} />}</div>
        <div className="num" style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)" }}>{fmtR(tr.r_multiple)}{tr.r_multiple === null && !isOpen ? " · " + t("journal.r_null_hint") : ""}</div>
      </div>
    </div>
  </div>;

  const legs = <div className="a-legs card">
    <div style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)", marginBottom: "var(--sp-3)" }}>{t("journal.legs_title")}</div>
    <div style={{ display: "flex", flexDirection: "column", gap: "var(--sp-2)" }}>
      {(tr.legs || []).map((lg, i) => <div key={i} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "6px 0", borderBottom: i < tr.legs.length - 1 ? "1px solid var(--hairline)" : "none" }}>
        <span style={{ fontSize: "var(--fs-caption)", fontWeight: 600, color: lg.kind === "entry" ? "var(--brand-bright)" : "var(--ink-dim)" }}>{lg.kind === "entry" ? t("journal.leg_entry") : t("journal.leg_exit")}</span>
        <span className="num" style={{ fontSize: "var(--fs-caption)" }}>{lg.price} · {lg.lots.toFixed(2)} lot</span>
        <span className="num" style={{ fontSize: "var(--fs-micro)", color: "var(--ink-mute)" }}>{fmtTime(lg.at)}</span>
      </div>)}
    </div>
    {tr.sl_price && <div style={{ marginTop: "var(--sp-3)", fontSize: "var(--fs-micro)", color: "var(--ink-mute)" }}>{t("journal.f_sl")}: <span className="num">{tr.sl_price}</span></div>}
  </div>;

  const ai = <div className="a-ai card" style={{ textAlign: "center", color: "var(--ink-mute)" }}>
    <div style={{ fontSize: 22, opacity: .5, marginBottom: 6 }}>✦</div>
    <div style={{ fontSize: "var(--fs-caption)", color: "var(--ink-dim)", fontWeight: 600 }}>{t("journal.ai_title")}</div>
    <div style={{ fontSize: "var(--fs-micro)" }}>{t("journal.ai_coming_soon")}</div>
  </div>;

  return <div>
    <button className="btn-ghost" onClick={() => navigate("/app/trades")} style={{ marginBottom: "var(--sp-3)", fontSize: "var(--fs-caption)", padding: 0 }}>‹ {t("journal.back_to_trades")}</button>
    <div className="detail-grid">
      {header}
      <div className="a-rail">
        <div className="card">
          <div style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)", marginBottom: "var(--sp-2)" }}>{t("journal.emotion_title")}</div>
          <EmotionPicker t={t} value={curEmotion} onPick={setEmotion} />
          <div style={{ fontSize: "var(--fs-micro)", color: "var(--ink-subtle)", marginTop: 6 }}>{t("journal.emotion_hint")}</div>
        </div>
        <div className="card"><TradeNoteBox t={t} tradeId={id} initial={journal && journal.note ? journal.note.body : ""} /></div>
        <div className="card"><TagPicker t={t} options={tagOptions} selected={journal.tag_ids} onToggle={toggleTag} onCreate={createTag} /></div>
      </div>
      {legs}
      {ai}
    </div>
  </div>;
}

/* ===========================================================================
   Wave 2: QuickAddSheet — ≤6 field · validation ไทย · หลังบันทึก → EmotionPicker ต่อ (ปิด loop)
   =========================================================================== */
function QuickAddSheet({ t, toast, onClose, onCreated }) {
  const [step, setStep] = useState("form");   // form | emotion
  const [newId, setNewId] = useState(null);
  const [emotion, setEmotionSel] = useState(null);
  const [f, setF] = useState({ symbol: "", direction: "long", lots: "", entry_price: "", entry_at: "", exit_price: "", exit_at: "", sl_price: "" });
  const [stillOpen, setStillOpen] = useState(false);
  const [err, setErr] = useState({});
  const [sug, setSug] = useState([]);
  const set = (k, v) => setF((o) => Object.assign({}, o, { [k]: v }));

  useEffect(() => {
    if (!f.symbol) { setSug([]); return; }
    let dead = false;
    window.MTOS_API.get("/symbols?q=" + encodeURIComponent(f.symbol)).then((r) => { if (!dead) setSug((r.data || []).filter((s) => s !== f.symbol.toUpperCase()).slice(0, 6)); }).catch(() => {});
    return () => { dead = true; };
  }, [f.symbol]);

  const validate = () => {
    const e = {};
    if (!f.symbol.trim()) e.symbol = t("quickadd.err_symbol");
    if (!(parseFloat(f.lots) > 0)) e.lots = t("quickadd.err_lots");
    if (!(parseFloat(f.entry_price) > 0)) e.entry_price = t("quickadd.err_entry_price");
    if (!f.entry_at) e.entry_at = t("quickadd.err_entry_time");
    if (!stillOpen && !(parseFloat(f.exit_price) > 0)) e.exit_price = t("quickadd.err_exit_price");
    setErr(e); return Object.keys(e).length === 0;
  };
  const save = () => {
    if (!validate()) return;
    const body = { symbol: f.symbol.toUpperCase(), direction: f.direction, lots: f.lots, entry_price: f.entry_price, entry_at: toIso(f.entry_at), sl_price: f.sl_price };
    if (!stillOpen) { body.exit_price = f.exit_price; body.exit_at = toIso(f.exit_at); }
    window.MTOS_API.post("/manual-entries", body).then((r) => {
      setNewId(r.data.trd_id); if (onCreated) onCreated(r.data.trd_id);
      toast(t("quickadd.saved_toast")); setStep("emotion");
    }).catch(() => toast(t("states.error_title")));
  };
  const pickEmotion = (k) => { setEmotionSel(k); if (newId) window.MTOS_API.put("/trades/" + newId + "/journal", { emotion: k }).catch(() => {}); };

  const field = (label, node, e) => <div style={{ marginBottom: "var(--sp-3)" }}>
    <label className="lbl" style={{ margin: "0 0 4px" }}>{label}</label>
    {node}
    {e && <div style={{ color: "var(--loss)", fontSize: "var(--fs-micro)", marginTop: 3 }}>{e}</div>}
  </div>;

  if (step === "emotion") return <div>
    <div className="disp" style={{ fontSize: "var(--fs-h3)", marginBottom: 4 }}>{t("quickadd.emotion_step_title")}</div>
    <div style={{ fontSize: "var(--fs-caption)", color: "var(--ink-mute)", marginBottom: "var(--sp-3)" }}>{t("quickadd.emotion_step_hint")}</div>
    <EmotionPicker t={t} value={emotion} onPick={pickEmotion} />
    <div style={{ display: "flex", gap: "var(--sp-3)", marginTop: "var(--sp-4)" }}>
      <button className="btn-secondary" onClick={onClose} style={{ flex: 1 }}>{t("quickadd.skip")}</button>
      <button className="btn-brand" onClick={() => { onClose(); if (newId) navigate("/app/trades/" + newId); }} style={{ flex: 1 }}>{t("quickadd.done")}</button>
    </div>
  </div>;

  return <div>
    {field(t("quickadd.f_symbol"), <div>
      <input className="inp" value={f.symbol} onChange={(e) => set("symbol", e.target.value)} placeholder={t("quickadd.f_symbol_ph")} autoComplete="off" />
      {sug.length > 0 && <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginTop: 6 }}>
        {sug.map((s) => <button key={s} className="chip" onClick={() => set("symbol", s)} style={{ minHeight: 30 }}>{s}</button>)}
      </div>}
    </div>, err.symbol)}

    {field(t("quickadd.f_direction"), <div style={{ display: "flex", gap: 8 }}>
      {[["long", t("quickadd.buy")], ["short", t("quickadd.sell")]].map(([v, lbl]) => <button key={v} onClick={() => set("direction", v)}
        className={f.direction === v ? "chip-on" : "chip"} style={{ flex: 1, minHeight: 44 }}>{lbl}</button>)}
    </div>)}

    {field(t("quickadd.f_lots"), <input className="inp" type="number" inputMode="decimal" step="0.01" value={f.lots} onChange={(e) => set("lots", e.target.value)} placeholder="0.50" />, err.lots)}

    <div style={{ display: "flex", gap: "var(--sp-3)" }}>
      <div style={{ flex: 1 }}>{field(t("quickadd.f_entry_price"), <input className="inp" type="number" inputMode="decimal" value={f.entry_price} onChange={(e) => set("entry_price", e.target.value)} placeholder="2400.00" />, err.entry_price)}</div>
      <div style={{ flex: 1 }}>{field(t("quickadd.f_entry_time"), <input className="inp" type="datetime-local" value={f.entry_at} onChange={(e) => set("entry_at", e.target.value)} />, err.entry_at)}</div>
    </div>

    <label style={{ display: "flex", alignItems: "center", gap: 8, margin: "0 0 var(--sp-3)", fontSize: "var(--fs-caption)", color: "var(--ink-dim)", cursor: "pointer", minHeight: 40 }}>
      <input type="checkbox" checked={stillOpen} onChange={(e) => setStillOpen(e.target.checked)} style={{ width: 18, height: 18 }} />{t("quickadd.still_open")}
    </label>

    {!stillOpen && <div style={{ display: "flex", gap: "var(--sp-3)" }}>
      <div style={{ flex: 1 }}>{field(t("quickadd.f_exit_price"), <input className="inp" type="number" inputMode="decimal" value={f.exit_price} onChange={(e) => set("exit_price", e.target.value)} placeholder="2412.50" />, err.exit_price)}</div>
      <div style={{ flex: 1 }}>{field(t("quickadd.f_exit_time"), <input className="inp" type="datetime-local" value={f.exit_at} onChange={(e) => set("exit_at", e.target.value)} />)}</div>
    </div>}

    {field(t("quickadd.f_sl"), <div>
      <input className="inp" type="number" inputMode="decimal" value={f.sl_price} onChange={(e) => set("sl_price", e.target.value)} placeholder="2392.00" />
      <div style={{ fontSize: "var(--fs-micro)", color: "var(--brand-bright)", marginTop: 3 }}>{t("quickadd.sl_hint")}</div>
    </div>)}

    <button className="btn-brand" onClick={save} style={{ width: "100%", marginTop: "var(--sp-3)" }}>{t("quickadd.save")}</button>
  </div>;
}
function toIso(local) { if (!local) return new Date().toISOString(); const d = new Date(local); return isNaN(d) ? new Date().toISOString() : d.toISOString(); }

/* ===========================================================================
   App root
   =========================================================================== */
function App() {
  const loc = useRoute();
  const [lang, setLang] = useState(getLang());
  const [cat, setCat] = useState(null);
  const [catErr, setCatErr] = useState(false);
  const [authed, setAuthed] = useState(Session.active());
  const [toastMsg, setToastMsg] = useState(null);
  const [listRev, setListRev] = useState(0);   // bump เพื่อบังคับ TradesPage refetch หลัง quick-add

  // i18n loader
  useEffect(() => {
    let dead = false;
    setCatErr(false);
    loadCatalog(lang).then((c) => { if (!dead) setCat(c); }).catch(() => { if (!dead) setCatErr(true); });
    return () => { dead = true; };
  }, [lang]);

  const t = useCallback((key, params) => {
    if (!cat) return key;
    let s = cat[key];
    if (s === undefined) { if (window.console) console.error("[i18n] missing key:", key); return key; }
    if (params) for (const p in params) s = s.split("{" + p + "}").join(params[p]);
    return s;
  }, [cat]);

  const onSetLang = (l) => { try { localStorage.setItem("mtos_lang", l); } catch (e) {} setLang(l); };
  const toast = (m) => setToastMsg(m);
  const onLogout = () => { Session.logout(); setAuthed(false); navigate("/", true); };
  const onLogin = () => { Session.login(); setAuthed(true); };
  const onDemo = () => { Session.startDemo(); setAuthed(true); navigate("/app", true); };

  if (catErr) return <div style={{ minHeight: "100dvh", display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column", gap: 12, color: "var(--ink-dim)" }}>
    <div>โหลดข้อความไม่สำเร็จ</div>
    <button className="btn-secondary" onClick={() => setLang(lang)}>ลองใหม่</button>
  </div>;
  if (!cat) return <div style={{ minHeight: "100dvh", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--ink-mute)" }}>…</div>;

  const pathname = window.location.pathname;
  const route = matchRoute(pathname);

  // landing (public) — มี session แล้วเข้า "/" → เด้งเข้าแอป
  if (route.name === "landing") {
    if (authed) { navigate("/app", true); return null; }
    return <LandingPage t={t} lang={lang} onSetLang={onSetLang} onDemo={onDemo} />;
  }

  // public routes
  if (route.name === "login" || route.name === "register") {
    return <React.Fragment>
      <AuthFormPage t={t} mode={route.name} onLogin={onLogin} />
      <Toast msg={toastMsg} onDone={() => setToastMsg(null)} />
    </React.Fragment>;
  }

  // SessionGuard
  if (!authed) { navigate("/login?next=" + encodeURIComponent(pathname + window.location.search), true); return null; }

  // onboarding = full-screen (นอก AppShell)
  if (route.name === "onboarding") {
    return <React.Fragment>
      <OnboardingPage t={t} lang={lang} onSetLang={onSetLang} />
      <Toast msg={toastMsg} onDone={() => setToastMsg(null)} />
    </React.Fragment>;
  }

  const qaOpen = getQuery().get("quickadd") === "1";
  const closeQa = () => patchQuery({ quickadd: null });

  let page;
  if (route.name === "dashboard") page = <DashboardPage t={t} toast={toast} />;
  else if (route.name === "calendar") page = <CalendarPage t={t} toast={toast} />;
  else if (route.name === "trades") page = <TradesPage t={t} toast={toast} rev={listRev} />;
  else if (route.name === "trade_detail") page = <TradeDetailPage t={t} id={route.params.id} toast={toast} />;
  else if (route.name === "connect") page = <ConnectPage t={t} toast={toast} />;
  else if (route.name === "import") page = <ImportPage t={t} toast={toast} />;
  else if (route.name === "insights") page = <InsightsPage t={t} toast={toast} />;
  else if (route.name === "unlock") page = <UnlockPage t={t} toast={toast} />;
  else if (route.name === "settings") page = <SettingsPage t={t} toast={toast} onLogout={onLogout} lang={lang} onSetLang={onSetLang} />;
  else if (route.name === "not_found") page = <NotFoundPage t={t} />;
  else page = <GenericPage t={t} page={route.name} toast={toast} />;

  return <React.Fragment>
    <AppShell t={t} active={route.name} lang={lang} onSetLang={onSetLang} onLogout={onLogout} onQuickAdd={() => patchQuery({ quickadd: "1" })}>
      {page}
    </AppShell>
    <BottomSheet open={qaOpen} title={t("quickadd.title")} onClose={closeQa}>
      {qaOpen && <QuickAddSheet t={t} toast={toast} onClose={closeQa} onCreated={() => setListRev((r) => r + 1)} />}
    </BottomSheet>
    <Toast msg={toastMsg} onDone={() => setToastMsg(null)} />
  </React.Fragment>;
}

/* render ย้ายไป pages-w3.jsx (โหลดหลัง app.jsx) — เพื่อให้ W3 page components พร้อมก่อน render แรก */
