/* ============================================================
   TRADESTER — trading coach & assistant (live API frontend)
   Ported from the mock-data prototype: all market data, broker
   actions and guardrail checks now go through /api/* (server.js).
   Policy + decision journal persist in localStorage.
   ============================================================ */

const { useState, useMemo, useEffect, useCallback } = React;

// ---------- Design tokens ----------
// Colors resolve from CSS variables (defined in index.html) so a single
// [data-theme="dark"] switch re-themes every inline style. Light values live
// in :root and are unchanged from the original fixed palette.
const T = {
  paper: "var(--paper)",
  card: "var(--card)",
  ink: "var(--ink)",
  inkSoft: "var(--ink-soft)",
  brand: "var(--brand)", // deep petrol — brand / headers
  onAccent: "var(--on-accent)", // text on brand/loss/chip fills
  gain: "var(--gain)",
  loss: "var(--loss)",
  caution: "var(--caution)",
  line: "var(--line)",
  chipOn: "var(--chip-on)",
  chipOff: "var(--chip-off)",
  noticeBg: "var(--notice-bg)",
  warnBg: "var(--warn-bg)",
  stampBg: "var(--stamp-bg)",
  shadow: "var(--shadow)",
};
const fontDisplay = "'Fraunces', Georgia, serif";
const fontBody = "'Inter', -apple-system, sans-serif";
const fontMono = "'IBM Plex Mono', 'SF Mono', monospace";

const SECTORS = ["Technology", "Healthcare", "Financials", "Energy", "Consumer", "Industrials"];
const RISK_UI = {
  Conservative: { maxPos: 8, maxSector: 30, volCeil: 18, cashBuffer: 20, blurb: "Capital preservation first. Low-vol, dividend-tilted." },
  Balanced: { maxPos: 12, maxSector: 40, volCeil: 28, cashBuffer: 10, blurb: "Growth with guardrails. Diversified, moderate volatility." },
  Aggressive: { maxPos: 18, maxSector: 55, volCeil: 45, cashBuffer: 5, blurb: "Maximum growth. Accepts deep drawdowns for upside." },
};

// ---------- API helpers ----------
async function api(path, body, method = "POST") {
  const res = await fetch(path, body && {
    method,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(data.error || `Request failed (${res.status})`);
  return data;
}

const store = {
  load(key, fallback) {
    try { return JSON.parse(localStorage.getItem(key)) ?? fallback; } catch { return fallback; }
  },
  save(key, value) { localStorage.setItem(key, JSON.stringify(value)); },
};

// ---------- Small UI atoms ----------
const fmt = (n) => "$" + Math.round(n).toLocaleString("en-US");
const pct = (x, dp = 0) => (x * 100).toFixed(dp) + "%";
const fmtBig = (n) => {
  if (n == null) return "—";
  const u = [["T", 1e12], ["B", 1e9], ["M", 1e6]].find(([, v]) => Math.abs(n) >= v);
  return u ? `$${(n / u[1]).toFixed(1)}${u[0]}` : fmt(n);
};

// Per-broker theming so live money never looks like the simulator
// Fixed dark header colors (not the themeable --brand, which brightens in dark
// mode) so all three headers stay dark with white text in both themes.
const MODE_THEME = {
  "simulator": { bg: "#123B4A", tag: "SIMULATOR" },
  "alpaca-paper": { bg: "#1D5A73", tag: "ALPACA PAPER" },
  "alpaca-live": { bg: "#7A2E24", tag: "⚠ LIVE MONEY" },
};

// Real company mark when the server resolved one (Clearbit, keyed off the
// issuer's own website — see getLogoUrl in server.js); falls back to a
// deterministic monogram for anything without a resolvable domain (most
// ETFs, some foreign listings) so the UI never shows a broken image.
function Logo({ symbol, size = 34, src }) {
  const [failed, setFailed] = useState(false);
  useEffect(() => setFailed(false), [src]); // re-try when a different logo is passed in

  if (src && !failed) {
    return (
      <img src={src} alt={symbol} width={size} height={size} onError={() => setFailed(true)}
        style={{
          width: size, height: size, borderRadius: 10, flexShrink: 0,
          objectFit: "contain", background: "#fff", border: `1px solid ${T.line}`, padding: size * 0.08,
        }} />
    );
  }
  let h = 0;
  for (const ch of symbol || "?") h = (h * 31 + ch.charCodeAt(0)) % 360;
  return (
    <span style={{
      width: size, height: size, borderRadius: 10, flexShrink: 0,
      display: "inline-flex", alignItems: "center", justifyContent: "center",
      background: `hsl(${h}, 42%, 34%)`, color: "#fff",
      fontFamily: fontMono, fontWeight: 700, fontSize: size * 0.38,
    }}>
      {(symbol || "?").slice(0, 2)}
    </span>
  );
}

const CHART_RANGES = ["1D", "1W", "1M", "YTD", "1Y", "MAX"];

function RangeTabs({ value, onChange, disabled }) {
  return (
    <div style={{ display: "flex", gap: 2 }}>
      {CHART_RANGES.map((r) => (
        <button key={r} onClick={() => onChange(r)} disabled={disabled}
          style={{
            padding: "4px 8px", borderRadius: 7, border: "none", cursor: disabled ? "default" : "pointer",
            fontFamily: fontMono, fontSize: 10.5, fontWeight: 700,
            background: value === r ? T.brand : "transparent",
            color: value === r ? T.onAccent : T.inkSoft,
          }}>
          {r}
        </button>
      ))}
    </div>
  );
}

function formatDateForRange(iso, range, full) {
  const d = new Date(iso);
  if (range === "1D") return d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
  if (range === "1W") return full
    ? d.toLocaleDateString("en-US", { weekday: "short", hour: "numeric" })
    : d.toLocaleDateString("en-US", { weekday: "short" });
  if (range === "1M") return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
  if (range === "YTD" || range === "1Y") return d.toLocaleDateString("en-US", full ? { month: "short", day: "numeric" } : { month: "short" });
  return d.toLocaleDateString("en-US", { month: "short", year: "2-digit" }); // MAX
}

function useElementWidth() {
  const ref = React.useRef(null);
  const [width, setWidth] = useState(0);
  useEffect(() => {
    if (!ref.current) return;
    const el = ref.current;
    const update = () => setWidth(el.clientWidth);
    update();
    const ro = new ResizeObserver(update);
    ro.observe(el);
    return () => ro.disconnect();
  }, []);
  return [ref, width];
}

/** Interactive line chart: real axes, hover/touch crosshair + tooltip, no dependency. */
// Resolve a --token to its computed hex — SVG presentation attributes (stroke=,
// fill=) don't accept var(), unlike CSS `style`. Re-reads on each render, so it
// picks up the current theme automatically.
function cssVar(name, fallback) {
  if (typeof getComputedStyle === "undefined") return fallback;
  return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
}

function InteractiveChart({ points, height = 160, range, formatY = fmt }) {
  const [containerRef, width] = useElementWidth();
  const [hover, setHover] = useState(null);

  const cLine = cssVar("--line", "#E1E7E3");
  const cInkSoft = cssVar("--ink-soft", "#5C6B64");
  const cCard = cssVar("--card", "#FFFFFF");
  const cGain = cssVar("--gain", "#177E4D");
  const cLoss = cssVar("--loss", "#B23A2E");

  const tooShort = !points || points.length < 2;

  const padL = 50, padR = 8, padT = 10, padB = 20;
  const plotW = Math.max(1, width - padL - padR);
  const plotH = height - padT - padB;

  let d = "", yTicks = [], xTickIdxs = [], color = cssVar("--brand", "#123B4A"), xAt = () => 0, yAt = () => 0;
  if (!tooShort && width) {
    const values = points.map((p) => p.v);
    let min = Math.min(...values), max = Math.max(...values);
    if (min === max) { min -= 1; max += 1; }
    const pad = (max - min) * 0.1;
    min -= pad; max += pad;

    xAt = (i) => padL + (plotW * i) / (points.length - 1);
    yAt = (v) => padT + plotH * (1 - (v - min) / (max - min));
    d = points.map((p, i) => `${i ? "L" : "M"}${xAt(i).toFixed(1)},${yAt(p.v).toFixed(1)}`).join("");
    color = points[points.length - 1].v >= points[0].v ? cGain : cLoss;
    yTicks = [max - (max - min) * 0.05, (min + max) / 2, min + (max - min) * 0.05];
    const xTickCount = width > 480 ? 5 : 3;
    xTickIdxs = Array.from({ length: xTickCount }, (_, i) => Math.round((i * (points.length - 1)) / (xTickCount - 1)));
  }

  function handleMove(clientX) {
    if (!containerRef.current || tooShort) return;
    const rect = containerRef.current.getBoundingClientRect();
    const idx = Math.round(((clientX - rect.left - padL) / plotW) * (points.length - 1));
    setHover(Math.max(0, Math.min(points.length - 1, idx)));
  }

  const hp = hover != null && !tooShort ? points[hover] : null;

  return (
    <div ref={containerRef} style={{ width: "100%", height, position: "relative" }}>
      {tooShort && width > 0 && (
        <div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%", color: T.inkSoft, fontSize: 12.5 }}>
          Not enough history yet for this range.
        </div>
      )}
      {!tooShort && width > 0 && (
        <React.Fragment>
          <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}
            onMouseMove={(e) => handleMove(e.clientX)}
            onMouseLeave={() => setHover(null)}
            onTouchStart={(e) => handleMove(e.touches[0].clientX)}
            onTouchMove={(e) => handleMove(e.touches[0].clientX)}
            onTouchEnd={() => setHover(null)}
            style={{ display: "block", touchAction: "pan-y" }}>
            {yTicks.map((v, i) => (
              <g key={i}>
                <line x1={padL} x2={width - padR} y1={yAt(v)} y2={yAt(v)} stroke={cLine} strokeWidth="1" />
                <text x={padL - 6} y={yAt(v) + 3} textAnchor="end" fontSize="9.5" fontFamily={fontMono} fill={cInkSoft}>{formatY(v)}</text>
              </g>
            ))}
            {xTickIdxs.map((idx) => (
              <text key={idx} x={xAt(idx)} y={height - 4} textAnchor="middle" fontSize="9.5" fontFamily={fontMono} fill={cInkSoft}>
                {formatDateForRange(points[idx].t, range)}
              </text>
            ))}
            <path d={`${d}L${xAt(points.length - 1).toFixed(1)},${(padT + plotH).toFixed(1)}L${xAt(0).toFixed(1)},${(padT + plotH).toFixed(1)}Z`} fill={color} opacity="0.08" />
            <path d={d} fill="none" stroke={color} strokeWidth="2" strokeLinejoin="round" />
            {hp && (
              <g>
                <line x1={xAt(hover)} x2={xAt(hover)} y1={padT} y2={padT + plotH} stroke={cInkSoft} strokeWidth="1" strokeDasharray="3,3" />
                <circle cx={xAt(hover)} cy={yAt(hp.v)} r="4" fill={color} stroke={cCard} strokeWidth="1.5" />
              </g>
            )}
          </svg>
          {hp && (
            <div style={{
              position: "absolute", top: 2, left: Math.min(Math.max(xAt(hover) - 55, 0), width - 116),
              background: T.ink, color: T.card, borderRadius: 8, padding: "6px 9px",
              fontSize: 11, fontFamily: fontMono, pointerEvents: "none", whiteSpace: "nowrap", zIndex: 2,
            }}>
              <div style={{ opacity: 0.65, fontSize: 9.5 }}>{formatDateForRange(hp.t, range, true)}</div>
              <div style={{ fontWeight: 700 }}>{formatY(hp.v)}</div>
            </div>
          )}
        </React.Fragment>
      )}
    </div>
  );
}

const Dot = ({ status }) => (
  <span style={{
    width: 10, height: 10, borderRadius: 99, flexShrink: 0, marginTop: 5,
    background: status === "pass" ? T.gain : status === "warn" ? T.caution : T.loss,
  }} />
);

/** Whether a journal entry has anything worth expanding — gates showing the chevron at all. */
function hasJournalDetail(j) {
  const m = j.meta;
  if (!m) return false;
  return Boolean(
    m.checks?.length || m.stock || m.policy || m.mandate || (m.from && m.to) ||
    m.balanceAfter != null || m.qty != null || m.price != null || m.fillPrice != null ||
    m.tp != null || m.sl != null || m.suggestedMax
  );
}

const planFieldRow = (label, from, to, fmtVal = (v) => v) => {
  const same = Array.isArray(from) ? JSON.stringify(from) === JSON.stringify(to) : from === to;
  if (same) return null;
  return (
    <div style={{ fontSize: 12, marginBottom: 3 }}>
      {label}: {fmtVal(from)} → <b>{fmtVal(to)}</b>
    </div>
  );
};

/** Everything captured about a journal entry at the moment it was written — guardrail
    checks, stock stats, the plan in effect, mandate rules, or a plan-revision diff. */
function JournalDetailPanel({ j }) {
  const m = j.meta;
  if (!m) return null;
  const orderBits = [];
  if (m.qty != null) orderBits.push(`${Number(m.qty).toFixed(4).replace(/\.?0+$/, "")} sh`);
  const px = m.price ?? m.fillPrice;
  if (px != null) orderBits.push(`@ $${Number(px).toFixed(2)}`);
  if (m.exitPrice != null) orderBits.push(`exit $${Number(m.exitPrice).toFixed(2)}`);
  if (m.tp != null) orderBits.push(`TP $${Number(m.tp).toFixed(2)}`);
  if (m.sl != null) orderBits.push(`SL $${Number(m.sl).toFixed(2)}`);

  return (
    <div style={{ marginTop: 10, paddingTop: 10, borderTop: `1px dashed ${T.line}` }}>
      {m.from && m.to && (
        <div style={{ marginBottom: 8 }}>
          {planFieldRow("Risk", m.from.risk, m.to.risk)}
          {planFieldRow("Sectors", m.from.sectors, m.to.sectors, (v) => v.join(", "))}
          {planFieldRow("Goal", m.from.goal, m.to.goal, (v) => `"${v || "—"}"`)}
          {planFieldRow("Target", m.from.targetAmount, m.to.targetAmount, (v) => (v ? fmt(v) : "none"))}
        </div>
      )}
      {m.balanceAfter != null && (
        <div style={{ fontSize: 12, color: T.inkSoft, marginBottom: 8 }}>Cash after: <b>{fmt(m.balanceAfter)}</b></div>
      )}
      {orderBits.length > 0 && (
        <div style={{ fontFamily: fontMono, fontSize: 11.5, color: T.inkSoft, marginBottom: 8 }}>{orderBits.join(" · ")}</div>
      )}
      {m.stock && (
        <div style={{ fontFamily: fontMono, fontSize: 11, color: T.inkSoft, marginBottom: 8 }}>
          {m.stock.sector} · vol {(m.stock.annVol * 100).toFixed(0)}%/yr
          {m.stock.beta != null ? ` · β ${m.stock.beta.toFixed(2)}` : ""}
          {m.stock.ret1m != null ? ` · 1m ${(m.stock.ret1m * 100).toFixed(1)}%` : ""}
          {m.stock.ret1y != null ? ` · 1y ${(m.stock.ret1y * 100).toFixed(1)}%` : ""}
        </div>
      )}
      {m.policy && (
        <div style={{ fontSize: 11.5, color: T.inkSoft, marginBottom: 8 }}>
          Under your <b>{m.policy.risk}</b> plan · {m.policy.sectors.join(", ")}
        </div>
      )}
      {m.mandate && (
        <div style={{ fontFamily: fontMono, fontSize: 11, color: T.inkSoft, marginBottom: 8 }}>
          cap {m.mandate.allocationPct != null ? pct(m.mandate.allocationPct) : fmt(m.mandate.allocationAbs)} ·
          {" "}clip {fmt(m.mandate.clipSize)} · dip {pct(m.mandate.buyDipPct)} · TP {pct(m.mandate.takeProfitPct)} ·
          {" "}SL {pct(m.mandate.stopLossPct)} · auto ≤ {fmt(m.mandate.autoLimit)}
        </div>
      )}
      {m.suggestedMax > 0 && (
        <div style={{ fontSize: 11.5, color: T.inkSoft, marginBottom: 8 }}>Risk-adjusted suggestion at the time: up to {fmt(m.suggestedMax)}</div>
      )}
      {m.checks?.length > 0 && (
        <div>
          {m.checks.map((c) => (
            <div key={c.id || c.label} style={{ display: "flex", gap: 8, padding: "6px 0", borderTop: `1px solid ${T.line}` }}>
              <Dot status={c.status} />
              <div>
                <div style={{ fontSize: 12.5, fontWeight: 600 }}>{c.label}</div>
                <div style={{ fontSize: 11.5, color: T.inkSoft, lineHeight: 1.4 }}>{c.detail}</div>
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function Stamp({ verdict }) {
  const color = verdict === "FITS YOUR PLAN" ? T.gain : verdict === "CAUTION" ? T.caution : T.loss;
  return (
    <div style={{
      display: "inline-block", transform: "rotate(-4deg)", border: `3px solid ${color}`,
      color, fontFamily: fontMono, fontWeight: 700, letterSpacing: "0.12em",
      padding: "8px 16px", borderRadius: 6, fontSize: 15, background: T.stampBg,
    }}>
      {verdict}
    </div>
  );
}

function Btn({ children, onClick, kind = "primary", disabled, small }) {
  const styles = {
    primary: { background: T.brand, color: T.onAccent, border: "none" },
    ghost: { background: "transparent", color: T.brand, border: `1.5px solid ${T.brand}` },
    danger: { background: T.loss, color: T.onAccent, border: "none" },
  }[kind];
  return (
    <button onClick={onClick} disabled={disabled} style={{
      ...styles, opacity: disabled ? 0.4 : 1, borderRadius: 12,
      padding: small ? "8px 14px" : "14px 18px", fontWeight: 600,
      fontSize: small ? 13 : 15, width: small ? "auto" : "100%", cursor: "pointer",
    }}>
      {children}
    </button>
  );
}

const Spinner = ({ label }) => (
  <div style={{ color: T.inkSoft, fontSize: 13.5, padding: "14px 0", animation: "pulse 1.2s ease infinite" }}>{label}</div>
);

// Module scope on purpose: defining Shell inside Tradester recreated the
// component type on every render, remounting the tree and dropping input focus.
const Shell = ({ children }) => (
  <div style={{ minHeight: "100vh", background: T.paper, fontFamily: fontBody, color: T.ink }}>
    <div style={{ maxWidth: 440, margin: "0 auto", padding: "0 0 90px" }}>{children}</div>
  </div>
);

const Field = (props) => (
  <input
    {...props}
    style={{
      width: "100%", padding: "13px 14px", borderRadius: 12, border: `1.5px solid ${T.line}`,
      fontSize: 15, background: T.card, marginBottom: 10, ...props.style,
    }}
  />
);

// One-line disclaimer reused near actions (auth, onboarding, order confirm).
const DISCLAIMER_SHORT =
  "Private friends & family test build — not financial advice. You alone are responsible for every trade and any resulting gains or losses.";

// Full plain-language terms. Shown pre-login (Terms link) and on the Profile tab.
// Plain-language, not a lawyer-drafted contract — have counsel review before any public launch.
function TermsContent() {
  const points = [
    ["Testing only", "This is a private, invite-only build shared with friends for testing. It is provided “as is”, with no warranty, and may change, break, or lose data at any time."],
    ["Not financial advice", "Nothing in the app — guardrail verdicts, ideas, coaching text, or the robo-trader — is financial, investment, tax, or legal advice, or a recommendation, offer, or solicitation to buy or sell anything. It’s a personal tool for organizing your own decisions."],
    ["You bear all outcomes", "Every trade is your own decision. You are solely responsible for all activity in your account and for any and all gains or losses. Trading involves risk, including loss of capital."],
    ["We don’t hold your money", "The app never holds or has access to your funds. Orders run in a built-in simulator or through your own brokerage account (e.g. Alpaca) using keys you provide. Your broker, not us, holds and settles everything."],
    ["No liability", "To the maximum extent permitted by law, the operators of this test build accept no liability for any losses, damages, or costs of any kind arising from your use of it."],
    ["Not licensed", "The operators are not a licensed financial adviser, broker, or capital markets services provider. Use live trading with your own keys entirely at your own risk."],
    ["Your agreement", "By creating an account or using the app you confirm you understand and accept these terms. If you don’t agree, please don’t use it."],
  ];
  return (
    <div>
      {points.map(([h, body]) => (
        <div key={h} style={{ marginBottom: 12 }}>
          <div style={{ fontWeight: 700, fontSize: 13, marginBottom: 3 }}>{h}</div>
          <div style={{ fontSize: 12.5, color: T.inkSoft, lineHeight: 1.55 }}>{body}</div>
        </div>
      ))}
    </div>
  );
}

// Small muted disclaimer line for placing under actions.
const DisclaimerNote = ({ style }) => (
  <p style={{ fontSize: 11, color: T.inkSoft, lineHeight: 1.5, ...style }}>{DISCLAIMER_SHORT}</p>
);

function LoginScreen({ onAuthed }) {
  const resetToken = new URLSearchParams(location.hash.slice(1)).get("reset");
  const [view, setView] = useState(resetToken ? "reset" : "login"); // login | register | forgot | 2fa | reset
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [code, setCode] = useState("");
  const [pending, setPending] = useState(null); // 2FA step token
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");
  const [notice, setNotice] = useState("");
  const [devLink, setDevLink] = useState(null);

  // Reset scroll on view change — otherwise switching from a long view (Terms)
  // back to a short one (sign in) leaves you scrolled past the content.
  const go = (v) => { setView(v); setError(""); setNotice(""); setDevLink(null); window.scrollTo(0, 0); };

  async function run(fn) {
    setBusy(true); setError("");
    try { await fn(); } catch (e) { setError(e.message); } finally { setBusy(false); }
  }

  const submitAuth = () => run(async () => {
    const r = await api(`/api/auth/${view}`, { email, password });
    if (r.requires2fa) { setPending(r.pending); setCode(""); go("2fa"); return; }
    if (r.devVerifyLink) setDevLink(r.devVerifyLink);
    onAuthed(r);
  });

  const submit2fa = () => run(async () => {
    onAuthed(await api("/api/auth/2fa", { pending, code }));
  });

  const submitForgot = () => run(async () => {
    const r = await api("/api/auth/forgot", { email });
    setNotice("If that email has an account, a reset link is on its way.");
    if (r.devResetLink) setDevLink(r.devResetLink);
  });

  const submitReset = () => run(async () => {
    await api("/api/auth/reset", { token: resetToken, password });
    history.replaceState(null, "", "/");
    setPassword("");
    setNotice("Password updated — sign in with the new one.");
    setView("login");
  });

  return (
    <Shell>
      <div style={{ padding: "72px 28px 0" }}>
        <div style={{ fontFamily: fontMono, fontSize: 12, letterSpacing: "0.2em", color: T.inkSoft }}>YOUR PLAN. KEPT.</div>
        <h1 style={{ fontFamily: fontDisplay, fontSize: 40, fontWeight: 700, lineHeight: 1.05, margin: "14px 0 22px" }}>Tradester</h1>

        {(view === "login" || view === "register") && (
          <React.Fragment>
            <div style={{ display: "flex", gap: 8, marginBottom: 18 }}>
              {[["login", "Sign in"], ["register", "Create account"]].map(([id, label]) => (
                <button key={id} onClick={() => go(id)}
                  style={{
                    flex: 1, padding: "10px 4px", borderRadius: 10, border: "none", fontSize: 13.5, fontWeight: 600, cursor: "pointer",
                    background: view === id ? T.chipOn : T.chipOff, color: view === id ? T.onAccent : T.ink,
                  }}>
                  {label}
                </button>
              ))}
            </div>
            <Field type="email" placeholder="Email" value={email} autoComplete="email"
              onChange={(e) => setEmail(e.target.value)} />
            <Field type="password" placeholder={view === "register" ? "Password (8+ characters)" : "Password"} value={password}
              autoComplete={view === "register" ? "new-password" : "current-password"}
              onChange={(e) => setPassword(e.target.value)}
              onKeyDown={(e) => e.key === "Enter" && !busy && submitAuth()} />
            {notice && <div style={{ color: T.gain, fontSize: 13, marginBottom: 10 }}>{notice}</div>}
            {error && <div style={{ color: T.loss, fontSize: 13, marginBottom: 10 }}>{error}</div>}
            <Btn onClick={submitAuth} disabled={busy || !email || !password}>
              {busy ? "One moment…" : view === "login" ? "Sign in" : "Create account"}
            </Btn>
            {view === "login" && (
              <button onClick={() => go("forgot")}
                style={{ background: "none", border: "none", color: T.brand, fontSize: 13, fontWeight: 600, cursor: "pointer", padding: 0, marginTop: 14 }}>
                Forgot password?
              </button>
            )}
            {view === "register" && (
              <p style={{ fontSize: 11.5, color: T.inkSoft, marginTop: 14, lineHeight: 1.5 }}>
                By creating an account you agree to the{" "}
                <button onClick={() => go("terms")} style={{ background: "none", border: "none", color: T.brand, fontWeight: 600, padding: 0, cursor: "pointer", fontSize: 11.5 }}>Terms &amp; Disclaimer</button>:
                a private test build, not financial advice, and you alone bear all trading outcomes.
              </p>
            )}
            <p style={{ fontSize: 11.5, color: T.inkSoft, marginTop: 16, lineHeight: 1.5 }}>
              Your plan, journal, and simulator portfolio live in your account. Brokerage keys are optional and stored encrypted.
            </p>
            <button onClick={() => go("terms")}
              style={{ background: "none", border: "none", color: T.inkSoft, fontSize: 11.5, fontWeight: 600, cursor: "pointer", padding: 0, marginTop: 8, textDecoration: "underline" }}>
              Terms &amp; Disclaimer
            </button>
          </React.Fragment>
        )}

        {view === "terms" && (
          <React.Fragment>
            <button onClick={() => go("login")}
              style={{ background: "none", border: "none", color: T.brand, fontSize: 13, fontWeight: 600, cursor: "pointer", padding: 0, marginBottom: 14 }}>
              ← Back to sign in
            </button>
            <h2 style={{ fontFamily: fontDisplay, fontSize: 22, marginBottom: 12 }}>Terms &amp; Disclaimer</h2>
            <TermsContent />
            <button onClick={() => go("login")}
              style={{ background: "none", border: "none", color: T.brand, fontSize: 13, fontWeight: 600, cursor: "pointer", padding: 0, marginTop: 8 }}>
              ← Back to sign in
            </button>
          </React.Fragment>
        )}

        {view === "forgot" && (
          <React.Fragment>
            <h2 style={{ fontFamily: fontDisplay, fontSize: 22, marginBottom: 6 }}>Reset your password</h2>
            <p style={{ fontSize: 13.5, color: T.inkSoft, marginBottom: 16 }}>We'll email you a link to choose a new one.</p>
            <Field type="email" placeholder="Email" value={email} autoComplete="email"
              onChange={(e) => setEmail(e.target.value)}
              onKeyDown={(e) => e.key === "Enter" && !busy && submitForgot()} />
            {notice && <div style={{ color: T.gain, fontSize: 13, marginBottom: 10 }}>{notice}</div>}
            {error && <div style={{ color: T.loss, fontSize: 13, marginBottom: 10 }}>{error}</div>}
            <Btn onClick={submitForgot} disabled={busy || !email}>{busy ? "One moment…" : "Send reset link"}</Btn>
            <button onClick={() => go("login")}
              style={{ background: "none", border: "none", color: T.brand, fontSize: 13, fontWeight: 600, cursor: "pointer", padding: 0, marginTop: 14 }}>
              ← Back to sign in
            </button>
          </React.Fragment>
        )}

        {view === "2fa" && (
          <React.Fragment>
            <h2 style={{ fontFamily: fontDisplay, fontSize: 22, marginBottom: 6 }}>Two-factor code</h2>
            <p style={{ fontSize: 13.5, color: T.inkSoft, marginBottom: 16 }}>Enter the 6-digit code from your authenticator app.</p>
            <Field inputMode="numeric" placeholder="123456" value={code} autoComplete="one-time-code"
              onChange={(e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
              onKeyDown={(e) => e.key === "Enter" && !busy && submit2fa()}
              style={{ fontFamily: fontMono, fontSize: 22, letterSpacing: "0.3em", textAlign: "center" }} />
            {error && <div style={{ color: T.loss, fontSize: 13, marginBottom: 10 }}>{error}</div>}
            <Btn onClick={submit2fa} disabled={busy || code.length !== 6}>{busy ? "Checking…" : "Verify"}</Btn>
            <button onClick={() => go("login")}
              style={{ background: "none", border: "none", color: T.brand, fontSize: 13, fontWeight: 600, cursor: "pointer", padding: 0, marginTop: 14 }}>
              ← Back to sign in
            </button>
          </React.Fragment>
        )}

        {view === "reset" && (
          <React.Fragment>
            <h2 style={{ fontFamily: fontDisplay, fontSize: 22, marginBottom: 6 }}>Choose a new password</h2>
            <Field type="password" placeholder="New password (8+ characters)" value={password} autoComplete="new-password"
              onChange={(e) => setPassword(e.target.value)}
              onKeyDown={(e) => e.key === "Enter" && !busy && submitReset()} />
            {error && <div style={{ color: T.loss, fontSize: 13, marginBottom: 10 }}>{error}</div>}
            <Btn onClick={submitReset} disabled={busy || password.length < 8}>{busy ? "Saving…" : "Set password"}</Btn>
          </React.Fragment>
        )}

        {devLink && (
          <div style={{ marginTop: 16, padding: 12, background: T.noticeBg, borderRadius: 10, fontSize: 11.5, wordBreak: "break-all" }}>
            <b>Dev mode (no email vendor):</b> <a href={devLink}>{devLink}</a>
          </div>
        )}
      </div>
    </Shell>
  );
}

// ================================================================
function Tradester() {
  const [screen, setScreen] = useState("boot"); // boot | login | welcome | onboard | app
  const [me, setMe] = useState(null); // { email, mode, hasPaperKeys, hasLiveKeys, policy, journal }
  const [step, setStep] = useState(0);
  const [tab, setTab] = useState("portfolio"); // portfolio | ideas | add | journal | profile
  const [policy, setPolicy] = useState({ sectors: [], risk: "Balanced", goal: "", horizon: "5+ years" });
  const [journal, setJournal] = useState([]);
  const [termsOpen, setTermsOpen] = useState(false); // Profile → Terms & Disclaimer expander
  const [journalKind, setJournalKind] = useState("all"); // all | trade | robo | cash | plan
  const [journalPeriod, setJournalPeriod] = useState("all"); // all | 7d | 30d | 90d | 1y
  const [expandedEntries, setExpandedEntries] = useState(() => new Set());
  const toggleExpanded = (key) => setExpandedEntries((prev) => {
    const next = new Set(prev);
    next.has(key) ? next.delete(key) : next.add(key);
    return next;
  });

  // Theme: "system" | "light" | "dark". Applied by toggling data-theme on <html>,
  // which flips the CSS variables the whole UI reads from. An inline script in
  // index.html sets it pre-paint too, so there's no light flash on load.
  const [theme, setTheme] = useState(() => store.load("theme", "system"));
  useEffect(() => {
    store.save("theme", theme);
    const mq = window.matchMedia("(prefers-color-scheme: dark)");
    const apply = () => {
      const dark = theme === "dark" || (theme === "system" && mq.matches);
      document.documentElement.dataset.theme = dark ? "dark" : "light";
    };
    apply();
    if (theme === "system") {
      mq.addEventListener("change", apply);
      return () => mq.removeEventListener("change", apply);
    }
  }, [theme]);

  // Session check on boot; localStorage policy is a one-time migration source
  useEffect(() => {
    api("/api/me")
      .then((m) => applyMe(m))
      .catch(() => setScreen("login"));
  }, []);

  // Re-check account status when the app returns to the foreground — e.g. after
  // tapping the email verify link (which opens in the browser) and switching
  // back. Otherwise `emailVerified` only refreshes on a full app restart. Only
  // touches `me` (so the verify banner clears); doesn't disturb the current
  // screen/policy/journal. No-ops when signed out (prev is null).
  useEffect(() => {
    const onVisible = () => {
      if (document.visibilityState !== "visible") return;
      api("/api/me")
        .then((m) => setMe((prev) => (prev ? { ...m, policy: prev.policy } : prev)))
        .catch(() => {});
    };
    document.addEventListener("visibilitychange", onVisible);
    return () => document.removeEventListener("visibilitychange", onVisible);
  }, []);

  function applyMe(m) {
    const migrated = m.policy || store.load("tradester.policy", null);
    if (m.policy == null && migrated) api("/api/me/policy", { policy: migrated }, "PUT").catch(() => {});
    setMe({ ...m, policy: migrated }); // me.policy = "last saved plan", even when migrated
    if (migrated) setPolicy(migrated);
    setJournal(m.journal || []);
    setScreen(migrated ? "app" : "welcome");
  }

  const [account, setAccount] = useState(null); // { cash, equity, broker }
  const [positions, setPositions] = useState([]);
  const [drift, setDrift] = useState([]);
  const [goal, setGoal] = useState(null); // { requiredCagr, profileRange, status, note }
  const [equityHistory, setEquityHistory] = useState([]);
  const [equityRange, setEquityRange] = useState("1M");
  const [ideas, setIdeas] = useState(null);
  const [banner, setBanner] = useState("");

  // Ticker search + stock detail (Add tab)
  const [searchResults, setSearchResults] = useState([]);
  const [stockDetail, setStockDetail] = useState(null); // null | "loading" | {...}
  const [detailRange, setDetailRange] = useState("1M");
  // Suppresses autocomplete right after a symbol is explicitly chosen (search
  // click, or "Check fit" from Ideas). A ref, not state derived from the async
  // stock-detail fetch — that fetch can take longer than the debounce, so
  // gating on its resolution let the dropdown reopen mid-load on a slow
  // network. Comparing the typed text straight to this ref has no such race.
  const lastSelectedSymbol = React.useRef(null);

  // Simulator fund transfers (Profile tab)
  const [fundAmt, setFundAmt] = useState(1000);
  const [funding, setFunding] = useState(false);

  // Robo-trader + pre-market brief
  const [roboState, setRoboState] = useState(null); // { paused, mandates, lots, approvals, learn }
  const [brief, setBrief] = useState(null);
  const [briefOpen, setBriefOpen] = useState(false);
  const [mForm, setMForm] = useState(null); // mandate editor form values (null = closed)
  const [roboBusy, setRoboBusy] = useState(false);
  const [mSymbolQuery, setMSymbolQuery] = useState(""); // raw text typed into the mandate symbol search
  const [mSearchResults, setMSearchResults] = useState([]);
  // mForm.symbol only gets set once a suggestion is clicked — this ref remembers
  // that choice so the debounced search doesn't reopen the dropdown right after.
  const mLastSelectedSymbol = React.useRef(null);

  const M_DEFAULTS = { symbol: "", allocPct: 10, clipSize: 1000, buyDipPct: 5, takeProfitPct: 15, stopLossPct: 8, autoLimit: 500, maxOpenPositions: 3, maxOrdersPerDay: 2 };

  // Mandate symbol autocomplete — mirrors the Add-tab ticker search. mForm.symbol
  // (what actually gets saved) is only ever set via selectMandateSymbol, so free-typed
  // text that was never picked from a real result can't reach "Create mandate".
  useEffect(() => {
    if (!mForm) { setMSearchResults([]); return; }
    const q = mSymbolQuery.trim().toUpperCase();
    if (!q || q === mLastSelectedSymbol.current) { setMSearchResults([]); return; }
    const id = setTimeout(() => {
      api(`/api/search?q=${encodeURIComponent(q)}`).then(setMSearchResults).catch(() => setMSearchResults([]));
    }, 300);
    return () => clearTimeout(id);
  }, [mSymbolQuery, mForm]);

  function selectMandateSymbol(sym) {
    mLastSelectedSymbol.current = sym.toUpperCase();
    setMSymbolQuery(sym.toUpperCase());
    setMSearchResults([]);
    setMForm((f) => ({ ...f, symbol: sym.toUpperCase() }));
  }

  function openMandateForm() {
    mLastSelectedSymbol.current = null;
    setMSymbolQuery("");
    setMSearchResults([]);
    setMForm({ ...M_DEFAULTS });
  }

  function closeMandateForm() {
    mLastSelectedSymbol.current = null;
    setMSymbolQuery("");
    setMSearchResults([]);
    setMForm(null);
  }

  async function saveRobo(patch) {
    setRoboBusy(true);
    try {
      setRoboState(await api("/api/me/robo", patch, "PUT"));
    } catch (e) {
      setBanner(e.message);
    } finally {
      setRoboBusy(false);
    }
  }

  function mandateFromForm(f) {
    return {
      symbol: f.symbol.trim().toUpperCase(),
      allocation: { type: "pct", value: Number(f.allocPct) / 100 },
      clipSize: Number(f.clipSize),
      autoLimit: Number(f.autoLimit),
      maxOpenPositions: Number(f.maxOpenPositions),
      maxOrdersPerDay: Number(f.maxOrdersPerDay),
      rules: { buyDipPct: Number(f.buyDipPct) / 100, takeProfitPct: Number(f.takeProfitPct) / 100, stopLossPct: Number(f.stopLossPct) / 100 },
    };
  }

  async function approveRobo(id, decision) {
    setRoboBusy(true);
    try {
      setRoboState(await api("/api/me/robo/approve", { id, decision }));
      await refreshPortfolio();
    } catch (e) {
      setBanner(e.message);
    } finally {
      setRoboBusy(false);
    }
  }

  // Inline sell form on holdings
  const [sellSym, setSellSym] = useState(null);
  const [sellAmt, setSellAmt] = useState(500);
  const [selling, setSelling] = useState(false);

  // Profile / broker keys form
  const [keys, setKeys] = useState({ paperKeyId: "", paperSecret: "", liveKeyId: "", liveSecret: "" });
  const [savingBroker, setSavingBroker] = useState(false);
  const [profileMsg, setProfileMsg] = useState(null); // { ok, text }

  // 2FA setup flow
  const [twofa, setTwofa] = useState(null); // { qr, secret } while enrolling
  const [twofaCode, setTwofaCode] = useState("");
  const [twofaBusy, setTwofaBusy] = useState(false);
  const [devVerifyLink, setDevVerifyLink] = useState(null);

  async function resendVerification() {
    try {
      const r = await api("/api/auth/resend-verification", {});
      setDevVerifyLink(r.devVerifyLink || null);
      setBanner("");
      setProfileMsg({ ok: true, text: r.devVerifyLink ? "Dev mode: verification link shown below." : "Verification email sent — check your inbox." });
    } catch (e) {
      setProfileMsg({ ok: false, text: e.message });
    }
  }

  async function twofaAction(path, body) {
    setTwofaBusy(true); setProfileMsg(null);
    try {
      const r = await api(path, body);
      if (path.endsWith("setup")) { setTwofa(r); setTwofaCode(""); }
      else { setMe((m) => ({ ...m, totpEnabled: r.totpEnabled })); setTwofa(null); setTwofaCode(""); }
      return true;
    } catch (e) {
      setProfileMsg({ ok: false, text: e.message });
      return false;
    } finally {
      setTwofaBusy(false);
    }
  }

  async function saveBroker(patch) {
    setSavingBroker(true); setProfileMsg(null);
    try {
      const m = await api("/api/me/broker", patch, "PUT");
      setMe(m);
      setKeys({ paperKeyId: "", paperSecret: "", liveKeyId: "", liveSecret: "" });
      setProfileMsg({
        ok: true,
        text: patch.mode === "paper"
          ? "Keys verified — you're now trading on your Alpaca paper account."
          : "Saved. Keys were verified against Alpaca. Tap the account above to switch.",
      });
      await refreshPortfolio();
    } catch (e) {
      setProfileMsg({ ok: false, text: e.message });
    } finally {
      setSavingBroker(false);
    }
  }

  async function switchMode(mode) {
    setSavingBroker(true); setProfileMsg(null);
    try {
      const m = await api("/api/me/broker", { mode }, "PUT");
      setMe(m);
      await refreshPortfolio();
    } catch (e) {
      setProfileMsg({ ok: false, text: e.message });
    } finally {
      setSavingBroker(false);
    }
  }

  // Add-trade flow state
  const [ticker, setTicker] = useState("");
  const [amount, setAmount] = useState(2000);
  const [checking, setChecking] = useState(false);
  const [checkError, setCheckError] = useState("");
  const [checkResult, setCheckResult] = useState(null); // { stock, checks, verdict, suggestedMax }
  const [revealed, setRevealed] = useState(0);
  const [coachText, setCoachText] = useState("");
  const [coachLoading, setCoachLoading] = useState(false);
  const [overrideReason, setOverrideReason] = useState("");
  const [placing, setPlacing] = useState(false);
  const [tpPct, setTpPct] = useState(""); // optional exit levels on the confirm step
  const [slPct, setSlPct] = useState("");
  const [openOrders, setOpenOrders] = useState([]);
  const [logoMap, setLogoMap] = useState({}); // symbol -> logoUrl, for lists that only ever had a bare symbol string

  // Journal history, open orders, and robo approvals surface symbols that were
  // never fetched through /api/stock or /api/positions, so they'd otherwise show
  // only the monogram fallback. One batched lookup per new symbol set beats a
  // per-row fetch (and getCompanyMeta itself caches 7 days, so this is cheap).
  useEffect(() => {
    const symbols = new Set();
    journal.forEach((j) => { if (j.symbol && j.symbol !== "PLAN" && j.symbol !== "CASH") symbols.add(j.symbol); });
    openOrders.forEach((o) => symbols.add(o.symbol));
    (roboState?.approvals || []).forEach((a) => symbols.add(a.symbol));
    const missing = [...symbols].filter((s) => !(s in logoMap));
    if (!missing.length) return;
    api(`/api/logos?symbols=${missing.join(",")}`)
      .then((map) => setLogoMap((prev) => ({ ...prev, ...Object.fromEntries(missing.map((s) => [s, null])), ...map })))
      .catch(() => {});
  }, [journal, openOrders, roboState]);

  async function cancelOrder(o) {
    try {
      const res = await fetch(`/api/orders/${o.id}?symbol=${o.symbol}&notional=${o.notional || ""}`, { method: "DELETE" });
      const d = await res.json().catch(() => ({}));
      if (!res.ok) throw new Error(d.error || "Cancel failed");
      refreshPortfolioBurst();
    } catch (e) {
      // Most likely cause: the order filled at the broker between the last
      // refresh and this click, so Alpaca refuses the cancel. Refresh anyway —
      // the stale "open order" row should clear once the fill posts.
      setBanner(`Couldn't cancel — ${e.message}`);
      refreshPortfolioBurst();
    }
  }

  const totalValue = account ? account.cash + positions.reduce((s, p) => s + p.value, 0) : 0;

  const refreshPortfolio = useCallback(async () => {
    try {
      const [acct, pos] = await Promise.all([api("/api/account"), api("/api/positions")]);
      setAccount(acct);
      setPositions(pos);
      api("/api/me/robo").then(setRoboState).catch(() => {});
      api("/api/me/brief").then(setBrief).catch(() => {});
      api("/api/orders").then(setOpenOrders).catch(() => setOpenOrders([]));
      if (policy.sectors.length) {
        api(`/api/drift?sectors=${policy.sectors.join(",")}&risk=${policy.risk}`)
          .then(setDrift).catch(() => setDrift([]));
      }
      if (policy.targetAmount > 0) {
        const horizonYears = { "1–3 years": 2, "3–5 years": 4, "5+ years": 7 }[policy.horizon] || 7;
        api(`/api/goal?risk=${policy.risk}&target=${policy.targetAmount}&horizonYears=${horizonYears}`)
          .then(setGoal).catch(() => setGoal(null));
      }
    } catch (e) {
      setBanner(`Can't reach the server: ${e.message}`);
    }
  }, [policy]);

  // Alpaca market orders often fill within seconds while the market's open, so
  // a single refresh right after submitting (or cancelling) an order can land
  // before that fill posts — the UI is left showing a stale "open order" a
  // moment longer than it should. Trail a couple more refreshes a few seconds
  // apart to catch the transition without the user having to reload or click
  // elsewhere. Fire-and-forget: callers don't need to wait on these.
  const refreshPortfolioBurst = useCallback(() => {
    refreshPortfolio();
    setTimeout(refreshPortfolio, 2500);
    setTimeout(refreshPortfolio, 5500);
  }, [refreshPortfolio]);

  useEffect(() => { if (screen === "app") refreshPortfolio(); }, [screen, refreshPortfolio]);

  useEffect(() => {
    if (screen !== "app") return;
    api(`/api/equity-history?range=${equityRange}`).then(setEquityHistory).catch(() => setEquityHistory([]));
  }, [screen, equityRange]);

  useEffect(() => {
    if (screen !== "app" || tab !== "ideas") return;
    setIdeas(null);
    api(`/api/ideas?sectors=${policy.sectors.join(",")}&risk=${policy.risk}`)
      .then(setIdeas)
      .catch((e) => { setIdeas([]); setBanner(e.message); });
  }, [screen, tab, policy]);

  // Ticker autocomplete (debounced) — suppressed once a symbol has been
  // explicitly picked, until the user edits the ticker text again.
  useEffect(() => {
    if (tab !== "add" || checking) { setSearchResults([]); return; }
    const q = ticker.trim().toUpperCase();
    if (!q || q === lastSelectedSymbol.current) {
      setSearchResults([]);
      return;
    }
    const id = setTimeout(() => {
      api(`/api/search?q=${encodeURIComponent(q)}`).then(setSearchResults).catch(() => setSearchResults([]));
    }, 300);
    return () => clearTimeout(id);
  }, [ticker, tab, checking]);

  function selectStock(sym) {
    lastSelectedSymbol.current = sym.toUpperCase();
    setTicker(sym);
    setSearchResults([]);
    setCheckResult(null); setCheckError(""); setCoachText("");
    setStockDetail("loading");
    setDetailRange("1M");
    api(`/api/stock/${sym}?range=1M`).then(setStockDetail).catch(() => setStockDetail(null));
  }

  // Re-fetch just the chart series (and refreshed stats) when the range tab changes
  useEffect(() => {
    if (!stockDetail || stockDetail === "loading") return;
    const sym = stockDetail.symbol;
    api(`/api/stock/${sym}?range=${detailRange}`)
      .then((d) => setStockDetail((cur) => (cur && cur !== "loading" && cur.symbol === sym ? d : cur)))
      .catch(() => {});
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [detailRange]);

  async function transferFunds(direction) {
    setFunding(true); setProfileMsg(null);
    try {
      const r = await api("/api/funds", { amount: fundAmt, direction });
      if (r.journal) setJournal(r.journal);
      await refreshPortfolio();
      setProfileMsg({ ok: true, text: `${direction === "deposit" ? "Added" : "Withdrew"} ${fmt(fundAmt)} — cash is now ${fmt(r.cash)}.` });
    } catch (e) {
      setProfileMsg({ ok: false, text: e.message });
    } finally {
      setFunding(false);
    }
  }

  // Sequential check reveal animation
  useEffect(() => {
    if (!checkResult) return;
    setRevealed(0);
    const id = setInterval(() => {
      setRevealed((r) => {
        if (r >= checkResult.checks.length + 1) { clearInterval(id); return r; }
        return r + 1;
      });
    }, 350);
    return () => clearInterval(id);
  }, [checkResult]);

  const sectorAlloc = useMemo(() => {
    const m = {};
    positions.forEach((p) => { m[p.sector] = (m[p.sector] || 0) + p.value; });
    return m;
  }, [positions]);

  function savePolicy(p) {
    setPolicy(p);
    setMe((m) => (m ? { ...m, policy: p } : m)); // keep the "saved plan" reference current
    store.save("tradester.policy", p); // offline fallback
    api("/api/me/policy", { policy: p }, "PUT")
      .then((r) => { if (r?.journal) setJournal(r.journal); }) // plan revisions land in the journal
      .catch(() => {});
  }

  async function addJournalEntry(entry) {
    try {
      const serverJournal = await api("/api/me/journal", { entry });
      setJournal(serverJournal);
    } catch {
      const next = [entry, ...journal];
      setJournal(next);
      store.save("tradester.journal", next);
    }
  }

  async function startCheck(sym, amt) {
    setCheckError(""); setCheckResult(null); setCoachText(""); setOverrideReason("");
    setTpPct(""); setSlPct("");
    lastSelectedSymbol.current = sym.toUpperCase();
    setSearchResults([]);
    // A detail card for some OTHER symbol (e.g. left over from a search
    // earlier in the session) must not linger under a check for this one.
    setStockDetail((cur) => (cur && cur !== "loading" && cur.symbol === sym.toUpperCase() ? cur : null));
    setChecking(true);
    setAmount(amt);
    try {
      // Only the cooling-off check reads the journal, and only its date/overrode
      // fields. Sending the full journal — now carrying fat per-entry `meta`
      // blobs — balloons the body past the server limit (413). Signed-in users'
      // checks use the server-side journal anyway; this lean copy just keeps the
      // anonymous path working without the bloat.
      const leanJournal = journal.map((j) => ({ date: j.date, overrode: j.overrode }));
      const result = await api("/api/check", { symbol: sym, amount: amt, policy, journal: leanJournal });
      setCheckResult(result);
    } catch (e) {
      setCheckError(e.message.includes("Not enough") || e.message.includes("Not Found") || e.message.includes("404")
        ? `Couldn't find price data for ${sym.toUpperCase()}. Check the ticker.`
        : e.message);
    } finally {
      setChecking(false);
    }
  }

  async function executeTrade(reason) {
    const { stock, verdict, checks, suggestedMax } = checkResult;
    setPlacing(true);
    try {
      const tp = tpPct ? Math.round(stock.price * (1 + Number(tpPct) / 100) * 100) / 100 : null;
      const sl = slPct ? Math.round(stock.price * (1 - Number(slPct) / 100) * 100) / 100 : null;
      await api("/api/orders", { symbol: stock.symbol, notional: amount, tp, sl });
      await addJournalEntry({
        date: new Date().toISOString(),
        symbol: stock.symbol, amount, verdict,
        overrode: verdict === "OUT OF POLICY",
        reason: reason || (tp || sl ? `TP ${tp ? "$" + tp : "—"} / SL ${sl ? "$" + sl : "—"}` : null),
        meta: {
          checks, suggestedMax, tp, sl,
          stock: { price: stock.price, sector: stock.sector, annVol: stock.annVol, beta: stock.beta, ret1m: stock.ret1m, ret1y: stock.ret1y },
          policy: { risk: policy.risk, sectors: policy.sectors, goal: policy.goal, targetAmount: policy.targetAmount || null },
        },
      });
      setCheckResult(null); setTicker(""); setTab("portfolio");
      refreshPortfolioBurst();
    } catch (e) {
      setCheckError(`Order failed: ${e.message}`);
    } finally {
      setPlacing(false);
    }
  }

  async function sellPosition(symbol, amt, closeAll = false) {
    setSelling(true);
    try {
      // "Sell all" closes the whole position by share quantity, so no rounded
      // dollar remainder is left behind; a partial sell trims a dollar amount.
      const body = closeAll ? { symbol, side: "sell", closeAll: true } : { symbol, notional: amt, side: "sell" };
      const order = await api("/api/orders", body);
      await addJournalEntry({
        date: new Date().toISOString(), symbol,
        amount: order.notional != null ? Math.round(order.notional) : amt,
        verdict: "SOLD", overrode: false, reason: closeAll ? "closed full position" : null,
        meta: { qty: order.qty, price: order.filled_avg_price },
      });
      setSellSym(null);
      refreshPortfolioBurst();
    } catch (e) {
      setBanner(`Sell failed: ${e.message}`);
    } finally {
      setSelling(false);
    }
  }

  async function askCoach() {
    setCoachLoading(true);
    try {
      const { stock, checks, verdict } = checkResult;
      const { text } = await api("/api/coach", { stock, amount, policy, result: { checks, verdict } });
      setCoachText(text);
    } catch {
      setCoachText("Coach is unavailable right now — the checks above are the substance; the stamp is the verdict.");
    } finally {
      setCoachLoading(false);
    }
  }

  if (screen === "boot")
    return (
      <Shell>
        <div style={{ padding: "120px 28px 0", textAlign: "center" }}><Spinner label="Loading…" /></div>
      </Shell>
    );

  if (screen === "login") return <LoginScreen onAuthed={applyMe} />;

  // --- Welcome ---
  if (screen === "welcome")
    return (
      <Shell>
        <div style={{ padding: "72px 28px 0" }}>
          <div style={{ fontFamily: fontMono, fontSize: 12, letterSpacing: "0.2em", color: T.inkSoft }}>YOUR PLAN. KEPT.</div>
          <h1 style={{ fontFamily: fontDisplay, fontSize: 46, fontWeight: 700, lineHeight: 1.05, margin: "14px 0 18px" }}>
            Tradester
          </h1>
          <p style={{ fontSize: 16, lineHeight: 1.55, color: T.inkSoft, marginBottom: 8 }}>
            A trading coach that helps you define what a good portfolio looks like for <em>you</em> — then holds you to it, trade by trade.
          </p>
          <div style={{ margin: "28px 0 36px" }}>
            {[["01", "Set your sectors, risk appetite and goal"], ["02", "Get trade ideas that fit your plan"], ["03", "Every buy is checked against your own rules"]].map(([n, t]) => (
              <div key={n} style={{ display: "flex", gap: 14, padding: "12px 0", borderBottom: `1px solid ${T.line}` }}>
                <span style={{ fontFamily: fontMono, color: T.brand, fontWeight: 700, fontSize: 13 }}>{n}</span>
                <span style={{ fontSize: 14.5 }}>{t}</span>
              </div>
            ))}
          </div>
          <Btn onClick={() => setScreen("onboard")}>Build my plan</Btn>
          <DisclaimerNote style={{ marginTop: 16 }} />
        </div>
      </Shell>
    );

  // --- Onboarding ---
  if (screen === "onboard") {
    const steps = ["Sectors", "Risk", "Ambition"];
    const editing = Boolean(me?.policy); // revising an existing plan vs first-run
    return (
      <Shell>
        <div style={{ padding: "40px 24px 0" }}>
          {editing && (
            <button onClick={() => { setPolicy(me.policy); setScreen("app"); }}
              style={{ background: "none", border: "none", color: T.brand, fontSize: 13, fontWeight: 600, cursor: "pointer", padding: 0, marginBottom: 14 }}>
              ← Keep current plan
            </button>
          )}
          <div style={{ display: "flex", gap: 6, marginBottom: 28 }}>
            {steps.map((s, i) => (
              <div key={s} style={{ flex: 1, height: 4, borderRadius: 4, background: i <= step ? T.brand : T.line }} />
            ))}
          </div>

          {step === 0 && (
            <React.Fragment>
              <h2 style={{ fontFamily: fontDisplay, fontSize: 28, marginBottom: 6 }}>Where do you want to invest?</h2>
              <p style={{ color: T.inkSoft, fontSize: 14, marginBottom: 20 }}>Pick 2–4 sectors you understand and believe in. Fewer, deeper convictions beat scattered bets.</p>
              <div style={{ display: "flex", flexWrap: "wrap", gap: 10 }}>
                {SECTORS.map((s) => {
                  const on = policy.sectors.includes(s);
                  return (
                    <button key={s}
                      onClick={() => setPolicy((p) => ({ ...p, sectors: on ? p.sectors.filter((x) => x !== s) : [...p.sectors, s] }))}
                      style={{
                        padding: "11px 16px", borderRadius: 999, border: "none", fontSize: 14, fontWeight: 600,
                        background: on ? T.chipOn : T.chipOff, color: on ? T.onAccent : T.ink, cursor: "pointer",
                      }}>
                      {s}
                    </button>
                  );
                })}
              </div>
              <div style={{ marginTop: 32 }}>
                <Btn disabled={policy.sectors.length < 2} onClick={() => setStep(1)}>
                  Continue {policy.sectors.length > 0 && `(${policy.sectors.length} selected)`}
                </Btn>
              </div>
            </React.Fragment>
          )}

          {step === 1 && (
            <React.Fragment>
              <h2 style={{ fontFamily: fontDisplay, fontSize: 28, marginBottom: 6 }}>How much heat can you take?</h2>
              <p style={{ color: T.inkSoft, fontSize: 14, marginBottom: 20 }}>This sets hard limits: position sizes, sector caps, volatility ceiling, cash buffer.</p>
              {Object.entries(RISK_UI).map(([name, r]) => {
                const on = policy.risk === name;
                return (
                  <button key={name} onClick={() => setPolicy((p) => ({ ...p, risk: name }))}
                    style={{
                      display: "block", width: "100%", textAlign: "left", marginBottom: 12, padding: 16,
                      borderRadius: 14, cursor: "pointer", background: T.card,
                      border: on ? `2px solid ${T.brand}` : `1.5px solid ${T.line}`,
                    }}>
                    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
                      <span style={{ fontWeight: 700, fontSize: 15 }}>{name}</span>
                      <span style={{ fontFamily: fontMono, fontSize: 11, color: T.inkSoft }}>vol ≤ {r.volCeil}% · max pos {r.maxPos}%</span>
                    </div>
                    <div style={{ fontSize: 13, color: T.inkSoft, marginTop: 4 }}>{r.blurb}</div>
                  </button>
                );
              })}
              <div style={{ marginTop: 20 }}>
                <Btn onClick={() => setStep(2)}>Continue</Btn>
              </div>
            </React.Fragment>
          )}

          {step === 2 && (
            <React.Fragment>
              <h2 style={{ fontFamily: fontDisplay, fontSize: 28, marginBottom: 6 }}>What are you building toward?</h2>
              <p style={{ color: T.inkSoft, fontSize: 14, marginBottom: 20 }}>Your coach will use this to keep trades tied to a purpose, not a mood.</p>
              <textarea
                value={policy.goal}
                onChange={(e) => setPolicy((p) => ({ ...p, goal: e.target.value }))}
                placeholder="e.g. Grow $25k into a house down-payment fund over 7 years"
                style={{
                  width: "100%", minHeight: 90, padding: 14, borderRadius: 14, fontSize: 15,
                  border: `1.5px solid ${T.line}`, background: T.card, resize: "vertical",
                }}
              />
              <div style={{ display: "flex", gap: 8, margin: "16px 0 0" }}>
                <input
                  type="number"
                  value={policy.targetAmount || ""}
                  onChange={(e) => setPolicy((p) => ({ ...p, targetAmount: Number(e.target.value) || null }))}
                  placeholder="Target amount, e.g. 100000 (optional)"
                  style={{ flex: 1, padding: "13px 14px", borderRadius: 12, border: `1.5px solid ${T.line}`, fontSize: 14, fontFamily: fontMono, background: T.card }}
                />
              </div>
              <div style={{ display: "flex", gap: 8, margin: "16px 0 28px" }}>
                {["1–3 years", "3–5 years", "5+ years"].map((h) => (
                  <button key={h} onClick={() => setPolicy((p) => ({ ...p, horizon: h }))}
                    style={{
                      flex: 1, padding: "10px 4px", borderRadius: 10, border: "none", fontSize: 13, fontWeight: 600,
                      background: policy.horizon === h ? T.chipOn : T.chipOff, color: policy.horizon === h ? T.onAccent : T.ink, cursor: "pointer",
                    }}>
                    {h}
                  </button>
                ))}
              </div>
              <Btn disabled={!policy.goal.trim()} onClick={() => { savePolicy(policy); setScreen("app"); }}>
                {me?.policy ? "Save my plan" : "Start trading on paper"}
              </Btn>
              {!me?.policy && <DisclaimerNote style={{ marginTop: 14 }} />}
            </React.Fragment>
          )}
        </div>
      </Shell>
    );
  }

  // --- Main app ---
  const r = RISK_UI[policy.risk];
  const modeTheme = MODE_THEME[account?.broker] || MODE_THEME.simulator;
  return (
    <Shell>
      {/* Header — background color signals which account is live */}
      <div style={{ background: modeTheme.bg, color: "#fff", padding: "26px 24px 22px", borderRadius: "0 0 24px 24px", transition: "background 0.4s ease" }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
          <span style={{ fontFamily: fontDisplay, fontSize: 20, fontWeight: 700 }}>Tradester</span>
          <span style={{
            fontFamily: fontMono, fontSize: 11, fontWeight: 700, letterSpacing: "0.06em",
            padding: "3px 8px", borderRadius: 6,
            background: account?.broker === "alpaca-live" ? "#F2B279" : "rgba(255,255,255,0.14)",
            color: account?.broker === "alpaca-live" ? "#3A1610" : "#fff",
          }}>
            {policy.risk.toUpperCase()} · {modeTheme.tag}
          </span>
        </div>
        <div style={{ fontFamily: fontMono, fontSize: 34, fontWeight: 700, margin: "14px 0 2px" }}>
          {account ? fmt(totalValue) : "—"}
        </div>
        <div style={{ fontSize: 12.5, opacity: 0.8 }}>
          {account ? `Cash ${fmt(account.cash)} · ` : ""}{policy.goal || "No goal set"}
        </div>
      </div>

      {banner && (
        <div style={{ margin: "12px 20px 0", padding: "10px 14px", borderRadius: 12, background: T.warnBg, color: T.loss, fontSize: 13 }}
          onClick={() => setBanner("")}>
          {banner}
        </div>
      )}

      {me && me.emailVerified === false && (
        <div style={{ margin: "12px 20px 0", padding: "10px 14px", borderRadius: 12, background: T.noticeBg, fontSize: 12.5, display: "flex", justifyContent: "space-between", alignItems: "center", gap: 10 }}>
          <span>Verify your email to secure your account{me.hasLiveKeys ? "" : " (required before live keys)"}.</span>
          <Btn small kind="ghost" onClick={resendVerification}>Resend</Btn>
        </div>
      )}

      <div style={{ padding: "18px 20px" }}>
        {/* ---- PORTFOLIO ---- */}
        {tab === "portfolio" && (
          <React.Fragment>
            {brief && (
              <div style={{ background: T.card, borderRadius: 16, border: `1px solid ${T.line}`, marginBottom: 18, overflow: "hidden" }}>
                <button onClick={() => setBriefOpen(!briefOpen)}
                  style={{ display: "flex", justifyContent: "space-between", alignItems: "center", width: "100%", padding: "14px 16px", border: "none", background: "transparent", cursor: "pointer" }}>
                  <span style={{ fontFamily: fontDisplay, fontSize: 16, fontWeight: 700 }}>☀ Pre-market brief</span>
                  <span style={{ fontFamily: fontMono, fontSize: 11, color: T.inkSoft }}>{brief.date} {briefOpen ? "▴" : "▾"}</span>
                </button>
                {briefOpen && (
                  <div style={{ padding: "0 16px 14px", fontSize: 13, lineHeight: 1.6, whiteSpace: "pre-wrap" }}>{brief.text}</div>
                )}
              </div>
            )}

            {openOrders.length > 0 && (
              <div style={{ background: T.card, borderRadius: 16, padding: 16, border: `1px solid ${T.line}`, marginBottom: 18 }}>
                <div style={{ fontFamily: fontDisplay, fontSize: 16, fontWeight: 700, marginBottom: 4 }}>Open orders</div>
                <p style={{ fontSize: 11.5, color: T.inkSoft, margin: "0 0 6px" }}>Queued at Alpaca — market orders fill at the next open. Cancel any time before the fill.</p>
                {openOrders.map((o) => (
                  <div key={o.id} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "9px 0", borderTop: `1px solid ${T.line}`, fontSize: 12.5 }}>
                    <span style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
                      <Logo symbol={o.symbol} size={24} src={logoMap[o.symbol]} />
                      <span>
                        <b style={{ fontFamily: fontMono }}>{o.symbol}</b> {o.side}
                        {" "}{o.notional ? fmt(o.notional) : `${o.qty} sh`}
                        <span style={{ color: T.inkSoft }}> · {o.type}{o.class ? ` (${o.class})` : ""}</span>
                        {o.legs.length > 0 && (
                          <span style={{ color: T.inkSoft, fontFamily: fontMono, fontSize: 11 }}>
                            {" "}{o.legs.map((l) => l.limit ? `TP $${l.limit}` : `SL $${l.stop}`).join(" / ")}
                          </span>
                        )}
                      </span>
                    </span>
                    <Btn small kind="ghost" onClick={() => cancelOrder(o)}>Cancel</Btn>
                  </div>
                ))}
              </div>
            )}

            {roboState?.approvals?.length > 0 && (
              <div style={{ background: T.card, borderRadius: 16, padding: 16, border: `1.5px solid ${T.caution}`, marginBottom: 18 }}>
                <div style={{ fontFamily: fontDisplay, fontSize: 16, fontWeight: 700, marginBottom: 8 }}>Robo orders awaiting approval</div>
                {roboState.approvals.map((a) => (
                  <div key={a.id} style={{ padding: "10px 0", borderTop: `1px solid ${T.line}` }}>
                    <div style={{ display: "flex", justifyContent: "space-between", fontSize: 13.5 }}>
                      <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
                        <Logo symbol={a.symbol} size={24} src={logoMap[a.symbol]} />
                        <b style={{ fontFamily: fontMono }}>{a.symbol}</b> {fmt(a.notional)}
                      </span>
                      <span style={{ fontFamily: fontMono, fontSize: 11, color: a.verdict === "CAUTION" ? T.caution : T.gain }}>{a.verdict}</span>
                    </div>
                    <div style={{ fontSize: 12, color: T.inkSoft, margin: "4px 0 8px" }}>
                      {a.reason} · TP ${a.tp.toFixed(2)} / SL ${a.sl.toFixed(2)} · expires {new Date(a.expiresAt).toLocaleTimeString("en-SG", { hour: "2-digit", minute: "2-digit" })}
                    </div>
                    <div style={{ display: "flex", gap: 8 }}>
                      <Btn small disabled={roboBusy} onClick={() => approveRobo(a.id, "approve")}>Approve</Btn>
                      <Btn small kind="ghost" disabled={roboBusy} onClick={() => approveRobo(a.id, "decline")}>Decline</Btn>
                    </div>
                  </div>
                ))}
              </div>
            )}

            {roboState?.mandates?.length > 0 && (
              <div style={{ background: T.card, borderRadius: 16, padding: 16, border: `1px solid ${T.line}`, marginBottom: 18, opacity: roboState.paused ? 0.55 : 1 }}>
                <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6 }}>
                  <span style={{ fontFamily: fontDisplay, fontSize: 16, fontWeight: 700 }}>Robo-trader</span>
                  <span style={{ fontFamily: fontMono, fontSize: 11, color: roboState.paused ? T.loss : T.gain }}>
                    {roboState.paused ? "⏸ PAUSED" : "● ACTIVE"}
                  </span>
                </div>
                {roboState.mandates.map((m) => {
                  const lots = roboState.lots.filter((l) => l.mandateId === m.id);
                  const learn = roboState.learn[m.id];
                  return (
                    <div key={m.id} style={{ padding: "8px 0", borderTop: `1px solid ${T.line}`, fontSize: 12.5 }}>
                      <div style={{ display: "flex", justifyContent: "space-between" }}>
                        <span><b style={{ fontFamily: fontMono }}>{m.symbol}</b>{!m.enabled && <span style={{ color: T.inkSoft }}> · off</span>}</span>
                        <span style={{ fontFamily: fontMono, color: T.inkSoft }}>
                          {lots.length} lot{lots.length === 1 ? "" : "s"} · dip {(m.rules.buyDipPct * 100).toFixed(0)}% / TP {(m.rules.takeProfitPct * 100).toFixed(0)}% / SL {(m.rules.stopLossPct * 100).toFixed(0)}%
                        </span>
                      </div>
                      {learn && learn.ticks > 0 && (
                        <div style={{ fontSize: 11, color: T.inkSoft, marginTop: 3, fontFamily: fontMono }}>
                          Q-shadow: {learn.ticks} ticks · shadow {learn.shadowReturnPct >= 0 ? "+" : ""}{learn.shadowReturnPct}% vs rules {learn.rulesReturnPct >= 0 ? "+" : ""}{learn.rulesReturnPct}%
                          {learn.canDrive && <span style={{ color: T.gain }}> · eligible to drive</span>}
                        </div>
                      )}
                    </div>
                  );
                })}
              </div>
            )}

            {account && (
              <div style={{ background: T.card, borderRadius: 16, padding: 16, border: `1px solid ${T.line}`, marginBottom: 18 }}>
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 4 }}>
                  <span style={{ fontFamily: fontDisplay, fontSize: 16, fontWeight: 700 }}>Equity</span>
                  <RangeTabs value={equityRange} onChange={setEquityRange} />
                </div>
                {equityHistory.length >= 2 && (
                  <div style={{ fontFamily: fontMono, fontSize: 12, marginBottom: 4, color: equityHistory[equityHistory.length - 1].equity >= equityHistory[0].equity ? T.gain : T.loss }}>
                    {(() => { const a = equityHistory[0].equity, b = equityHistory[equityHistory.length - 1].equity; const d = b / a - 1; return `${d >= 0 ? "+" : ""}${(d * 100).toFixed(1)}% over ${equityRange}`; })()}
                  </div>
                )}
                <InteractiveChart points={equityHistory.map((p) => ({ t: p.date, v: p.equity }))} range={equityRange} formatY={fmt} />
                {policy.targetAmount > 0 && account && (
                  <div style={{ marginTop: 10 }}>
                    <div style={{ display: "flex", justifyContent: "space-between", fontSize: 11.5, color: T.inkSoft, marginBottom: 4 }}>
                      <span>Progress to {fmt(policy.targetAmount)}</span>
                      <span style={{ fontFamily: fontMono }}>{Math.min((account.equity / policy.targetAmount) * 100, 100).toFixed(0)}%</span>
                    </div>
                    <div style={{ height: 6, borderRadius: 6, background: T.chipOff, overflow: "hidden" }}>
                      <div style={{ height: "100%", width: `${Math.min((account.equity / policy.targetAmount) * 100, 100)}%`, background: T.brand, borderRadius: 6 }} />
                    </div>
                  </div>
                )}
              </div>
            )}
            {goal && (
              <div style={{ background: T.card, borderRadius: 16, padding: 16, border: `1px solid ${T.line}`, marginBottom: 18 }}>
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 6 }}>
                  <span style={{ fontFamily: fontDisplay, fontSize: 16, fontWeight: 700 }}>Goal check</span>
                  <span style={{
                    fontFamily: fontMono, fontSize: 11, fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase",
                    color: goal.status === "comfortable" ? T.gain : goal.status === "achievable" ? T.caution : T.loss,
                  }}>
                    {goal.status} · {pct(goal.requiredCagr, 1)}/yr needed
                  </span>
                </div>
                <div style={{ fontSize: 12.5, color: T.inkSoft, lineHeight: 1.5 }}>{goal.note}</div>
              </div>
            )}
            <h3 style={{ fontFamily: fontDisplay, fontSize: 19, margin: "6px 0 12px" }}>Allocation vs your caps</h3>
            <div style={{ background: T.card, borderRadius: 16, padding: 16, border: `1px solid ${T.line}` }}>
              {!account ? <Spinner label="Loading portfolio…" /> : (
                <React.Fragment>
                  {policy.sectors.map((s) => {
                    const v = sectorAlloc[s] || 0;
                    const p = totalValue ? (v / totalValue) * 100 : 0;
                    const over = p > r.maxSector;
                    return (
                      <div key={s} style={{ marginBottom: 12 }}>
                        <div style={{ display: "flex", justifyContent: "space-between", fontSize: 12.5, marginBottom: 4 }}>
                          <span style={{ fontWeight: 600 }}>{s}</span>
                          <span style={{ fontFamily: fontMono, color: over ? T.loss : T.inkSoft }}>{p.toFixed(0)}% / {r.maxSector}%</span>
                        </div>
                        <div style={{ height: 8, borderRadius: 8, background: T.chipOff, overflow: "hidden" }}>
                          <div style={{ height: "100%", width: `${Math.min((p / r.maxSector) * 100, 100)}%`, background: over ? T.loss : T.brand, borderRadius: 8 }} />
                        </div>
                      </div>
                    );
                  })}
                  <div style={{ fontSize: 12, color: T.inkSoft, marginTop: 4 }}>
                    Cash buffer target: {r.cashBuffer}% · currently {totalValue ? ((account.cash / totalValue) * 100).toFixed(0) : 0}%
                  </div>
                </React.Fragment>
              )}
            </div>

            {drift.some((d) => d.action !== "hold") && (
              <React.Fragment>
                <h3 style={{ fontFamily: fontDisplay, fontSize: 19, margin: "22px 0 12px" }}>Rebalance nudges</h3>
                <div style={{ background: T.card, borderRadius: 16, padding: "6px 16px", border: `1px solid ${T.line}` }}>
                  {drift.filter((d) => d.action !== "hold").map((d) => (
                    <div key={d.sector} style={{ display: "flex", justifyContent: "space-between", padding: "10px 0", borderBottom: `1px solid ${T.line}`, fontSize: 13 }}>
                      <span>
                        <b>{d.sector}</b>{d.offPolicy && <span style={{ color: T.loss }}> · off-policy</span>}
                        <span style={{ color: T.inkSoft }}> — {pct(d.actualPct)} vs {pct(d.targetPct)} target</span>
                      </span>
                      <span style={{ fontFamily: fontMono, fontWeight: 700, color: d.action === "trim" ? T.loss : T.gain }}>
                        {d.action} {fmt(d.amount)}
                      </span>
                    </div>
                  ))}
                </div>
              </React.Fragment>
            )}

            <h3 style={{ fontFamily: fontDisplay, fontSize: 19, margin: "22px 0 12px" }}>Holdings</h3>
            {positions.length === 0 ? (
              <div style={{ background: T.card, borderRadius: 16, padding: 24, border: `1px dashed ${T.line}`, textAlign: "center", color: T.inkSoft, fontSize: 14 }}>
                Nothing yet. Start from <b>Ideas</b> — they're already filtered to fit your plan.
              </div>
            ) : (
              positions.map((p) => (
                <div key={p.symbol} style={{ background: T.card, borderRadius: 14, padding: "13px 16px", border: `1px solid ${T.line}`, marginBottom: 8 }}>
                  <div
                    onClick={() => { setSellSym(sellSym === p.symbol ? null : p.symbol); setSellAmt(Math.min(500, Math.round(p.value))); }}
                    style={{ display: "flex", justifyContent: "space-between", alignItems: "center", cursor: "pointer" }}>
                    <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
                      <Logo symbol={p.symbol} size={30} src={p.logoUrl} />
                      <div>
                        <span style={{ fontFamily: fontMono, fontWeight: 700 }}>{p.symbol}</span>
                        <span style={{ fontSize: 12.5, color: T.inkSoft, marginLeft: 8 }}>{p.sector || "—"}</span>
                      </div>
                    </div>
                    <div style={{ textAlign: "right" }}>
                      <div style={{ fontFamily: fontMono, fontSize: 14 }}>{fmt(p.value)}</div>
                      {p.unrealizedPlPct != null && (
                        <div style={{ fontFamily: fontMono, fontSize: 11.5, color: p.unrealizedPlPct >= 0 ? T.gain : T.loss }}>
                          {p.unrealizedPlPct >= 0 ? "+" : ""}{(p.unrealizedPlPct * 100).toFixed(1)}%
                        </div>
                      )}
                    </div>
                  </div>
                  {sellSym === p.symbol && (
                    <div style={{ display: "flex", gap: 8, marginTop: 12, alignItems: "center", animation: "rise 0.25s ease" }}>
                      <input
                        type="number"
                        value={sellAmt}
                        onChange={(e) => setSellAmt(Number(e.target.value))}
                        style={{ width: 110, padding: "9px 12px", borderRadius: 10, border: `1.5px solid ${T.line}`, fontSize: 14, fontFamily: fontMono }}
                      />
                      <Btn small kind="danger" disabled={selling || !(sellAmt > 0) || sellAmt > p.value + 0.01} onClick={() => sellPosition(p.symbol, sellAmt, sellAmt >= p.value - 0.01)}>
                        {selling ? "Selling…" : "Sell"}
                      </Btn>
                      <Btn small kind="ghost" onClick={() => sellPosition(p.symbol, p.value, true)} disabled={selling}>Sell all</Btn>
                    </div>
                  )}
                </div>
              ))
            )}
          </React.Fragment>
        )}

        {/* ---- IDEAS ---- */}
        {tab === "ideas" && (
          <React.Fragment>
            <h3 style={{ fontFamily: fontDisplay, fontSize: 19, margin: "6px 0 4px" }}>Ideas that fit your plan</h3>
            <p style={{ fontSize: 13, color: T.inkSoft, marginBottom: 14 }}>Filtered to your sectors and volatility budget, ranked by where you're most underweight. Live data.</p>
            {ideas === null && <Spinner label="Fetching live quotes…" />}
            {ideas && ideas.map((s) => (
              <div key={s.symbol} style={{ background: T.card, borderRadius: 16, padding: 16, border: `1px solid ${T.line}`, marginBottom: 10 }}>
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
                  <div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 0 }}>
                    <Logo symbol={s.symbol} size={30} src={s.logoUrl} />
                    <div style={{ minWidth: 0 }}>
                      <span style={{ fontFamily: fontMono, fontWeight: 700, fontSize: 16 }}>{s.symbol}</span>
                      <span style={{ fontSize: 13, color: T.inkSoft, marginLeft: 8 }}>{s.name}</span>
                    </div>
                  </div>
                  <span style={{ fontFamily: fontMono, fontSize: 13, flexShrink: 0, color: s.ret1y >= 0 ? T.gain : T.loss }}>
                    {s.ret1y > 0 ? "+" : ""}{(s.ret1y * 100).toFixed(1)}% 1y
                  </span>
                </div>
                <div style={{ fontSize: 13, color: T.inkSoft, margin: "8px 0 12px", lineHeight: 1.5 }}>{s.note}</div>
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
                  <span style={{ fontFamily: fontMono, fontSize: 11.5, color: T.inkSoft }}>
                    {s.sector} · ${s.price?.toFixed(2)} · vol {(s.annVol * 100).toFixed(0)}%{s.beta != null ? ` · β ${s.beta.toFixed(2)}` : ""}
                  </span>
                  <Btn small onClick={() => { setTab("add"); setTicker(s.symbol); startCheck(s.symbol, 2000); }}>Check fit</Btn>
                </div>
              </div>
            ))}
            {ideas && ideas.length === 0 && (
              <div style={{ color: T.inkSoft, fontSize: 14 }}>No new ideas within your policy right now — your plan sectors are well covered.</div>
            )}
          </React.Fragment>
        )}

        {/* ---- ADD / GUARDRAIL ---- */}
        {tab === "add" && (
          <React.Fragment>
            <h3 style={{ fontFamily: fontDisplay, fontSize: 19, margin: "6px 0 12px" }}>Add a trade</h3>
            <div style={{ position: "relative", marginBottom: 10 }}>
              <div style={{ display: "flex", gap: 8 }}>
                <input
                  value={ticker}
                  onChange={(e) => {
                    setTicker(e.target.value);
                    if (stockDetail && stockDetail !== "loading" && stockDetail.symbol !== e.target.value.trim().toUpperCase()) setStockDetail(null);
                  }}
                  placeholder="Search name or ticker…"
                  style={{ flex: 1, padding: "13px 14px", borderRadius: 12, border: `1.5px solid ${T.line}`, fontSize: 15, background: T.card, minWidth: 0 }}
                />
                <input
                  type="number"
                  value={amount}
                  onChange={(e) => setAmount(Number(e.target.value))}
                  style={{ width: 110, padding: "13px 14px", borderRadius: 12, border: `1.5px solid ${T.line}`, fontSize: 15, fontFamily: fontMono, background: T.card }}
                />
              </div>
              {searchResults.length > 0 && (
                <div style={{ position: "absolute", top: "100%", left: 0, right: 118, zIndex: 10, background: T.card, border: `1px solid ${T.line}`, borderRadius: 12, marginTop: 4, boxShadow: `0 8px 24px ${T.shadow}`, overflow: "hidden" }}>
                  {searchResults.map((s) => (
                    <button key={s.symbol} onClick={() => selectStock(s.symbol)}
                      style={{ display: "flex", alignItems: "center", gap: 10, width: "100%", padding: "10px 12px", border: "none", borderBottom: `1px solid ${T.line}`, background: "transparent", cursor: "pointer", textAlign: "left" }}>
                      <Logo symbol={s.symbol} size={26} src={s.logoUrl} />
                      <span style={{ fontFamily: fontMono, fontWeight: 700, fontSize: 13 }}>{s.symbol}</span>
                      <span style={{ fontSize: 12.5, color: T.inkSoft, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1 }}>{s.name}</span>
                      <span style={{ fontFamily: fontMono, fontSize: 10, color: T.inkSoft }}>{s.exchange}</span>
                    </button>
                  ))}
                </div>
              )}
            </div>

            {stockDetail === "loading" && <Spinner label="Loading company data…" />}
            {stockDetail && stockDetail !== "loading" && (
              <div style={{ background: T.card, borderRadius: 16, padding: 16, border: `1px solid ${T.line}`, marginBottom: 10 }}>
                <div style={{ display: "flex", gap: 12, alignItems: "center", marginBottom: 10 }}>
                  <Logo symbol={stockDetail.symbol} src={stockDetail.logoUrl} />
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontWeight: 700, fontSize: 15, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{stockDetail.name || stockDetail.symbol}</div>
                    <div style={{ fontFamily: fontMono, fontSize: 11.5, color: T.inkSoft }}>{stockDetail.symbol} · {stockDetail.sector || "—"} · {fmtBig(stockDetail.marketCap)}</div>
                  </div>
                  <div style={{ textAlign: "right" }}>
                    <div style={{ fontFamily: fontMono, fontWeight: 700 }}>${stockDetail.price?.toFixed(2)}</div>
                    <div style={{ fontFamily: fontMono, fontSize: 11.5, color: stockDetail.ret1y >= 0 ? T.gain : T.loss }}>
                      {stockDetail.ret1y >= 0 ? "+" : ""}{(stockDetail.ret1y * 100).toFixed(1)}% 1y
                    </div>
                  </div>
                </div>
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 4 }}>
                  {stockDetail.closes?.length >= 2 ? (
                    <span style={{ fontFamily: fontMono, fontSize: 11.5, color: stockDetail.closes[stockDetail.closes.length - 1] >= stockDetail.closes[0] ? T.gain : T.loss }}>
                      {(() => { const a = stockDetail.closes[0], b = stockDetail.closes[stockDetail.closes.length - 1]; const d = b / a - 1; return `${d >= 0 ? "+" : ""}${(d * 100).toFixed(1)}% over ${detailRange}`; })()}
                    </span>
                  ) : <span />}
                  <RangeTabs value={detailRange} onChange={setDetailRange} />
                </div>
                <InteractiveChart
                  points={(stockDetail.dates || []).map((t, i) => ({ t, v: stockDetail.closes[i] }))}
                  range={detailRange} height={120}
                  formatY={(v) => "$" + v.toFixed(2)} />
                <div style={{ display: "flex", gap: 14, fontFamily: fontMono, fontSize: 11, color: T.inkSoft, margin: "8px 0" }}>
                  <span>vol {(stockDetail.annVol * 100).toFixed(0)}%/yr</span>
                  {stockDetail.beta != null && <span>β {stockDetail.beta.toFixed(2)}</span>}
                  {stockDetail.dividendYield != null && <span>yield {(stockDetail.dividendYield * 100).toFixed(1)}%</span>}
                  {stockDetail.institutionsPctHeld != null && <span>inst {(stockDetail.institutionsPctHeld * 100).toFixed(0)}%</span>}
                </div>
                {stockDetail.analyst && (
                  <div style={{ fontSize: 12.5, padding: "8px 0", borderTop: `1px solid ${T.line}` }}>
                    <b>Analysts ({stockDetail.analyst.analysts || "?"})</b>: {stockDetail.analyst.recommendation || "—"} ·
                    target ${stockDetail.analyst.targetMean?.toFixed(0)}
                    {stockDetail.price > 0 && (
                      <span style={{ fontFamily: fontMono, marginLeft: 6, color: stockDetail.analyst.targetMean >= stockDetail.price ? T.gain : T.loss }}>
                        ({stockDetail.analyst.targetMean >= stockDetail.price ? "+" : ""}{((stockDetail.analyst.targetMean / stockDetail.price - 1) * 100).toFixed(0)}%)
                      </span>
                    )}
                    {stockDetail.analyst.targetLow != null && <span style={{ color: T.inkSoft }}> · range ${stockDetail.analyst.targetLow?.toFixed(0)}–${stockDetail.analyst.targetHigh?.toFixed(0)}</span>}
                  </div>
                )}
                {stockDetail.topInstitutions?.length > 0 && (
                  <div style={{ fontSize: 12, padding: "8px 0", borderTop: `1px solid ${T.line}` }}>
                    <b>Top holders</b>
                    {stockDetail.topInstitutions.slice(0, 3).map((h) => (
                      <div key={h.org} style={{ display: "flex", justifyContent: "space-between", color: T.inkSoft, marginTop: 3 }}>
                        <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{h.org}</span>
                        <span style={{ fontFamily: fontMono, flexShrink: 0, marginLeft: 8 }}>
                          {h.pctHeld != null ? (h.pctHeld * 100).toFixed(1) + "%" : ""}
                          {h.pctChange != null && (
                            <span style={{ color: h.pctChange >= 0 ? T.gain : T.loss }}> {h.pctChange >= 0 ? "▲" : "▼"}{Math.abs(h.pctChange * 100).toFixed(0)}%</span>
                          )}
                        </span>
                      </div>
                    ))}
                    {stockDetail.insiderNetPct3m != null && (
                      <div style={{ marginTop: 4, color: T.inkSoft }}>
                        Insider net activity (3m): <span style={{ fontFamily: fontMono, color: stockDetail.insiderNetPct3m >= 0 ? T.gain : T.loss }}>{stockDetail.insiderNetPct3m >= 0 ? "+" : ""}{(stockDetail.insiderNetPct3m * 100).toFixed(1)}%</span>
                      </div>
                    )}
                  </div>
                )}
                {stockDetail.description && (
                  <div style={{ fontSize: 12, color: T.inkSoft, lineHeight: 1.5, paddingTop: 8, borderTop: `1px solid ${T.line}` }}>
                    {stockDetail.description}…
                  </div>
                )}
              </div>
            )}

            <Btn onClick={() => startCheck(ticker.trim(), amount)} disabled={!ticker.trim() || amount <= 0 || checking}>
              {checking ? "Fetching live data…" : "Run guardrail check"}
            </Btn>

            {checkError && (
              <div style={{ marginTop: 14, fontSize: 13.5, color: T.loss }}>{checkError}</div>
            )}

            {checkResult && (
              <div style={{ marginTop: 18, background: T.card, borderRadius: 18, border: `1px solid ${T.line}`, padding: 18 }}>
                <div style={{ fontSize: 13, color: T.inkSoft, marginBottom: 2 }}>Checking against your plan</div>
                <div style={{ fontFamily: fontMono, fontWeight: 700, fontSize: 17, marginBottom: 12 }}>
                  {fmt(amount)} · {checkResult.stock.symbol}
                  <span style={{ fontWeight: 500, fontSize: 12, color: T.inkSoft, marginLeft: 8 }}>
                    ${checkResult.stock.price?.toFixed(2)} · vol {(checkResult.stock.annVol * 100).toFixed(0)}%/yr
                  </span>
                </div>
                {(() => {
                  // Cooling-off is a circuit breaker on the trader, not a check on
                  // the trade — pull it out of the checklist and show it as a banner.
                  const cooloff = checkResult.checks.find((c) => c.id === "cooloff");
                  const rows = checkResult.checks.filter((c) => c.id !== "cooloff");
                  return (
                    <React.Fragment>
                      {cooloff && (
                        <div style={{ background: T.warnBg, border: `1.5px solid ${T.loss}`, borderRadius: 12, padding: "12px 14px", margin: "4px 0 10px" }}>
                          <div style={{ fontWeight: 700, fontSize: 13.5, color: T.loss, marginBottom: 4 }}>Trading paused — cooling-off</div>
                          <div style={{ fontSize: 12.5, lineHeight: 1.5 }}>{cooloff.detail}</div>
                          <div style={{ fontSize: 12, color: T.inkSoft, marginTop: 6 }}>
                            The checks below aren't the problem — your override streak is. Revising your plan (Profile → Edit plan) resets this.
                          </div>
                        </div>
                      )}
                      {rows.map((c, i) => (
                        <div key={c.id || c.label} style={{ display: i < revealed ? "flex" : "none", gap: 10, padding: "9px 0", borderTop: `1px solid ${T.line}`, animation: "rise 0.35s ease" }}>
                          <Dot status={c.status} />
                          <div>
                            <div style={{ fontSize: 13.5, fontWeight: 600 }}>{c.label}</div>
                            <div style={{ fontSize: 12.5, color: T.inkSoft, lineHeight: 1.45 }}>{c.detail}</div>
                          </div>
                        </div>
                      ))}
                    </React.Fragment>
                  );
                })()}
                {revealed > checkResult.checks.length && (
                  <div style={{ textAlign: "center", padding: "16px 0 6px", animation: "rise 0.4s ease" }}>
                    <Stamp verdict={checkResult.verdict} />

                    <div style={{ marginTop: 16, textAlign: "left" }}>
                      {!coachText && (
                        <Btn kind="ghost" onClick={askCoach} disabled={coachLoading}>
                          {coachLoading ? "Coach is thinking…" : "What would my coach say?"}
                        </Btn>
                      )}
                      {coachText && (
                        <div style={{ background: T.paper, borderRadius: 12, padding: 14, fontSize: 13.5, lineHeight: 1.55, borderLeft: `3px solid ${T.brand}` }}>
                          <div style={{ fontFamily: fontMono, fontSize: 10.5, letterSpacing: "0.15em", color: T.inkSoft, marginBottom: 6 }}>COACH</div>
                          {coachText}
                        </div>
                      )}

                      {/* Confirm step: what you're buying + optional exits */}
                      <div style={{ marginTop: 12, background: T.paper, borderRadius: 12, padding: 12 }}>
                        <div style={{ display: "flex", justifyContent: "space-between", fontSize: 12.5, marginBottom: 8 }}>
                          <span style={{ color: T.inkSoft }}>Order</span>
                          <span style={{ fontFamily: fontMono }}>
                            ≈ {(amount / checkResult.stock.price).toFixed(2)} sh @ ${checkResult.stock.price.toFixed(2)}
                          </span>
                        </div>
                        <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
                          <div style={{ flex: 1 }}>
                            <div style={{ fontSize: 10.5, color: T.inkSoft, marginBottom: 3 }}>Take profit % (opt.)</div>
                            <input type="number" value={tpPct} placeholder="e.g. 15"
                              onChange={(e) => setTpPct(e.target.value)}
                              style={{ width: "100%", padding: "8px 10px", borderRadius: 8, border: `1.5px solid ${T.line}`, fontFamily: fontMono, fontSize: 13 }} />
                          </div>
                          <div style={{ flex: 1 }}>
                            <div style={{ fontSize: 10.5, color: T.inkSoft, marginBottom: 3 }}>Stop loss % (opt.)</div>
                            <input type="number" value={slPct} placeholder="e.g. 8"
                              onChange={(e) => setSlPct(e.target.value)}
                              style={{ width: "100%", padding: "8px 10px", borderRadius: 8, border: `1.5px solid ${T.line}`, fontFamily: fontMono, fontSize: 13 }} />
                          </div>
                        </div>
                        {(tpPct || slPct) && (
                          <div style={{ fontFamily: fontMono, fontSize: 11, color: T.inkSoft, marginTop: 6 }}>
                            {tpPct ? `TP $${(checkResult.stock.price * (1 + Number(tpPct) / 100)).toFixed(2)}` : ""}
                            {tpPct && slPct ? " · " : ""}
                            {slPct ? `SL $${(checkResult.stock.price * (1 - Number(slPct) / 100)).toFixed(2)}` : ""}
                            {account?.broker !== "simulator" ? " — placed as a bracket at Alpaca (whole shares)" : " — watched automatically"}
                          </div>
                        )}
                        <div style={{ fontSize: 11, color: T.inkSoft, marginTop: 6 }}>
                          Market orders fill at the next open when the market is closed — cancel any time before that from Portfolio → Open orders.
                        </div>
                      </div>

                      <div style={{ marginTop: 12 }}>
                        {checkResult.verdict !== "OUT OF POLICY" ? (
                          <Btn onClick={() => executeTrade()} disabled={placing}>
                            {placing ? "Placing order…" : "Confirm order"}
                          </Btn>
                        ) : (
                          <React.Fragment>
                            <textarea
                              value={overrideReason}
                              onChange={(e) => setOverrideReason(e.target.value)}
                              placeholder="To override, write your reasoning. It goes in your journal — future you will read it."
                              style={{ width: "100%", minHeight: 70, padding: 12, borderRadius: 12, fontSize: 13.5, border: `1.5px solid ${T.line}`, marginBottom: 10 }}
                            />
                            <Btn kind="danger" disabled={overrideReason.trim().length < 20 || placing} onClick={() => executeTrade(overrideReason)}>
                              {placing ? "Placing order…" : "Override my own rules"}
                            </Btn>
                          </React.Fragment>
                        )}
                        <DisclaimerNote style={{ marginTop: 10 }} />
                      </div>
                    </div>
                  </div>
                )}
              </div>
            )}
          </React.Fragment>
        )}

        {/* ---- JOURNAL ---- */}
        {tab === "journal" && (
          <React.Fragment>
            <h3 style={{ fontFamily: fontDisplay, fontSize: 19, margin: "6px 0 12px" }}>Decision journal</h3>
            {journal.length === 0 ? (
              <div style={{ color: T.inkSoft, fontSize: 14 }}>Every trade you place lands here — with its verdict, and your reasoning if you overrode the rules.</div>
            ) : (() => {
              const cutoff = { "7d": 7, "30d": 30, "90d": 90, "1y": 365 }[journalPeriod];
              const cutoffTs = cutoff ? Date.now() - cutoff * 864e5 : 0;
              const kindOf = (j) => j.type || "trade";
              const filtered = journal.filter((j) =>
                (journalKind === "all" || kindOf(j) === journalKind) &&
                (!cutoffTs || new Date(j.date).getTime() >= cutoffTs)
              );
              const chipRow = (opts, value, setValue) => (
                <div style={{ display: "flex", gap: 6, overflowX: "auto", paddingBottom: 2 }}>
                  {opts.map(([val, label]) => (
                    <button key={val} onClick={() => setValue(val)}
                      style={{
                        flexShrink: 0, padding: "6px 12px", borderRadius: 999, cursor: "pointer",
                        border: `1px solid ${value === val ? T.brand : T.line}`,
                        background: value === val ? T.chipOn : T.chipOff, color: value === val ? T.onAccent : T.ink,
                        fontFamily: fontBody, fontSize: 12.5, fontWeight: 600,
                      }}>
                      {label}
                    </button>
                  ))}
                </div>
              );
              return (
                <React.Fragment>
                  <div style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 14 }}>
                    {chipRow([["all", "All"], ["trade", "Trades"], ["robo", "Robo"], ["cash", "Cash"], ["plan", "Plan"]], journalKind, setJournalKind)}
                    {chipRow([["all", "All time"], ["7d", "7D"], ["30d", "30D"], ["90d", "90D"], ["1y", "1Y"]], journalPeriod, setJournalPeriod)}
                  </div>
                  {filtered.length === 0 ? (
                    <div style={{ color: T.inkSoft, fontSize: 14 }}>
                      No entries match these filters.{" "}
                      <button onClick={() => { setJournalKind("all"); setJournalPeriod("all"); }}
                        style={{ background: "none", border: "none", color: T.brand, fontWeight: 600, padding: 0, cursor: "pointer", fontSize: 14 }}>Clear filters</button>
                    </div>
                  ) : (
                    <div style={{ fontSize: 11.5, color: T.inkSoft, marginBottom: 10 }}>
                      Showing {filtered.length} of {journal.length}
                    </div>
                  )}
                  {filtered.map((j, i) => {
                const when = new Date(j.date).toLocaleDateString("en-SG", { day: "numeric", month: "short", year: "numeric" });
                const rowKey = `${j.date}|${j.type || "trade"}|${j.symbol}|${j.amount}`;
                const expandable = hasJournalDetail(j);
                const open = expandedEntries.has(rowKey);
                const toggle = () => expandable && toggleExpanded(rowKey);
                const chevron = expandable && (
                  <span style={{ fontFamily: fontMono, fontSize: 11, color: T.inkSoft, marginLeft: 8, flexShrink: 0 }}>{open ? "▴" : "▾"}</span>
                );

                if (j.type === "plan")
                  return (
                    <div key={i} onClick={toggle}
                      style={{ background: T.card, borderRadius: 14, padding: 14, border: `1px solid ${T.line}`, borderLeftWidth: 4, borderLeftColor: T.brand, marginBottom: 8, cursor: expandable ? "pointer" : "default" }}>
                      <div style={{ display: "flex", justifyContent: "space-between", fontSize: 13 }}>
                        <span style={{ fontWeight: 700, color: T.brand }}>Plan revised</span>
                        <span style={{ display: "flex", alignItems: "center" }}>
                          <span style={{ fontFamily: fontMono, fontSize: 11, color: T.brand }}>PLAN</span>
                          {chevron}
                        </span>
                      </div>
                      <div style={{ fontSize: 12.5, color: T.inkSoft, marginTop: 4 }}>{j.detail}</div>
                      <div style={{ fontSize: 11, color: T.inkSoft, marginTop: 4 }}>{when}</div>
                      {open && <JournalDetailPanel j={j} />}
                    </div>
                  );
                if (j.type === "cash")
                  return (
                    <div key={i} onClick={toggle}
                      style={{ background: T.card, borderRadius: 14, padding: 14, border: `1px solid ${T.line}`, borderLeftWidth: 4, borderLeftColor: j.direction === "deposit" ? T.gain : T.caution, marginBottom: 8, cursor: expandable ? "pointer" : "default" }}>
                      <div style={{ display: "flex", justifyContent: "space-between", fontSize: 13 }}>
                        <span style={{ fontWeight: 700 }}>{j.direction === "deposit" ? "Funds added" : "Funds withdrawn"}</span>
                        <span style={{ display: "flex", alignItems: "center" }}>
                          <span style={{ fontFamily: fontMono, fontSize: 13, color: j.direction === "deposit" ? T.gain : T.caution }}>
                            {j.direction === "deposit" ? "+" : "−"}{fmt(j.amount)}
                          </span>
                          {chevron}
                        </span>
                      </div>
                      <div style={{ fontSize: 11, color: T.inkSoft, marginTop: 4 }}>{when}</div>
                      {open && <JournalDetailPanel j={j} />}
                    </div>
                  );
                const isRobo = j.type === "robo";
                const vColor = isRobo
                  ? (/(BLOCKED|DECLINED)/.test(j.verdict) ? T.loss : T.brand)
                  : j.verdict === "FITS YOUR PLAN" ? T.gain : j.verdict === "CAUTION" ? T.caution : j.verdict === "SOLD" ? T.inkSoft : T.loss;
                return (
                  <div key={i} onClick={toggle}
                    style={{ background: T.card, borderRadius: 14, padding: 14, border: `1px solid ${T.line}`, marginBottom: 8, cursor: expandable ? "pointer" : "default", ...(isRobo ? { borderLeftWidth: 4, borderLeftColor: T.brand } : {}) }}>
                    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: 13.5 }}>
                      <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
                        <Logo symbol={j.symbol} size={24} src={logoMap[j.symbol]} />
                        <span><b style={{ fontFamily: fontMono }}>{j.symbol}</b> · {fmt(j.amount)}</span>
                      </span>
                      <span style={{ display: "flex", alignItems: "center" }}>
                        <span style={{ fontFamily: fontMono, fontSize: 11, color: vColor }}>{j.verdict}</span>
                        {chevron}
                      </span>
                    </div>
                    {j.reason && <div style={{ fontSize: 12.5, color: T.inkSoft, marginTop: 6, fontStyle: "italic" }}>"{j.reason}"</div>}
                    <div style={{ fontSize: 11, color: T.inkSoft, marginTop: 4 }}>{when}</div>
                    {open && <JournalDetailPanel j={j} />}
                  </div>
                );
              })}
                </React.Fragment>
              );
            })()}
          </React.Fragment>
        )}
        {/* ---- PROFILE ---- */}
        {tab === "profile" && me && (
          <React.Fragment>
            <h3 style={{ fontFamily: fontDisplay, fontSize: 19, margin: "6px 0 4px" }}>Profile</h3>
            <p style={{ fontSize: 13, color: T.inkSoft, marginBottom: 14 }}>{me.email}</p>

            <div style={{ background: T.card, borderRadius: 16, padding: 16, border: `1px solid ${T.line}`, marginBottom: 14 }}>
              <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
                <span style={{ fontWeight: 700, fontSize: 14 }}>Your plan</span>
                <Btn small kind="ghost" onClick={() => { setStep(0); setScreen("onboard"); }}>Edit plan</Btn>
              </div>
              <div style={{ fontSize: 13, marginBottom: 4 }}>
                <b>{policy.risk}</b> · {policy.sectors.join(", ")}
              </div>
              <div style={{ fontSize: 12.5, color: T.inkSoft, lineHeight: 1.5 }}>
                {policy.goal || "No goal set"}{policy.targetAmount ? ` · target ${fmt(policy.targetAmount)}` : ""} · {policy.horizon}
              </div>
            </div>

            <div style={{ background: T.card, borderRadius: 16, padding: 16, border: `1px solid ${T.line}`, marginBottom: 14 }}>
              <div style={{ fontWeight: 700, fontSize: 14, marginBottom: 10 }}>Appearance</div>
              <div style={{ display: "flex", gap: 8 }}>
                {[["system", "System"], ["light", "Light"], ["dark", "Dark"]].map(([val, label]) => (
                  <button key={val} onClick={() => setTheme(val)}
                    style={{
                      flex: 1, padding: "9px 0", borderRadius: 10, border: `1.5px solid ${theme === val ? T.brand : T.line}`,
                      background: theme === val ? T.chipOn : T.chipOff, color: theme === val ? T.onAccent : T.ink,
                      fontFamily: fontBody, fontSize: 13, fontWeight: 600, cursor: "pointer",
                    }}>
                    {label}
                  </button>
                ))}
              </div>
            </div>

            <div style={{ background: T.card, borderRadius: 16, padding: 16, border: `1px solid ${T.line}`, marginBottom: 14 }}>
              <div style={{ fontWeight: 700, fontSize: 14, marginBottom: 10 }}>Trading account</div>
              {[
                ["sim", "Simulator", "Built-in $25k paper portfolio. No keys needed.", true],
                ["paper", "Alpaca Paper", "Your Alpaca paper account — realistic fills, fake money.", me.hasPaperKeys],
                ["live", "Alpaca Live", "Real money. Guardrails still check every buy.", me.hasLiveKeys],
              ].map(([id, label, blurb, enabled]) => (
                <button key={id} onClick={() => enabled && switchMode(id)} disabled={!enabled || savingBroker}
                  style={{
                    display: "block", width: "100%", textAlign: "left", marginBottom: 8, padding: "12px 14px",
                    borderRadius: 12, cursor: enabled ? "pointer" : "default", background: T.card, opacity: enabled ? 1 : 0.45,
                    border: me.mode === id ? `2px solid ${id === "live" ? T.loss : T.brand}` : `1.5px solid ${T.line}`,
                  }}>
                  <div style={{ display: "flex", justifyContent: "space-between" }}>
                    <span style={{ fontWeight: 700, fontSize: 13.5, color: id === "live" ? T.loss : T.ink }}>{label}</span>
                    <span style={{ fontFamily: fontMono, fontSize: 10.5, color: me.mode === id ? T.gain : T.brand }}>
                      {me.mode === id ? "● ACTIVE" : enabled ? "TAP TO SWITCH" : ""}
                    </span>
                  </div>
                  <div style={{ fontSize: 12, color: T.inkSoft, marginTop: 3 }}>{blurb}{!enabled && " Add keys below to enable."}</div>
                </button>
              ))}
            </div>

            <div style={{ background: T.card, borderRadius: 16, padding: 16, border: `1px solid ${T.line}`, marginBottom: 14 }}>
              <div style={{ fontWeight: 700, fontSize: 14, marginBottom: 4 }}>Alpaca paper keys {me.hasPaperKeys && <span style={{ color: T.gain, fontFamily: fontMono, fontSize: 11 }}>· SET</span>}</div>
              <p style={{ fontSize: 12, color: T.inkSoft, margin: "0 0 10px" }}>From app.alpaca.markets → Paper trading. Stored encrypted; verified before saving.</p>
              <Field placeholder="Paper API Key ID" value={keys.paperKeyId} autoComplete="off"
                onChange={(e) => setKeys((k) => ({ ...k, paperKeyId: e.target.value }))} />
              <Field type="password" placeholder="Paper API Secret" value={keys.paperSecret} autoComplete="off"
                onChange={(e) => setKeys((k) => ({ ...k, paperSecret: e.target.value }))} />
              <Btn small disabled={savingBroker || !keys.paperKeyId || !keys.paperSecret}
                onClick={() => saveBroker({ paperKeyId: keys.paperKeyId, paperSecret: keys.paperSecret, mode: "paper" })}>
                {savingBroker ? "Verifying…" : "Save & switch to paper"}
              </Btn>
            </div>

            <div style={{ background: T.card, borderRadius: 16, padding: 16, border: `1.5px solid ${T.loss}`, marginBottom: 14 }}>
              <div style={{ fontWeight: 700, fontSize: 14, marginBottom: 4, color: T.loss }}>Alpaca live keys {me.hasLiveKeys && <span style={{ fontFamily: fontMono, fontSize: 11 }}>· SET</span>}</div>
              <p style={{ fontSize: 12, color: T.inkSoft, margin: "0 0 10px" }}>
                Live mode places <b>real-money market orders</b>. The same guardrails apply, but losses are real. Use API keys with trading permission only — never your login password.
              </p>
              <Field placeholder="Live API Key ID" value={keys.liveKeyId} autoComplete="off"
                onChange={(e) => setKeys((k) => ({ ...k, liveKeyId: e.target.value }))} />
              <Field type="password" placeholder="Live API Secret" value={keys.liveSecret} autoComplete="off"
                onChange={(e) => setKeys((k) => ({ ...k, liveSecret: e.target.value }))} />
              <Btn small kind="danger" disabled={savingBroker || !keys.liveKeyId || !keys.liveSecret}
                onClick={() => saveBroker({ liveKeyId: keys.liveKeyId, liveSecret: keys.liveSecret })}>
                {savingBroker ? "Verifying…" : "Save live keys"}
              </Btn>
            </div>

            <div style={{ background: T.card, borderRadius: 16, padding: 16, border: `1px solid ${T.line}`, marginBottom: 14 }}>
              <div style={{ fontWeight: 700, fontSize: 14, marginBottom: 10 }}>Security</div>

              <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "6px 0", fontSize: 13 }}>
                <span>Email {me.emailVerified
                  ? <span style={{ color: T.gain, fontFamily: fontMono, fontSize: 11 }}>· VERIFIED</span>
                  : <span style={{ color: T.caution, fontFamily: fontMono, fontSize: 11 }}>· UNVERIFIED</span>}
                </span>
                {!me.emailVerified && <Btn small kind="ghost" onClick={resendVerification}>Resend email</Btn>}
              </div>
              {devVerifyLink && (
                <div style={{ padding: 10, background: T.noticeBg, borderRadius: 10, fontSize: 11, wordBreak: "break-all", marginBottom: 8 }}>
                  <b>Dev:</b> <a href={devVerifyLink}>{devVerifyLink}</a>
                </div>
              )}

              <div style={{ borderTop: `1px solid ${T.line}`, marginTop: 6, paddingTop: 10 }}>
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: 13 }}>
                  <span>Two-factor authentication {me.totpEnabled
                    ? <span style={{ color: T.gain, fontFamily: fontMono, fontSize: 11 }}>· ON</span>
                    : <span style={{ color: T.inkSoft, fontFamily: fontMono, fontSize: 11 }}>· OFF</span>}
                  </span>
                  {!me.totpEnabled && !twofa && (
                    <Btn small onClick={() => twofaAction("/api/me/2fa/setup", {})} disabled={twofaBusy}>Enable 2FA</Btn>
                  )}
                </div>
                {twofa && !me.totpEnabled && (
                  <div style={{ marginTop: 10, textAlign: "center" }}>
                    <img src={twofa.qr} alt="Scan with your authenticator app" style={{ borderRadius: 12, border: `1px solid ${T.line}` }} />
                    <div style={{ fontSize: 11.5, color: T.inkSoft, margin: "6px 0 10px" }}>
                      Scan with Google Authenticator / 1Password / Apple Passwords, or enter:
                      <div style={{ fontFamily: fontMono, fontSize: 12, marginTop: 4, userSelect: "all" }}>{twofa.secret}</div>
                    </div>
                    <div style={{ display: "flex", gap: 8, justifyContent: "center" }}>
                      <input inputMode="numeric" placeholder="123456" value={twofaCode}
                        onChange={(e) => setTwofaCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
                        style={{ width: 130, padding: "9px 12px", borderRadius: 10, border: `1.5px solid ${T.line}`, fontFamily: fontMono, fontSize: 16, textAlign: "center", letterSpacing: "0.2em" }} />
                      <Btn small disabled={twofaBusy || twofaCode.length !== 6} onClick={() => twofaAction("/api/me/2fa/verify", { code: twofaCode })}>
                        {twofaBusy ? "…" : "Confirm"}
                      </Btn>
                    </div>
                  </div>
                )}
                {me.totpEnabled && (
                  <div style={{ display: "flex", gap: 8, alignItems: "center", marginTop: 10 }}>
                    <input inputMode="numeric" placeholder="Code to disable" value={twofaCode}
                      onChange={(e) => setTwofaCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
                      style={{ width: 150, padding: "9px 12px", borderRadius: 10, border: `1.5px solid ${T.line}`, fontFamily: fontMono, fontSize: 14 }} />
                    <Btn small kind="danger" disabled={twofaBusy || twofaCode.length !== 6} onClick={() => twofaAction("/api/me/2fa/disable", { code: twofaCode })}>
                      Disable 2FA
                    </Btn>
                  </div>
                )}
              </div>
            </div>

            <div style={{ background: T.card, borderRadius: 16, padding: 16, border: `1px solid ${T.line}`, marginBottom: 14 }}>
              <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 4 }}>
                <span style={{ fontWeight: 700, fontSize: 14 }}>Robo-trader</span>
                {roboState?.mandates?.length > 0 && (
                  <Btn small kind={roboState.paused ? "primary" : "danger"} disabled={roboBusy}
                    onClick={() => saveRobo({ paused: !roboState.paused })}>
                    {roboState.paused ? "Resume" : "Pause all"}
                  </Btn>
                )}
              </div>
              <p style={{ fontSize: 12, color: T.inkSoft, margin: "0 0 10px" }}>
                Automates a capped slice per stock. Every order passes your guardrails; out-of-policy never executes. Orders above the auto-limit wait for your approval.
              </p>

              {(roboState?.mandates || []).map((m) => (
                <div key={m.id} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "8px 0", borderTop: `1px solid ${T.line}`, fontSize: 12.5 }}>
                  <span>
                    <b style={{ fontFamily: fontMono }}>{m.symbol}</b>
                    <span style={{ color: T.inkSoft }}> · {(m.allocation.value * 100).toFixed(0)}% cap · {fmt(m.clipSize)} clips · auto ≤ {fmt(m.autoLimit)}</span>
                  </span>
                  <span style={{ display: "flex", gap: 6 }}>
                    <Btn small kind="ghost" disabled={roboBusy}
                      onClick={() => saveRobo({ mandates: roboState.mandates.map((x) => x.id === m.id ? { ...x, enabled: !x.enabled } : x) })}>
                      {m.enabled ? "Disable" : "Enable"}
                    </Btn>
                    <Btn small kind="danger" disabled={roboBusy}
                      onClick={() => saveRobo({ mandates: roboState.mandates.filter((x) => x.id !== m.id) })}>
                      ✕
                    </Btn>
                  </span>
                </div>
              ))}

              {!mForm && <div style={{ marginTop: 10 }}><Btn small onClick={openMandateForm}>+ New mandate</Btn></div>}
              {mForm && (
                <div style={{ marginTop: 10, borderTop: `1px solid ${T.line}`, paddingTop: 10 }}>
                  <div style={{ position: "relative", marginBottom: 6 }}>
                    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 10 }}>
                      <span style={{ fontSize: 12, color: T.inkSoft }}>Symbol</span>
                      <input value={mSymbolQuery}
                        onChange={(e) => {
                          const v = e.target.value;
                          setMSymbolQuery(v);
                          if (v.trim().toUpperCase() !== mLastSelectedSymbol.current) setMForm((f) => ({ ...f, symbol: "" }));
                        }}
                        placeholder="Search ticker…"
                        style={{ width: 150, padding: "7px 10px", borderRadius: 8, border: `1.5px solid ${T.line}`, fontFamily: fontMono, fontSize: 13 }} />
                    </div>
                    {mSearchResults.length > 0 && (
                      <div style={{ position: "absolute", top: "100%", right: 0, zIndex: 10, width: 240, background: T.card, border: `1px solid ${T.line}`, borderRadius: 10, marginTop: 4, boxShadow: `0 8px 24px ${T.shadow}`, overflow: "hidden" }}>
                        {mSearchResults.map((s) => (
                          <button key={s.symbol} onClick={() => selectMandateSymbol(s.symbol)}
                            style={{ display: "flex", alignItems: "center", gap: 8, width: "100%", padding: "8px 10px", border: "none", borderBottom: `1px solid ${T.line}`, background: "transparent", cursor: "pointer", textAlign: "left" }}>
                            <Logo symbol={s.symbol} size={20} src={s.logoUrl} />
                            <span style={{ fontFamily: fontMono, fontWeight: 700, fontSize: 12 }}>{s.symbol}</span>
                            <span style={{ fontSize: 11, color: T.inkSoft, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1 }}>{s.name}</span>
                          </button>
                        ))}
                      </div>
                    )}
                    {mSymbolQuery.trim() && !mForm.symbol && mSearchResults.length === 0 && (
                      <div style={{ fontSize: 11, color: T.loss, marginTop: 4, textAlign: "right" }}>Pick a ticker from the dropdown</div>
                    )}
                  </div>
                  {[
                    ["allocPct", "Allocation cap (% of portfolio)", "number"],
                    ["clipSize", "Clip size ($ per order)", "number"],
                    ["buyDipPct", "Buy dip (% off 20-day high)", "number"],
                    ["takeProfitPct", "Take profit (%)", "number"],
                    ["stopLossPct", "Stop loss (%)", "number"],
                    ["autoLimit", "Auto-execute limit ($; larger orders ask you)", "number"],
                    ["maxOpenPositions", "Max open robot lots", "number"],
                    ["maxOrdersPerDay", "Max orders per day", "number"],
                  ].map(([k, label, type]) => (
                    <div key={k} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 6, gap: 10 }}>
                      <span style={{ fontSize: 12, color: T.inkSoft }}>{label}</span>
                      <input type={type} value={mForm[k]}
                        onChange={(e) => setMForm((f) => ({ ...f, [k]: e.target.value }))}
                        style={{ width: 110, padding: "7px 10px", borderRadius: 8, border: `1.5px solid ${T.line}`, fontFamily: fontMono, fontSize: 13 }} />
                    </div>
                  ))}
                  <div style={{ display: "flex", gap: 8, marginTop: 8 }}>
                    <Btn small disabled={roboBusy || !mForm.symbol}
                      onClick={async () => { await saveRobo({ mandates: [...(roboState?.mandates || []), mandateFromForm(mForm)] }); closeMandateForm(); }}>
                      {roboBusy ? "Saving…" : "Create mandate"}
                    </Btn>
                    <Btn small kind="ghost" onClick={closeMandateForm}>Cancel</Btn>
                  </div>
                </div>
              )}
            </div>

            {me.mode === "sim" && (
              <div style={{ background: T.card, borderRadius: 16, padding: 16, border: `1px solid ${T.line}`, marginBottom: 14 }}>
                <div style={{ fontWeight: 700, fontSize: 14, marginBottom: 4 }}>Simulator funds</div>
                <p style={{ fontSize: 12, color: T.inkSoft, margin: "0 0 10px" }}>
                  Add or withdraw simulated cash — logged to your journal. (Alpaca paper balances are set in the Alpaca dashboard; live accounts fund via bank transfer at Alpaca.)
                </p>
                <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
                  <input type="number" value={fundAmt} onChange={(e) => setFundAmt(Number(e.target.value))}
                    style={{ width: 110, padding: "9px 12px", borderRadius: 10, border: `1.5px solid ${T.line}`, fontSize: 14, fontFamily: fontMono }} />
                  <Btn small disabled={funding || !(fundAmt > 0)} onClick={() => transferFunds("deposit")}>Add funds</Btn>
                  <Btn small kind="ghost" disabled={funding || !(fundAmt > 0)} onClick={() => transferFunds("withdraw")}>Withdraw</Btn>
                </div>
              </div>
            )}

            <div style={{ background: T.card, borderRadius: 16, padding: 16, border: `1px solid ${T.line}`, marginBottom: 14 }}>
              <button onClick={() => setTermsOpen((v) => !v)}
                style={{ display: "flex", justifyContent: "space-between", alignItems: "center", width: "100%", background: "none", border: "none", padding: 0, cursor: "pointer" }}>
                <span style={{ fontWeight: 700, fontSize: 14 }}>Terms &amp; Disclaimer</span>
                <span style={{ fontFamily: fontMono, fontSize: 12, color: T.inkSoft }}>{termsOpen ? "▴" : "▾"}</span>
              </button>
              <p style={{ fontSize: 12, color: T.inkSoft, margin: "8px 0 0", lineHeight: 1.5 }}>{DISCLAIMER_SHORT}</p>
              {termsOpen && <div style={{ marginTop: 12, paddingTop: 12, borderTop: `1px solid ${T.line}` }}><TermsContent /></div>}
            </div>

            {profileMsg && (
              <div style={{ fontSize: 13, color: profileMsg.ok ? T.gain : T.loss, marginBottom: 14 }}>{profileMsg.text}</div>
            )}

            <Btn kind="ghost" onClick={async () => { await api("/api/auth/logout", {}); location.reload(); }}>Sign out</Btn>
          </React.Fragment>
        )}
      </div>

      {/* Bottom nav */}
      <div style={{ position: "fixed", bottom: 0, left: 0, right: 0, background: T.card, borderTop: `1px solid ${T.line}` }}>
        <div style={{ maxWidth: 440, margin: "0 auto", display: "flex" }}>
          {[["portfolio", "Portfolio"], ["ideas", "Ideas"], ["add", "Add"], ["journal", "Journal"], ["profile", "Profile"]].map(([id, label]) => (
            <button key={id} onClick={() => setTab(id)}
              style={{
                flex: 1, padding: "14px 0 18px", border: "none", background: "transparent", cursor: "pointer",
                fontSize: 12.5, fontWeight: tab === id ? 700 : 500, color: tab === id ? T.brand : T.inkSoft,
                borderTop: tab === id ? `2.5px solid ${T.brand}` : "2.5px solid transparent",
              }}>
              {label}
            </button>
          ))}
        </div>
      </div>
    </Shell>
  );
}

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