/* ===============================================================
   AGENTICS.CREDIT — CAMPAIGN · DASHBOARD MODULE
   Split out of campaign.jsx on 2026-07-03. Contains the dashboard
   experience + surrounding chrome:

     Panels — ReasoningFeed, ReasoningEntry, ScorePanel, AgentPanel,
     LoanProgressStrip, EligibilityPanel, CreditPanel,
     ScoreHistoryPanel, EquityChartPanel (+ EQUITY_TIMEFRAMES),
     ActiveTradesPanel + CloseTradeConfirm, RecentTradesPanel,
     DecisionJournalPanel + StrategyTimeline, CombinedAgentsPanel,
     AgentsGridPanel

     Top-level — CampaignDashboard

     Chrome — CampaignHeader, ShareModal, GraduationScreen

   Load AFTER campaign.jsx (constants, apiPost, acGet, acSend, Card,
   TurnstileWidget, seed helpers) and after campaign-shared.jsx
   (ReallocateCapitalModal, AddAgentModal, RiskBanner, KyaBadge etc.
   are referenced from AgentsGridPanel + ActiveTradesPanel + dashboard).
   =============================================================== */

function ReasoningFeed({ user, onShare }) {
  // Real reasoning feed = real trades. We poll /trades every 30s and turn
  // each into an OPEN or CLOSE feed entry. The agent's actual strategy
  // reasoning (from agents.strategy_state.reasoning) is surfaced on the
  // most recent open if available.
  const [entries, setEntries] = useState([]);
  const [loaded, setLoaded] = useState(false);
  const toast = useToast && useToast();
  const seenIds = useRef(new Set());

  useEffect(() => {
    function acJwt()  { try { return window.AC_JWT || localStorage.getItem("agc_jwt") || null; } catch (_) { return null; } }
    function acBase() { return ((typeof window !== "undefined" && window.AC_API) || "").replace(/\/$/, ""); }
    let cancelled = false;

    async function refresh() {
      const jwt = acJwt(); const base = acBase();
      if (!jwt || !base) { setLoaded(true); return; }
      try {
        const r = await fetch(base + "/trades?limit=20",
          { headers: { Authorization: "Bearer " + jwt }, credentials: "omit" });
        if (!r.ok) { setLoaded(true); return; }
        const data = await r.json();
        if (cancelled) return;
        const out = [];
        for (const t of (data.trades || [])) {
          // OPEN entry — for every trade we have
          const openTime = t.opened_at ? new Date(t.opened_at) : null;
          out.push({
            id: "open-" + t.id,
            time: openTime ? openTime.toTimeString().slice(0, 5) : "—",
            sortTs: openTime ? openTime.getTime() : 0,
            action: "OPEN",
            market: t.market,
            side: (t.side || "").toUpperCase(),
            sizeUsd: Number(t.size_usd || 0),
            entryPrice: Number(t.entry_price || 0),
            reasoning: t.trigger_type === "manual"
              ? `Manual trade by ${user.username || "you"}.`
              : "Your agent opened this position based on the current strategy.",
            shareable: true,
          });
          // CLOSE entry — only if the trade has closed
          if (!t.is_open) {
            const closeTime = t.closed_at ? new Date(t.closed_at) : openTime;
            const pnl = Number(t.pnl || 0);
            const credit = Number(t.credit_earned || 0);
            const isWin = pnl > 0;
            out.push({
              id: "close-" + t.id,
              time: closeTime ? closeTime.toTimeString().slice(0, 5) : "—",
              sortTs: (closeTime ? closeTime.getTime() : 0) + 1,   // close after open at same ms
              action: "CLOSE",
              market: t.market,
              sizeUsd: Number(t.size_usd || 0),
              pnl,
              pnlPct: Number(t.pnl_pct || 0),
              creditEarned: credit,
              reasoning: isWin
                ? "Take-profit hit. Locked in gains."
                : "Stop-loss hit. Cut the loss cleanly.",
              shareable: true,
              win: isWin,
            });
            // Toast on newly-seen wins
            if (isWin && toast && !seenIds.current.has("close-" + t.id)) {
              seenIds.current.add("close-" + t.id);
              toast.push({
                title:    `+$${pnl.toFixed(2)} profit`,
                meta:     credit > 0 ? `+${credit.toFixed(2)} $CREDIT` : "",
                duration: 4500,
              });
            }
          }
        }
        out.sort((a, b) => b.sortTs - a.sortTs);   // newest first
        setEntries(out.slice(0, 14));
        setLoaded(true);
      } catch (_) { setLoaded(true); }
    }
    refresh();
    const id = setInterval(refresh, 30_000);
    return () => { cancelled = true; clearInterval(id); };
  }, [user && user.username, toast]);

  return (
    <div className="cg-feed">
      <div className="cg-feed-head">
        <span><span className="led"></span> REASONING_FEED — LIVE</span>
        <span className="cg-feed-sub">Your agent's thinking, every trade</span>
      </div>
      <div className="cg-feed-list">
        {loaded && entries.length === 0 && (
          <div style={{ padding: "32px 16px", color: "var(--muted)", fontStyle: "italic", textAlign: "center" }}>
            Your agent hasn't traded yet. Its strategy tier reviews every ~1 hour and the executor ticks every ~15s — the feed will start filling soon.
          </div>
        )}
        {entries.map(e => <ReasoningEntry key={e.id} entry={e} onShare={onShare} />)}
      </div>
    </div>
  );
}

function ReasoningEntry({ entry, onShare }) {
  const colorClass = entry.action === "OPEN" ? "open"
                  : entry.action === "CLOSE" ? (entry.win ? "close-win" : "close-loss")
                  : "hold";
  return (
    <div className={"cg-feed-entry " + colorClass}>
      <div className="cg-feed-top">
        <span className="t">{entry.time}</span>
        <span className="a">
          {entry.action === "OPEN" && `OPENED ${entry.side} ${entry.market} $${entry.sizeUsd}`}
          {entry.action === "CLOSE" && `CLOSED ${entry.market} ${entry.pnl >= 0 ? "+" : ""}$${entry.pnl} (${entry.pnlPct > 0 ? "+" : ""}${entry.pnlPct}%)`}
          {entry.action === "HOLD" && `HOLD — ${entry.market}`}
        </span>
      </div>
      <div className="cg-feed-body">"{entry.reasoning}"</div>
      {entry.shareable && (
        <div className="cg-feed-actions">
          <button className="cg-share-btn" onClick={() => onShare && onShare(entry)}>[SHARE THIS TRADE ↗]</button>
          {entry.action === "CLOSE" && entry.creditEarned > 0 && (
            <span className="cg-credit-earned">+{entry.creditEarned} $CREDIT</span>
          )}
        </div>
      )}
    </div>
  );
}

/* ===============================================================
   SCORE PANEL
   =============================================================== */

function ScorePanel({ user }) {
  const hasScore = user.score && user.score > 0;
  const tier = !hasScore ? { label: "UNRATED", className: "untiered" }
             : user.score >= 780 ? TIERS.ELITE
             : user.score >= 640 ? TIERS.QUAL
             : user.score >= 540 ? TIERS.EMRG
             : TIERS.SPEC;
  const signals = user.signals || { profitability: 0, drawdown: 0, consistency: 0, longevity: 0, winRate: 0 };
  return (
    <Card title={`CREDIT_SCORE — DAY_${user.day}`}>
      <div className="cg-score-row">
        <ScoreDial score={hasScore ? user.score : 0} label={hasScore ? "AGENT_SCORE" : "UNRATED"} size={180} />
        <div className="cg-signals">
          {[
            ["PROFITABILITY", signals.profitability],
            ["DRAWDOWN", signals.drawdown],
            ["CONSISTENCY", signals.consistency],
            ["LONGEVITY", signals.longevity],
            ["WIN_RATE", signals.winRate],
          ].map(([k, v]) => (
            <div key={k} className="cg-signal">
              <div className="cg-signal-head"><span>{k}</span><span className="v">{v}</span></div>
              <ProgressBar value={v} max={100} />
            </div>
          ))}
          <div className="cg-tier-line">
            <TierBadge tier={tier} big /> <span className="cg-tier-trend">↗ trending</span>
          </div>
        </div>
      </div>
    </Card>
  );
}

/* ===============================================================
   AGENT STATS PANEL
   =============================================================== */

function AgentPanel({ user }) {
  // ACCOUNT_EQUITY = the meaningful total — what the account is worth right
  // now, including margin currently locked in open positions. This matches
  // the equity-chart headline so the two panels show ONE number, not two.
  //
  // paper_balance (free cash) is shown as a sublabel underneath so users
  // can see at a glance what's available for new trades vs what's tied up.
  //
  //    ACCOUNT_EQUITY = paperStart + totalPnl
  //    free           = agents.paper_balance     (free cash)
  //    inOpenTrades   = equity - free            (margin locked + unrealised)
  const seed         = Number(user.paperStart || 0);
  const totalPnl     = Number(user.totalPnl   || 0);
  const equity       = seed + totalPnl;
  const free         = Number(user.paperBalance || 0);
  // Floor at 0 — if equity < free (e.g. before any trades open) the
  // delta would be slightly negative due to rounding; suppress that noise.
  const inOpenTrades = Math.max(equity - free, 0);

  // Signed formatters — fixes the "+$-335.436" bug where the JSX hardcoded
  // a + prefix and the number rendered its own minus, producing "+$-".
  const pnlSign      = totalPnl >= 0 ? "+" : "-";
  const pnlAbs       = Math.abs(totalPnl).toLocaleString(undefined, { maximumFractionDigits: 2 });
  const pnlPctSign   = (user.totalPnlPct || 0) >= 0 ? "+" : "-";
  const pnlPctAbs    = Math.abs(user.totalPnlPct || 0).toFixed(2);
  const pnlClass     = totalPnl >= 0 ? "pos" : "neg";

  return (
    <Card
      title="YOUR_AGENT"
      right={<span style={{ color: "var(--accent)", fontSize: 11, letterSpacing: 1 }}>● TRADING LIVE</span>}
    >
      <div className="cg-agent-meta">
        <div><div className="k">NAME</div><div className="v">{user.agentName}</div></div>
        <div><div className="k">TYPE</div><div className="v">AI · {(user.agentType || "").replace(/^claude_/, "")}</div></div>
        <div><div className="k">DAY</div><div className="v">{user.day} / {user.totalDays}</div></div>
        <div><div className="k">RANK</div><div className="v accent">{user.rank ? "#" + user.rank : "—"}</div></div>
      </div>
      <div className="cg-balance">
        <div className="cg-balance-k">ACCOUNT_EQUITY</div>
        <div className="cg-balance-v">${equity.toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 })}</div>
        <div className="cg-balance-breakdown">
          <span><span className="k">Free:</span> ${free.toLocaleString(undefined, { maximumFractionDigits: 2 })}</span>
          <span className="sep">·</span>
          <span><span className="k">In open trades:</span> ${inOpenTrades.toLocaleString(undefined, { maximumFractionDigits: 2 })}</span>
        </div>
      </div>
      <div className="cg-stat-grid">
        <div className="cg-stat">
          <div className="k">TOTAL_PnL</div>
          <div className={"v " + pnlClass}>
            {pnlSign}${pnlAbs} <span className="pct">({pnlPctSign}{pnlPctAbs}%)</span>
          </div>
        </div>
        <div className="cg-stat"><div className="k">WIN_RATE</div><div className="v">{user.winRate}%</div></div>
        <div className="cg-stat"><div className="k">MAX_DRAWDOWN</div><div className="v">{(user.drawdown || 0).toFixed(2)}%</div></div>
        <div className="cg-stat"><div className="k">TRADES_TODAY</div><div className="v">{user.tradesToday || 0}</div></div>
      </div>
    </Card>
  );
}

/* ===============================================================
   ELIGIBILITY PANEL
   =============================================================== */

/* Slim hero strip — splits the loan story into the two pieces that
   trigger the user's competitive/aspirational instinct most loudly:
   the progress bar (curiosity — "how close am I?") and the prize
   amount (reward — "what do I get?"). Clicking jumps to the full
   eligibility card below. */
function LoanProgressStrip({ user, onJump }) {
  const pct = Math.max(0, Math.min(100, Number(user.progressToQualified || 0)));
  const ceiling = Number(user.estLoanUsdc || 0);
  // Tier label from progress milestones — keeps the strip honest about
  // which tier the user is currently tracking toward.
  const tierLabel = pct >= 100 ? "QUALIFIED" : pct >= 75 ? "ALMOST QUALIFIED" : pct >= 40 ? "PROGRESSING" : "EARLY";
  return (
    <button className="cg-loan-strip" onClick={onJump} aria-label="Jump to loan eligibility details">
      <div className="cg-loan-strip-left">
        <div className="cg-loan-strip-eyebrow">// PROGRESS_TO_QUALIFIED</div>
        <div className="cg-loan-strip-bar">
          <div className="cg-loan-strip-bar-fill" style={{ width: pct + "%" }} />
        </div>
        <div className="cg-loan-strip-pct">{pct}% <span className="cg-loan-strip-tier">{tierLabel}</span></div>
      </div>
      <div className="cg-loan-strip-right">
        <div className="cg-loan-strip-eyebrow">// APPROVED_LOAN_CEILING</div>
        <div className="cg-loan-strip-amt">${ceiling.toLocaleString()} <span className="cg-loan-strip-ccy">USDC</span></div>
        <div className="cg-loan-strip-cta">See what's blocking →</div>
      </div>
    </button>
  );
}

function EligibilityPanel({ user }) {
  const winRate    = Number(user.winRate || 0);
  const drawdown   = Number(user.drawdown || 0);
  const totalPnl   = Number(user.totalPnl || 0);
  const day        = Number(user.day || 0);
  const score      = Number(user.score || 0);
  // Score-based gate for Qualified tier (matches the backend scoring model).
  // The PnL/win-rate/drawdown rows are the underlying signals the scorer uses.
  const checks = [
    { ok: winRate >= 45,    label: `Win rate ≥ 45%`,          val: `${winRate.toFixed(1)}%` },
    { ok: drawdown < 35,    label: `Drawdown < 35%`,          val: `${drawdown.toFixed(1)}%` },
    { ok: totalPnl > 0,     label: `PnL positive`,            val: `${totalPnl >= 0 ? "+" : ""}$${Math.abs(totalPnl).toFixed(2)}` },
    { ok: day >= 60,        label: `Days ≥ 60`,               val: `${day}/60` },
    { ok: totalPnl >= 500,  label: `PnL ≥ $500`,              val: `${totalPnl >= 0 ? "+" : ""}$${Math.abs(totalPnl).toFixed(2)}` },
    { ok: score >= 670,     label: `Credit score ≥ 670`,      val: score > 0 ? `${score}` : "unrated" },
  ];
  const anchor = (user.loanAnchorTier || user.targetTier || "qualified");
  const anchorLabel = anchor.charAt(0).toUpperCase() + anchor.slice(1);
  const isProjection = !user.currentTier;   // no current tier → loan is the target estimate
  const progress = Number(user.progressToQualified || 0);
  return (
    <Card id="cg-loan-eligibility" title="LOAN_ELIGIBILITY">
      <div className="cg-checks">
        {checks.map((c, i) => (
          <div key={i} className={"cg-check " + (c.ok ? "ok" : "no")}>
            <span className="cg-check-mark">{c.ok ? "✓" : "✗"}</span>
            <span className="cg-check-label">{c.label}</span>
            <span className="cg-check-val">{c.val}</span>
            <span className="cg-check-tag">{c.ok ? "QUALIFIED" : "BLOCKED"}</span>
          </div>
        ))}
      </div>
      <div className="cg-progress">
        <div className="cg-progress-top"><span>PROGRESS TO QUALIFIED</span><span className="v">{progress}%</span></div>
        <ProgressBar value={progress} max={100} height={8} />
      </div>
      <div className="cg-estimate">
        <span>
          {isProjection
            ? `Projected loan at ${anchorLabel} tier`
            : `Approved loan ceiling (${anchorLabel})`}
        </span>
        <span className="cg-estimate-v">${Number(user.estLoanUsdc || 0).toLocaleString()} USDC</span>
      </div>
    </Card>
  );
}

/* ===============================================================
   $CREDIT BALANCE PANEL
   =============================================================== */

function CreditPanel({ user }) {
  return (
    <Card title="$CREDIT_BALANCE">
      <div className="cg-credit-grid">
        <div><div className="k">EARNED FROM TRADES</div><div className="v">{user.creditFromTrades.toLocaleString()} $CREDIT</div></div>
        <div><div className="k">REFERRAL BONUSES</div><div className="v">{user.creditFromReferrals.toLocaleString()} $CREDIT</div></div>
        <div className="total"><div className="k">TOTAL</div><div className="v accent">{user.creditBalance.toLocaleString()} $CREDIT</div></div>
      </div>
      <div className="cg-credit-foot">= ${user.creditBalance.toLocaleString()} credit line boost at graduation</div>
    </Card>
  );
}

/* ===============================================================
   SCORE HISTORY CHART
   =============================================================== */

function ScoreHistoryPanel({ user }) {
  // Real score history from /agents/{id} — only renders once we have daily
  // scoring rows. Until then, show an empty-state with the user's actual day count.
  const [history, setHistory] = useState(null);
  useEffect(() => {
    function acJwt()  { try { return window.AC_JWT || localStorage.getItem("agc_jwt") || null; } catch (_) { return null; } }
    function acBase() { return ((typeof window !== "undefined" && window.AC_API) || "").replace(/\/$/, ""); }
    let cancelled = false;
    async function refresh() {
      const jwt = acJwt(); const base = acBase();
      const primaryId = (user.agents && user.agents[0] && user.agents[0].id);
      if (!jwt || !base || !primaryId) return;
      try {
        const r = await fetch(`${base}/agents/${primaryId}`,
          { headers: { Authorization: "Bearer " + jwt }, credentials: "omit" });
        if (!r.ok) return;
        const data = await r.json();
        if (cancelled) return;
        const scores = (data.daily_scores || []).slice().sort((a, b) => a.day - b.day);
        setHistory(scores.map(s => ({ x: s.day, y: s.score })));
      } catch (_) {}
    }
    refresh();
    const id = setInterval(refresh, 60_000);
    return () => { cancelled = true; clearInterval(id); };
  }, [user.agents && user.agents[0] && user.agents[0].id]);

  return (
    <Card title="SCORE_HISTORY — 90D">
      {history && history.length > 0 ? (
        <>
          <AreaChart data={history} height={140} yMin={300} yMax={850} />
          <div className="cg-chart-x">
            <span>day {history[0].x}</span>
            <span>day {history[history.length - 1].x}</span>
            <span>day 90</span>
          </div>
        </>
      ) : (
        <div style={{ padding: "40px 16px", color: "var(--muted)", fontStyle: "italic", textAlign: "center" }}>
          Score history will appear after the first daily scoring pass (runs at UTC midnight).
        </div>
      )}
    </Card>
  );
}

/* ===============================================================
   EQUITY CHART — account value over time, switchable timeframes
   ===============================================================
   Reconstructs the equity curve from the participant's closed trades.
   No new backend needed — pulls /trades?is_open=false, walks chronologically,
   equity(t) = initial_seed + cumulative sum of trade.pnl up to t. Buckets the
   resulting curve to the selected timeframe so the chart stays readable. */

const EQUITY_TIMEFRAMES = [
  { key: "1D",  label: "24H",   ms: 24 * 3600_000,         bucketMs: 60   * 60_000 },   // 1h buckets
  { key: "7D",  label: "7D",    ms: 7  * 86400_000,        bucketMs: 6    * 3600_000 }, // 6h buckets
  { key: "30D", label: "30D",   ms: 30 * 86400_000,        bucketMs: 24   * 3600_000 }, // 1d buckets
  { key: "ALL", label: "ALL",   ms: Infinity,              bucketMs: 24   * 3600_000 }, // 1d buckets
];

function EquityChartPanel({ user }) {
  const [tf, setTf] = useState("7D");
  const [trades, setTrades] = useState(null);

  useEffect(() => {
    function acJwt()  { try { return window.AC_JWT || localStorage.getItem("agc_jwt") || null; } catch (_) { return null; } }
    function acBase() { return ((typeof window !== "undefined" && window.AC_API) || "").replace(/\/$/, ""); }
    let cancelled = false;
    async function refresh() {
      const jwt = acJwt(); const base = acBase();
      if (!jwt || !base) return;
      try {
        const r = await fetch(base + "/trades?is_open=false&limit=200",
          { headers: { Authorization: "Bearer " + jwt }, credentials: "omit" });
        if (!r.ok) return;
        const data = await r.json();
        if (cancelled) return;
        // Sort chronologically by closed_at (oldest first)
        const closed = (data.trades || [])
          .filter(t => t.closed_at)
          .map(t => ({ ts: new Date(t.closed_at).getTime(), pnl: Number(t.pnl || 0) }))
          .sort((a, b) => a.ts - b.ts);
        setTrades(closed);
      } catch (_) {}
    }
    refresh();
    const id = setInterval(refresh, 30_000);
    return () => { cancelled = true; clearInterval(id); };
  }, []);

  const seed = Number(user.paperStart || 1000);
  const totalPnl = Number(user.totalPnl || 0);
  const currentEquity = seed + totalPnl;
  const tfDef = EQUITY_TIMEFRAMES.find(t => t.key === tf) || EQUITY_TIMEFRAMES[1];

  // Build the curve. If we have trades, build a full cumulative series.
  // If no trades yet, draw a flat line at the seed value.
  const curve = (() => {
    if (!trades) return null;
    const now = Date.now();
    const fromTs = tf === "ALL"
      ? (trades.length ? Math.min(trades[0].ts, now - 86400_000) : now - 86400_000)
      : now - tfDef.ms;
    // Initial equity BEFORE the timeframe = seed + sum of trade pnls before fromTs.
    let priorEquity = seed;
    for (const t of trades) {
      if (t.ts < fromTs) priorEquity += t.pnl;
    }
    // Bucket from fromTs to now in bucketMs steps. For each bucket, the
    // equity at the END of that bucket = priorEquity + sum of pnls up to bucket end.
    const buckets = [];
    let cursor = priorEquity;
    let tradeIdx = trades.findIndex(t => t.ts >= fromTs);
    if (tradeIdx === -1) tradeIdx = trades.length;
    // Seed the first point so the line starts visibly even with no activity
    buckets.push({ x: fromTs, y: cursor });
    for (let bucketEnd = fromTs + tfDef.bucketMs; bucketEnd <= now + tfDef.bucketMs; bucketEnd += tfDef.bucketMs) {
      while (tradeIdx < trades.length && trades[tradeIdx].ts <= bucketEnd) {
        cursor += trades[tradeIdx].pnl;
        tradeIdx++;
      }
      buckets.push({ x: Math.min(bucketEnd, now), y: cursor });
      if (bucketEnd > now) break;
    }
    return buckets;
  })();

  // Stats for the timeframe — change vs first point in the window
  const tfStart   = curve && curve.length ? curve[0].y : seed;
  const tfEnd     = curve && curve.length ? curve[curve.length - 1].y : seed;
  const tfChange  = tfEnd - tfStart;
  const tfChangePct = tfStart > 0 ? (tfChange / tfStart) * 100 : 0;
  const isUp = tfChange >= 0;
  const color = isUp ? "#1D9E75" : "#DC2626";

  // Generous Y bounds so the curve isn't flat against the edges
  const ys = curve ? curve.map(p => p.y) : [seed];
  const yMin = Math.min(...ys, seed) - 1;
  const yMax = Math.max(...ys, seed) + 1;

  function fmtCurrency(n) {
    return `$${Number(n).toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 })}`;
  }

  return (
    <Card
      title={`ACCOUNT_EQUITY — ${fmtCurrency(currentEquity)}`}
      right={
        <div style={{ display: "flex", gap: 4 }}>
          {EQUITY_TIMEFRAMES.map(t => (
            <button
              key={t.key}
              className={"cg-cta-ghost" + (t.key === tf ? " active" : "")}
              style={{
                padding: "3px 10px", fontSize: 11,
                background: t.key === tf ? "rgba(29,158,117,0.16)" : "transparent",
                color:      t.key === tf ? "var(--accent)" : "var(--muted)",
              }}
              onClick={() => setTf(t.key)}
            >
              {t.label}
            </button>
          ))}
        </div>
      }
    >
      <div style={{ display: "flex", alignItems: "baseline", gap: 16, marginBottom: 10 }}>
        <span className="mono" style={{ fontSize: 28, fontWeight: 600 }}>
          {fmtCurrency(tfEnd)}
        </span>
        <span className="mono" style={{ fontSize: 14, color, fontWeight: 600 }}>
          {tfChange >= 0 ? "▲" : "▼"} {fmtCurrency(Math.abs(tfChange))} ({tfChange >= 0 ? "+" : ""}{tfChangePct.toFixed(2)}%)
        </span>
        <span className="muted mono" style={{ fontSize: 11 }}>
          // {tfDef.label} change
        </span>
      </div>
      {curve == null ? (
        <div style={{ padding: "32px 0", color: "var(--muted)", textAlign: "center" }}>Loading…</div>
      ) : trades.length === 0 ? (
        <div style={{ padding: "32px 0", color: "var(--muted)", textAlign: "center", fontStyle: "italic" }}>
          No closed trades yet — equity curve will draw itself as your agent trades.
        </div>
      ) : (
        <>
          <AreaChart data={curve} height={180} yMin={yMin} yMax={yMax} color={color}/>
          <div style={{ display: "flex", justifyContent: "space-between", fontSize: 10, color: "var(--muted)", marginTop: 4, fontFamily: "JetBrains Mono, monospace" }}>
            <span>{new Date(curve[0].x).toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })}</span>
            <span>{new Date(curve[curve.length - 1].x).toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })}</span>
          </div>
        </>
      )}
    </Card>
  );
}

/* ===============================================================
   ACTIVE TRADES — open positions w/ live PnL + Close button
   =============================================================== */

function ActiveTradesPanel({ user }) {
  // Polls open trades every 5s, fetches current market prices for each
  // unique market every 5s, computes live PnL + $CREDIT projection.
  // Close button posts /trade/close. Optimistic UI: row vanishes
  // immediately, refetch confirms. A "Are you sure?" modal sits in
  // front of every close click so a misaimed tap on a phone (the CLOSE
  // button sits next to a column with frequently-updating numbers) can't
  // accidentally book a loss.
  const [trades, setTrades] = useState([]);
  const [prices, setPrices] = useState({});      // {market: currentPrice}
  const [closingId, setClosingId] = useState(null);
  const [pendingClose, setPendingClose] = useState(null);   // {trade, liveMetrics}
  const [loaded, setLoaded] = useState(false);
  const toast = useToast && useToast();

  // Same configurable knobs the backend uses (campaign/api/config.py).
  // Used to estimate close fee + slippage + projected $CREDIT so the
  // numbers shown to the user match what they'll actually realize.
  const SLIPPAGE_PCT     = 0.05;
  const EXCHANGE_FEE_PCT = 0.0005;
  const CREDIT_EARN_RATE = 1.0;

  function acJwt()  { try { return window.AC_JWT || localStorage.getItem("agc_jwt") || null; } catch (_) { return null; } }
  function acBase() { return ((typeof window !== "undefined" && window.AC_API) || "").replace(/\/$/, ""); }

  useEffect(() => {
    let cancelled = false;

    async function refreshTrades() {
      const jwt = acJwt(); const base = acBase();
      if (!jwt || !base) { setLoaded(true); return; }
      try {
        const r = await fetch(base + "/trades?is_open=true&limit=20",
          { headers: { Authorization: "Bearer " + jwt }, credentials: "omit" });
        if (!r.ok) { setLoaded(true); return; }
        const data = await r.json();
        if (cancelled) return;
        const rows = (data.trades || []).map(t => ({
          id:         t.id,
          agent_id:   t.agent_id,
          market:     t.market,
          side:       t.side,
          size:       Number(t.size_usd || 0),     // notional
          leverage:   Number(t.leverage || 1.0),
          entry:      Number(t.entry_price || 0),
          opened_at:  t.opened_at,
        }));
        setTrades(rows);
        setLoaded(true);
        // Refresh prices for every market we hold
        const markets = Array.from(new Set(rows.map(r => r.market)));
        if (markets.length) refreshPrices(markets);
      } catch (_) { setLoaded(true); }
    }

    async function refreshPrices(markets) {
      const base = acBase();
      const next = {};
      await Promise.all(markets.map(async (m) => {
        try {
          const r = await fetch(`${base}/trade/price/${encodeURIComponent(m)}`, { credentials: "omit" });
          if (!r.ok) return;
          const d = await r.json();
          if (d && typeof d.price === "number") next[m] = d.price;
        } catch (_) {}
      }));
      if (cancelled) return;
      setPrices(prev => ({ ...prev, ...next }));
    }

    refreshTrades();
    const id = setInterval(refreshTrades, 5_000);
    return () => { cancelled = true; clearInterval(id); };
  }, []);

  function liveMetrics(t) {
    const price = prices[t.market];
    if (!price || !t.entry || !t.size) return null;
    // Adverse slippage on close: long sells low, short buys high
    const slip = SLIPPAGE_PCT / 100;
    const exitPrice = t.side === "long" ? price * (1 - slip) : price * (1 + slip);
    const fee = t.size * EXCHANGE_FEE_PCT;
    // PnL on NOTIONAL — leverage is already baked in because size_usd IS
    // the leveraged notional. Margin = size / leverage; % of margin shows
    // how close we are to liquidation.
    const grossPnl = t.side === "long"
      ? t.size * (exitPrice - t.entry) / t.entry
      : t.size * (t.entry - exitPrice) / t.entry;
    const netPnl = grossPnl - fee;
    const pnlPct = t.size ? (netPnl / t.size) * 100 : 0;
    const leverage = Math.max(t.leverage || 1, 1);
    const margin   = t.size / leverage;
    // Loss as % of margin (negative = losing). At -100% we're liquidated.
    const marginUsedPct = margin > 0 ? (netPnl / margin) * 100 : 0;
    // Liquidation price = entry where notional moves 100/leverage % against
    const liqMovePct = 100 / leverage;
    const liqPrice = t.side === "long"
      ? t.entry * (1 - liqMovePct / 100)
      : t.entry * (1 + liqMovePct / 100);
    // % of safety budget consumed — 0% = at entry, 100% = liquidated
    const moveAgainstPct = t.side === "long"
      ? (price < t.entry ? (t.entry - price) / t.entry * 100 : 0)
      : (price > t.entry ? (price - t.entry) / t.entry * 100 : 0);
    const liqRiskPct = liqMovePct > 0 ? Math.min(100, (moveAgainstPct / liqMovePct) * 100) : 0;
    const creditIfClosed = Math.max(netPnl * CREDIT_EARN_RATE, 0);
    return { current: price, exitPrice, netPnl, pnlPct, marginUsedPct,
             margin, leverage, liqPrice, liqRiskPct, creditIfClosed };
  }

  async function closeTrade(t) {
    if (closingId) return;
    setClosingId(t.id);
    // Optimistic remove
    setTrades(prev => prev.filter(x => x.id !== t.id));
    try {
      const jwt = acJwt(); const base = acBase();
      const r = await fetch(base + "/trade/close", {
        method:  "POST",
        headers: { "Content-Type": "application/json", Authorization: "Bearer " + jwt },
        credentials: "omit",
        body:    JSON.stringify({ trade_id: t.id }),
      });
      if (!r.ok) throw new Error("close_failed");
      const result = await r.json();
      const pnl = Number(result.pnl || 0);
      const credit = Number(result.credit_earned || 0);
      toast && toast.push({
        title: pnl >= 0 ? `+$${pnl.toFixed(2)} closed` : `$${pnl.toFixed(2)} closed`,
        meta:  credit > 0 ? `+${credit.toFixed(2)} $CREDIT` : "",
        duration: 4500,
      });
    } catch (e) {
      // Put it back on failure
      toast && toast.push({ title: "Close failed", meta: "Try again", duration: 3500 });
    } finally {
      setClosingId(null);
    }
  }

  // 9 columns now: OPENED · MARKET · SIDE · NOTIONAL · LEV · ENTRY · NOW · LIVE_PNL · LIQ_RISK · $CREDIT · CLOSE
  const COLS = "64px 84px 50px 80px 50px 80px 80px 100px 90px 90px 72px";

  return (
    <Card title={`ACTIVE_TRADES — ${trades.length} OPEN`}>
      <div className="cg-table">
        <div className="cg-tr head" style={{ gridTemplateColumns: COLS }}>
          <span>OPENED</span><span>MARKET</span><span>SIDE</span><span>NOTIONAL</span>
          <span>LEV</span><span>ENTRY</span><span>NOW</span>
          <span>LIVE_PNL</span><span>LIQ_RISK</span><span>$CREDIT</span><span></span>
        </div>
        {loaded && trades.length === 0 && (
          <div className="cg-tr" style={{ gridTemplateColumns: "1fr", padding: "20px 12px", color: "var(--muted)", fontStyle: "italic" }}>
            No open positions. When your agent opens a trade it'll show here with live PnL + liquidation risk.
          </div>
        )}
        {!loaded && (
          <div className="cg-tr" style={{ gridTemplateColumns: "1fr", padding: "20px 12px", color: "var(--muted)" }}>Loading…</div>
        )}
        {trades.map(t => {
          const m = liveMetrics(t);
          const time = t.opened_at ? new Date(t.opened_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "—";
          const pnlClass = m && m.netPnl >= 0 ? "pos" : "neg";
          // Liquidation-risk colour: green < 25% used, amber 25-65%, red 65%+
          const risk = m ? m.liqRiskPct : 0;
          const riskColor = risk >= 65 ? "var(--err, #DC2626)"
                          : risk >= 25 ? "#D97706"
                          : "var(--accent)";
          return (
            <div key={t.id} className="cg-tr" style={{ gridTemplateColumns: COLS, alignItems: "center" }}>
              <span className="mono" style={{ fontSize: 11, color: "var(--muted)" }}>{time}</span>
              <span className="mono">{t.market}</span>
              <span className={t.side === "long" ? "accent" : ""} style={{ fontWeight: 600 }}>{(t.side || "").toUpperCase()}</span>
              <span className="mono">${t.size.toFixed(0)}</span>
              <span className="mono" style={{ color: t.leverage > 1 ? "var(--accent)" : "var(--muted)" }}>
                {(t.leverage || 1).toFixed(1)}x
              </span>
              <span className="mono">${t.entry.toLocaleString(undefined, { maximumFractionDigits: 2 })}</span>
              <span className="mono">{m ? `$${m.current.toLocaleString(undefined, { maximumFractionDigits: 2 })}` : "…"}</span>
              <span className={`mono ${pnlClass}`} title={m ? `Margin used: ${m.marginUsedPct.toFixed(1)}%` : ""}>
                {m ? `${m.netPnl >= 0 ? "+" : ""}$${m.netPnl.toFixed(2)} (${m.pnlPct >= 0 ? "+" : ""}${m.pnlPct.toFixed(2)}%)` : "…"}
              </span>
              <span className="mono" style={{ color: riskColor, fontWeight: risk >= 65 ? 700 : 500 }}
                    title={m ? `Liquidation at $${m.liqPrice.toFixed(2)} (-${(100/t.leverage).toFixed(1)}%)` : ""}>
                {m ? `${risk.toFixed(0)}%` : "…"}
              </span>
              <span className="mono accent">{m && m.creditIfClosed > 0 ? `+${m.creditIfClosed.toFixed(2)}` : "—"}</span>
              <button
                className="cg-cta-ghost"
                disabled={closingId === t.id}
                style={{ padding: "4px 10px", fontSize: 11, background: "rgba(220,38,38,0.08)" }}
                onClick={() => setPendingClose({ trade: t, metrics: m })}
              >
                {closingId === t.id ? "…" : "CLOSE"}
              </button>
            </div>
          );
        })}
      </div>
      {trades.length > 0 && (
        <div style={{ marginTop: 8, fontSize: 11, color: "var(--muted)", fontFamily: "JetBrains Mono, monospace", lineHeight: 1.6 }}>
          // NOTIONAL is leverage-amplified exposure · margin = notional / leverage<br/>
          // LIQ_RISK = % of safety budget consumed · 100% = forced liquidation, you lose the margin<br/>
          // Prices refresh every 5s · LIVE_PNL includes 0.05% close slippage + 0.05% fee
        </div>
      )}

      {pendingClose && (
        <CloseTradeConfirm
          trade={pendingClose.trade}
          metrics={pendingClose.metrics}
          onCancel={() => setPendingClose(null)}
          onConfirm={() => {
            const t = pendingClose.trade;
            setPendingClose(null);
            closeTrade(t);
          }}
        />
      )}
    </Card>
  );
}

/* Confirmation modal for ActiveTradesPanel's CLOSE button. Shows the
   trade's current state in human terms (long / short, notional, live
   PnL, leverage) and forces a deliberate click before /trade/close
   fires. ESC + backdrop click both cancel. */
function CloseTradeConfirm({ trade, metrics, onCancel, onConfirm }) {
  useEffect(() => {
    function onKey(e) { if (e.key === "Escape") onCancel(); }
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [onCancel]);

  const pnl = metrics ? metrics.netPnl : 0;
  const pnlPct = metrics ? metrics.pnlPct : 0;
  const credit = metrics ? metrics.creditIfClosed : 0;
  const isWin = pnl >= 0;

  return (
    <div className="cg-modal-backdrop" onClick={onCancel}>
      <div className="cg-modal" onClick={e => e.stopPropagation()} style={{ width: 460 }}>
        <div className="cg-modal-head">
          <div>
            <div className="cg-modal-eyebrow">// CLOSE_POSITION</div>
            <div className="cg-modal-title">Close this trade?</div>
          </div>
          <button className="cg-modal-x" onClick={onCancel}>×</button>
        </div>
        <div className="cg-modal-body">
          <div className="cg-pine-summary">
            <div className="cg-pine-summary-row"><span>Market</span><span>{trade.market}</span></div>
            <div className="cg-pine-summary-row"><span>Side</span><span>{(trade.side || "").toUpperCase()}</span></div>
            <div className="cg-pine-summary-row"><span>Notional</span><span>${trade.size.toFixed(0)}</span></div>
            <div className="cg-pine-summary-row"><span>Leverage</span><span>{(trade.leverage || 1).toFixed(1)}x</span></div>
            <div className="cg-pine-summary-row">
              <span>Live PnL</span>
              <span style={{ color: isWin ? "var(--cg-pos)" : "var(--cg-neg)", fontWeight: 700 }}>
                {metrics ? `${isWin ? "+" : ""}$${pnl.toFixed(2)} (${pnlPct >= 0 ? "+" : ""}${pnlPct.toFixed(2)}%)` : "—"}
              </span>
            </div>
            {credit > 0 && (
              <div className="cg-pine-summary-row"><span>$CREDIT earned</span><span style={{ color: "var(--cg-accent)" }}>+{credit.toFixed(2)}</span></div>
            )}
          </div>
          <p style={{ fontSize: 12, color: "var(--cg-fg-soft, #A6B0BE)", margin: 0, lineHeight: 1.6 }}>
            {isWin
              ? "Booking the win locks in the PnL above and stops the agent from managing this position further."
              : "Booking the loss locks in the PnL above. If you want to give it more room, cancel — the agent's own stop / take-profit still apply."}
          </p>
          <div className="cg-pine-actions">
            <button className="cg-cta-secondary" onClick={onCancel}>← Keep open</button>
            <button
              className="cg-cta-primary"
              onClick={onConfirm}
              style={!isWin ? { background: "var(--cg-neg, #FF6B6B)", boxShadow: "0 0 24px rgba(255,107,107,0.35)" } : null}
            >
              {isWin ? "BOOK PROFIT →" : "BOOK LOSS →"}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

/* ===============================================================
   RECENT TRADES TABLE
   =============================================================== */

function RecentTradesPanel({ user, onShare }) {
  // Live trades from the campaign API — across EVERY agent the user
  // owns (no agent_id filter), so the dashboard surface matches what
  // the TRADES tab shows. Refetches every 30s.
  const [trades, setTrades] = useState([]);
  const [loaded, setLoaded] = useState(false);
  const [nameById, setNameById] = useState({});      // agent_id → display name

  useEffect(() => {
    function acJwt()  { try { return window.AC_JWT || localStorage.getItem("agc_jwt") || null; } catch (_) { return null; } }
    function acBase() { return ((typeof window !== "undefined" && window.AC_API) || "").replace(/\/$/, ""); }
    let cancelled = false;
    async function refresh() {
      const jwt = acJwt(); const base = acBase();
      if (!jwt || !base) { setLoaded(true); return; }
      try {
        // Pull trades + agents in parallel so the row labels resolve
        // immediately rather than flicker through "—".
        const [tr, ag] = await Promise.all([
          fetch(base + "/trades?is_open=false&limit=10",
            { headers: { Authorization: "Bearer " + jwt }, credentials: "omit" }),
          fetch(base + "/agents",
            { headers: { Authorization: "Bearer " + jwt }, credentials: "omit" }).catch(() => null),
        ]);
        if (!tr.ok) { setLoaded(true); return; }
        const data = await tr.json();
        const agentsJson = (ag && ag.ok) ? await ag.json() : null;
        const agents = (agentsJson && (agentsJson.agents || agentsJson)) || [];
        const map = {};
        for (const a of agents) map[a.id] = a.name;
        if (cancelled) return;
        const rows = (data.trades || []).map(t => {
          const opened = t.opened_at ? new Date(t.opened_at) : null;
          const time = opened ? `${String(opened.getHours()).padStart(2,"0")}:${String(opened.getMinutes()).padStart(2,"0")}` : "—";
          return {
            id:        t.id,
            agent_id:  t.agent_id,
            time,
            market:    t.market,
            side:      (t.side || "").toUpperCase() === "LONG" ? "BUY" : "SELL",
            size:      Number(t.size_usd || 0),
            entry:     Number(t.entry_price || 0),
            exit:      Number(t.exit_price || 0),
            pnl:       Number(t.pnl || 0),
            credit:    Number(t.credit_earned || 0),
          };
        });
        setNameById(map);
        setTrades(rows); setLoaded(true);
      } catch (_) { setLoaded(true); }
    }
    refresh();
    const id = setInterval(refresh, 30_000);
    return () => { cancelled = true; clearInterval(id); };
  }, []);

  return (
    <Card title="RECENT_TRADES — ALL AGENTS">
      <div className="cg-table cg-table-with-agent">
        <div className="cg-tr head">
          <span>TIME</span><span>AGENT</span><span>MARKET</span><span>SIDE</span><span>SIZE</span><span>ENTRY</span><span>EXIT</span><span>PnL</span><span>$CREDIT</span><span></span>
        </div>
        {loaded && trades.length === 0 && (
          <div className="cg-tr" style={{ gridTemplateColumns: "1fr", padding: "20px 12px", color: "var(--muted)", fontStyle: "italic" }}>
            No closed trades yet — your agents tick every ~15s. New trades show up here as they close.
          </div>
        )}
        {!loaded && trades.length === 0 && (
          <div className="cg-tr" style={{ gridTemplateColumns: "1fr", padding: "20px 12px", color: "var(--muted)" }}>
            Loading…
          </div>
        )}
        {trades.map((t, i) => (
          <div key={t.id || i} className="cg-tr">
            <span>{t.time}</span>
            <span className="cg-tt-agent" title={t.agent_id}>{nameById[t.agent_id] || "—"}</span>
            <span>{(t.market || "").replace(/-PERP$/, "")}</span>
            <span className={t.side === "BUY" ? "accent" : ""}>{t.side === "BUY" ? "LONG" : "SHORT"}</span>
            <span>${t.size.toFixed(0)}</span>
            <span>${t.entry.toLocaleString(undefined, { maximumFractionDigits: 2 })}</span>
            <span>${t.exit ? t.exit.toLocaleString(undefined, { maximumFractionDigits: 2 }) : "—"}</span>
            <span className={t.pnl >= 0 ? "pos" : "neg"}>{t.pnl >= 0 ? "+" : ""}${t.pnl.toFixed(2)}</span>
            <span className="accent">{t.credit > 0 ? "+" + t.credit.toFixed(2) : t.credit < 0 ? t.credit.toFixed(2) : "—"}</span>
            <button
              className={"cg-share-cta" + (t.pnl >= 0 ? " win" : " loss")}
              onClick={() => onShare && onShare(t)}
              aria-label={"Share " + t.market + " trade"}
            >
              SHARE <span className="cg-share-cta-arrow">↗</span>
            </button>
          </div>
        ))}
      </div>
    </Card>
  );
}

/* ===============================================================
   DECISION JOURNAL — past strategy reviews, transparent reasoning
   =============================================================== */
/* DecisionJournalPanel — strategy-update history across EVERY agent the
   user owns. Previously hardcoded to user.agents[0]. Now pulls decisions
   from each agent in parallel, merges newest-first, tags each row with
   the agent name. A filter dropdown narrows to a single agent.

   For every row we compute the DELTA vs the previous decision for the
   SAME agent — that's what the user can actually act on hour over hour
   ("oh, the agent flipped BTC to short and tightened the stop"). */
function DecisionJournalPanel({ user }) {
  const [byAgent, setByAgent]   = useState({});      // agent_id → decisions[]
  const [agents, setAgents]     = useState([]);      // full agent rows for label lookup
  const [filter, setFilter]     = useState("all");
  const [loaded, setLoaded]     = useState(false);
  const [openId, setOpenId]     = useState(null);    // id of decision whose delta is expanded
  const userAgentIds = (user && user.agents || []).map(a => a.id).filter(Boolean);

  useEffect(() => {
    function acJwt()  { try { return window.AC_JWT || localStorage.getItem("agc_jwt") || null; } catch (_) { return null; } }
    function acBase() { return ((typeof window !== "undefined" && window.AC_API) || "").replace(/\/$/, ""); }
    let cancelled = false;

    async function refresh() {
      const jwt = acJwt(); const base = acBase();
      if (!jwt || !base) return;
      try {
        // Pull the canonical agent list + every agent's recent decisions
        // in parallel so a slow per-agent endpoint can't gate the whole UI.
        const agentsResp = await fetch(`${base}/agents`,
          { headers: { Authorization: "Bearer " + jwt }, credentials: "omit" });
        if (!agentsResp.ok) { setLoaded(true); return; }
        const ag = await agentsResp.json();
        const list = (ag && (ag.agents || ag)) || [];
        if (cancelled) return;
        setAgents(list);

        const fetches = list.map(a =>
          fetch(`${base}/agents/${a.id}/decisions?limit=24`,
            { headers: { Authorization: "Bearer " + jwt }, credentials: "omit" })
            .then(r => r.ok ? r.json() : null)
            .then(j => [a.id, (j && j.decisions) || []])
            .catch(() => [a.id, []])
        );
        const pairs = await Promise.all(fetches);
        if (cancelled) return;
        const byAgentNext = {};
        for (const [id, rows] of pairs) byAgentNext[id] = rows;
        setByAgent(byAgentNext);
      } catch (_) {
        /* keep stale; next tick will retry */
      } finally {
        if (!cancelled) setLoaded(true);
      }
    }
    refresh();
    const id = setInterval(refresh, 60_000);
    return () => { cancelled = true; clearInterval(id); };
  }, [userAgentIds.join(",")]);

  // Resolution maps
  const nameById = {};
  for (const a of agents) nameById[a.id] = a.name;

  // Flatten + sort. Each row carries its agent_id so we can compute the
  // delta against the PRIOR decision for the same agent.
  const flat = [];
  for (const [aid, rows] of Object.entries(byAgent)) {
    for (const r of rows) flat.push({ ...r, _agent_id: aid });
  }
  flat.sort((a, b) => (b.decided_at || "").localeCompare(a.decided_at || ""));

  const view = filter === "all" ? flat : flat.filter(r => r._agent_id === filter);

  return (
    <Card
      title="AGENT_DECISIONS — STRATEGY_HISTORY"
      right={
        agents.length > 1 ? (
          <select
            className="cg-trades-sel"
            value={filter}
            onChange={e => setFilter(e.target.value)}
            style={{ fontSize: 11, padding: "3px 8px" }}
          >
            <option value="all">All agents</option>
            {agents.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
          </select>
        ) : null
      }
    >
      {/* Strategy timeline — visual strip per agent above the text feed */}
      <StrategyTimeline
        agents={agents}
        byAgent={byAgent}
        filter={filter}
        openId={openId}
        onSelect={setOpenId}
      />

      {/* Text feed */}
      {!loaded && (
        <div style={{ padding: "16px 4px", color: "var(--muted)" }}>Loading…</div>
      )}
      {loaded && view.length === 0 && (
        <div style={{ padding: "16px 4px", color: "var(--muted)", fontStyle: "italic" }}>
          No strategy updates yet. Each agent reviews its strategy every ~1 hour —
          new decisions appear here as they happen.
        </div>
      )}
      {view.length > 0 && (
        <div className="cg-journal">
          {view.map(d => {
            const ts = d.decided_at ? new Date(d.decided_at) : null;
            const when = ts ? ts.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : "—";
            const strat = d.strategy || {};
            const biases = strat.market_bias || {};
            const biasChips = Object.entries(biases);
            const agentName = nameById[d._agent_id] || "—";

            // Find the previous decision for THIS agent to compute the delta.
            const prior = (byAgent[d._agent_id] || []).find(
              x => (x.decided_at || "") < (d.decided_at || "")
            );
            const deltas = _strategyDeltas(prior, d);

            const isOpen = openId === d.id;

            return (
              <div
                key={d.id}
                style={{
                  padding: "12px 0",
                  borderBottom: "1px dashed var(--border)",
                  fontSize: 13,
                  lineHeight: 1.55,
                  cursor: deltas.length ? "pointer" : "default",
                }}
                onClick={() => setOpenId(isOpen ? null : d.id)}
              >
                <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6, fontSize: 11, color: "var(--muted)", gap: 8, alignItems: "center" }}>
                  <span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
                    <span
                      className="mono"
                      style={{
                        background: "rgba(29,158,117,0.10)",
                        color: "var(--cg-accent)",
                        padding: "2px 8px",
                        borderRadius: 3,
                        fontSize: 10,
                        letterSpacing: 0.5,
                        fontWeight: 700,
                      }}
                    >{agentName}</span>
                    <span className="mono">// {when} · DAY {d.day ?? "—"}</span>
                  </span>
                  <span className="mono">
                    {strat.aggression ? `${(strat.aggression || "").toUpperCase()}` : ""}
                    {strat.target_position_size_pct != null ? ` · ${strat.target_position_size_pct}% size` : ""}
                    {strat.stop_loss_pct != null ? ` · ${strat.stop_loss_pct}% SL` : ""}
                  </span>
                </div>
                <div style={{ color: "var(--fg)", marginBottom: 8 }}>{d.reasoning || "(no reasoning)"}</div>
                {biasChips.length > 0 && (
                  <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
                    {biasChips.map(([m, b]) => (
                      <span key={m} className="mono" style={{
                        fontSize: 11,
                        padding: "2px 8px",
                        borderRadius: 4,
                        background:
                          b === "long"  ? "rgba(29,158,117,0.16)" :
                          b === "short" ? "rgba(220,38,38,0.16)" :
                          "var(--bg)",
                        color:
                          b === "long"  ? "var(--accent)" :
                          b === "short" ? "var(--err, #DC2626)" :
                          "var(--muted)",
                        border: "1px solid var(--border)",
                      }}>
                        {m} · {b}
                      </span>
                    ))}
                  </div>
                )}
                {/* Delta panel — only when the row is expanded and there's
                    something to show vs the previous decision. */}
                {isOpen && deltas.length > 0 && (
                  <div style={{
                    marginTop: 10,
                    padding: "10px 12px",
                    background: "rgba(29,158,117,0.04)",
                    borderLeft: "2px solid var(--cg-accent)",
                    fontSize: 12,
                  }}>
                    <div style={{ color: "var(--cg-muted)", fontSize: 10, letterSpacing: 1, marginBottom: 6 }}>
                      // CHANGES VS PREVIOUS REVIEW
                    </div>
                    {deltas.map((dlt, i) => (
                      <div key={i} className="mono" style={{ marginBottom: 3 }}>
                        <span style={{ color: "var(--cg-muted)" }}>{dlt.label}: </span>
                        <span style={{ color: "var(--cg-fg-soft, #A6B0BE)" }}>{dlt.from}</span>
                        <span style={{ color: "var(--cg-accent)", margin: "0 6px" }}>→</span>
                        <span style={{ color: "var(--cg-fg)", fontWeight: 700 }}>{dlt.to}</span>
                      </div>
                    ))}
                  </div>
                )}
              </div>
            );
          })}
        </div>
      )}
    </Card>
  );
}

/* Strategy timeline — visual strip per agent. Each node = one strategy
   review. Colour reflects the dominant bias of that review's strategy:
   long-dominant → green, short → red, neutral → gray, mixed → split.
   Click a node to expand its delta vs the prior node. The strip is
   capped at 12 nodes per agent so it fits on a phone. */
function StrategyTimeline({ agents, byAgent, filter, openId, onSelect }) {
  if (!agents || agents.length === 0) return null;
  const visibleAgents = filter === "all" ? agents : agents.filter(a => a.id === filter);
  if (visibleAgents.length === 0) return null;

  function dominantColor(strategy) {
    const biases = (strategy && strategy.market_bias) || {};
    const vals = Object.values(biases).filter(v => v);
    if (vals.length === 0) return { bg: "var(--cg-tint-2)", fg: "var(--cg-muted)" };
    const longs  = vals.filter(v => v === "long").length;
    const shorts = vals.filter(v => v === "short").length;
    const neut   = vals.length - longs - shorts;
    if (longs > shorts && longs >= neut)  return { bg: "rgba(29,158,117,0.55)", fg: "var(--cg-bg)" };
    if (shorts > longs && shorts >= neut) return { bg: "rgba(220,38,38,0.55)",  fg: "#fff" };
    if (longs > 0 && shorts > 0)          return { bg: "linear-gradient(90deg, rgba(29,158,117,0.55) 50%, rgba(220,38,38,0.55) 50%)", fg: "#fff" };
    return { bg: "var(--cg-tint-2)", fg: "var(--cg-muted)" };
  }

  function tooltipFor(d) {
    const t = d.decided_at ? new Date(d.decided_at) : null;
    const when = t ? t.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : "—";
    const biases = (d.strategy && d.strategy.market_bias) || {};
    const summary = Object.entries(biases).map(([m, b]) => `${m.replace(/-PERP$/, "")}=${b}`).join(", ") || "—";
    return `${when} · ${summary}`;
  }

  return (
    <div className="cg-strat-timeline">
      {visibleAgents.map(a => {
        // Newest LAST so the strip reads left-to-right oldest → newest,
        // which is what users expect from a timeline.
        const rows = (byAgent[a.id] || []).slice().sort((x, y) => (x.decided_at || "").localeCompare(y.decided_at || "")).slice(-12);
        return (
          <div key={a.id} className="cg-strat-timeline-row">
            <div className="cg-strat-timeline-label">{a.name}</div>
            <div className="cg-strat-timeline-strip">
              {rows.length === 0 && (
                <span className="cg-strat-timeline-empty">— no decisions yet —</span>
              )}
              {rows.map(d => {
                const colour = dominantColor(d.strategy);
                const isOpen = openId === d.id;
                return (
                  <button
                    key={d.id}
                    className={"cg-strat-node" + (isOpen ? " selected" : "")}
                    style={{ background: colour.bg, color: colour.fg }}
                    onClick={(e) => { e.stopPropagation(); onSelect(isOpen ? null : d.id); }}
                    title={tooltipFor(d)}
                    aria-label={"Decision " + (d.decided_at || "")}
                  >
                    {/* Tiny diagonal slash marker when stop or leverage changed
                        vs the previous node — gives users a quick "ooh, something
                        moved" cue even before they click. */}
                  </button>
                );
              })}
            </div>
          </div>
        );
      })}
    </div>
  );
}

/* Compute strategy deltas between two consecutive decisions for the same
   agent. Returns [{label, from, to}, ...] for fields that changed. Empty
   when nothing changed or there's no prior decision. */
function _strategyDeltas(prior, current) {
  if (!prior || !current) return [];
  const p = prior.strategy || {};
  const c = current.strategy || {};
  const out = [];

  // Numeric scalars
  const numericFields = [
    ["stop_loss_pct",              "Stop loss"],
    ["take_profit_pct",            "Take profit"],
    ["target_position_size_pct",   "Position size"],
    ["leverage",                   "Leverage"],
    ["max_positions",              "Max positions"],
    ["trailing_trigger_pct",       "Trailing trigger"],
    ["trailing_lock_pct",          "Trailing lock"],
  ];
  for (const [key, label] of numericFields) {
    const pv = p[key];
    const cv = c[key];
    if (pv == null && cv == null) continue;
    if (Number(pv) !== Number(cv)) {
      out.push({
        label,
        from: pv == null ? "—" : String(pv),
        to:   cv == null ? "—" : String(cv),
      });
    }
  }

  // Aggression string
  if ((p.aggression || "") !== (c.aggression || "")) {
    out.push({
      label: "Aggression",
      from:  (p.aggression || "—").toUpperCase(),
      to:    (c.aggression || "—").toUpperCase(),
    });
  }

  // Per-market bias deltas (the most interesting changes — flips matter most)
  const pBias = p.market_bias || {};
  const cBias = c.market_bias || {};
  const markets = Array.from(new Set([...Object.keys(pBias), ...Object.keys(cBias)]));
  for (const m of markets) {
    const pb = pBias[m] || "—";
    const cb = cBias[m] || "—";
    if (pb !== cb) {
      out.push({
        label: m.replace(/-PERP$/, "") + " bias",
        from:  pb,
        to:    cb,
      });
    }
  }
  return out;
}

/* ===============================================================
   CAMPAIGN DASHBOARD (main view)
   =============================================================== */

function CampaignDashboard({ user, onGraduate }) {
  const [shareTrade, setShareTrade] = useState(null);

  // Real agents come from two sources, kept in sync:
  //   (a) user.agents on the parent prop, populated by the periodic /me
  //       refresh in CampaignApp. This is the canonical source.
  //   (b) a direct GET /agents fetch on mount + tab-visibility regain
  //       (in refreshAgents() below), which catches the case where the
  //       user creates an agent, switches tabs, and the parent's /me
  //       hasn't repolled yet. Belt-and-suspenders — both ultimately
  //       call setAgents with the same shape so they don't race.
  const [agents, setAgents] = useState(() => Array.isArray(user.agents) ? user.agents : []);
  useEffect(() => {
    if (Array.isArray(user.agents)) setAgents(user.agents);
  }, [user.agents]);
  const [addOpen, setAddOpen] = useState(false);
  const [reallocSource, setReallocSource] = useState(null);  // halted-agent passed to ReallocateModal
  const [dismissBanner, setDismissBanner] = useState(false);
  const toast = useToast && useToast();

  // Adapter — the API returns campaign.agents rows (status / agent_type /
  // strategy_state). Map to the legacy shape the panel renderer expects.
  function _normaliseAgent(a) {
    const strat = a.strategy_state || {};
    return {
      id:           a.id,
      name:         a.name,
      type:         a.agent_tier || a.agent_type || "claude_lite",
      status:       a.status,
      risk_state:   a.risk_state || null,
      paperBalance: Number(a.paper_balance || 0),
      pauseReason:  a.pause_reason || null,
      pnlToday:     Number(a.pnl_today || 0),
      day:          Number(a.day || 0),
      reasoning:    strat.reasoning || strat.personality || a.personality || "Configuring — first trade incoming.",
      kyaId:        a.kya_id || null,
      kyaStatus:    a.kya_status || "unassigned",
    };
  }

  const refreshAgents = useCallback(async () => {
    try {
      const data = await acGet("/agents");
      if (!data) return;                          // stub mode — keep existing state
      const list = Array.isArray(data) ? data : (data.agents || []);
      setAgents(list.map(_normaliseAgent));
    } catch (_) { /* surface via toast on next action */ }
  }, []);

  // Refetch on mount and on tab-visibility regain. Without this, an agent
  // created in the [+ Add agent] modal disappears the moment the user
  // navigates away and back (since the component remounts and the local
  // state resets to the stale `user.agents` snapshot from login).
  useEffect(() => {
    refreshAgents();
    function onVisibility() { if (!document.hidden) refreshAgents(); }
    document.addEventListener("visibilitychange", onVisibility);
    return () => document.removeEventListener("visibilitychange", onVisibility);
  }, [refreshAgents]);

  const paused = agents.find(a => a.status === "paused");
  async function resume(id) {
    try {
      await acSend("/agents/" + id, { status: "active" }, "PUT");
    } catch (_) { /* fall through; refresh below will surface real state */ }
    refreshAgents();
  }

  function bannerCopy(reason) {
    if (reason === "inactive_user") return "Your agent was paused for inactivity — it's back on now.";
    if (reason === "underperforming") return "An agent was paused after 14 days of low engagement. Edit its strategy to resume.";
    return "An agent is paused — make a trade or customise it to bring it back.";
  }

  return (
    <div className="cg-dash">
      {paused && !dismissBanner && (
        <div className="cg-pause-banner">
          <span className="cg-pause-ico">⏸</span>
          <span className="cg-pause-txt">{bannerCopy(paused.pauseReason)}</span>
          <button className="cg-pause-resume" onClick={() => { resume(paused.id); toast && toast.push({ title: paused.name + " resumed", meta: "Trading live again", duration: 3000 }); }}>RESUME →</button>
          <button className="cg-pause-x" onClick={() => setDismissBanner(true)}>×</button>
        </div>
      )}

      {/* Active trades — moved to the top so users see their open
          positions + live PnL before scrolling. Was previously in the
          bottom row alongside recent trades. */}
      <div style={{ marginBottom: 16 }}>
        <ActiveTradesPanel user={user} />
      </div>

      {/* Hero loan-eligibility strip — the highest-emotion data point on
          the page elevated to a single-click jump target. */}
      <LoanProgressStrip user={user} onJump={() => {
        const el = document.getElementById("cg-loan-eligibility");
        if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
      }} />

      <CombinedAgentsPanel combined={user && user.combined} />

      <AgentsGridPanel
        agents={agents}
        onResume={(id) => { resume(id); toast && toast.push({ title: "Agent resumed", duration: 2500 }); }}
        onAdd={() => setAddOpen(true)}
        onReallocate={(a) => setReallocSource(a)}
      />

      <div style={{ marginBottom: 16 }}>
        <EquityChartPanel user={user} />
      </div>

      <div className="cg-dash-grid">
        <div className="cg-dash-left">
          <AgentPanel user={user} />
          <ScorePanel user={user} />
          <EligibilityPanel user={user} />
          <CreditPanel user={user} />
        </div>
        <div className="cg-dash-right">
          <ReasoningFeed user={user} onShare={setShareTrade} />
          <DecisionJournalPanel user={user} />
          <ScoreHistoryPanel user={user} />
        </div>
      </div>
      <div className="cg-dash-bottom">
        <RecentTradesPanel user={user} onShare={setShareTrade} />
      </div>
      {shareTrade && <ShareModal trade={shareTrade} user={user} onClose={() => setShareTrade(null)} />}
      {reallocSource && (
        <ReallocateCapitalModal
          source={reallocSource}
          agents={agents}
          onClose={() => setReallocSource(null)}
          onTransferred={() => {
            setReallocSource(null);
            toast && toast.push({ title: "Capital moved", meta: "Balance updates on the next refresh", duration: 3000 });
            refreshAgents();
          }}
          onCreateNew={() => {
            // Hand off to the existing AddAgentModal with the halted
            // agent as the funding source pre-selected.
            setReallocSource(null);
            setAddOpen(true);
          }}
        />
      )}

      {addOpen && (
        <AddAgentModal
          onClose={() => setAddOpen(false)}
          onCreated={(agent) => {
            // Optimistic insert so the new card appears instantly; the
            // visibility-driven refetch will reconcile against the server.
            setAgents(as => [...as, _normaliseAgent(agent)]);
            setAddOpen(false);
            toast && toast.push({ title: agent.name + " launched", meta: "Agent is live", duration: 3000 });
            refreshAgents();
          }}
        />
      )}
    </div>
  );
}

/* Multi-agent roster grid */
/* ===============================================================
   COMBINED AGENTS PANEL — per-agent PnL/Sharpe + aggregate totals
   Reads from user.combined (populated by /me's combined block in
   campaign/api/routes/registration.py).
   =============================================================== */
function CombinedAgentsPanel({ combined }) {
  if (!combined || !combined.agents || combined.agents.length === 0) return null;
  const t = combined.total || {};

  function fmtUsd(n)  { const v = Number(n || 0); const sign = v < 0 ? "-" : ""; return `${sign}$${Math.abs(v).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; }
  function pctClass(v) { return Number(v || 0) >= 0 ? "cg-num pos" : "cg-num neg"; }
  function sharpeTone(s) {
    const v = Number(s || 0);
    if (v >= 2)   return "var(--accent)";
    if (v >= 1)   return "var(--blue, #5b8def)";
    if (v >  0)   return "var(--fg)";
    return "var(--red, #b13b3b)";
  }
  function stateBadge(rs) {
    if (rs === "halted")   return <span style={{ fontSize: 9, letterSpacing: 1, padding: "1px 5px", marginLeft: 6, color: "#fff", background: "var(--red, #b13b3b)", borderRadius: 3 }}>DONE</span>;
    if (rs === "hard_cut") return <span style={{ fontSize: 9, letterSpacing: 1, padding: "1px 5px", marginLeft: 6, color: "var(--red, #b13b3b)", border: "1px solid rgba(177,59,59,0.45)", borderRadius: 3 }}>HARD</span>;
    if (rs === "soft_cut") return <span style={{ fontSize: 9, letterSpacing: 1, padding: "1px 5px", marginLeft: 6, color: "var(--amber, #d29022)", border: "1px solid rgba(210,144,34,0.45)", borderRadius: 3 }}>SOFT</span>;
    return null;
  }

  return (
    <div className="cg-panel" style={{ marginBottom: 16 }}>
      <div className="cg-panel-head">
        <div className="cg-panel-eyebrow">// ALL_AGENTS · COMBINED</div>
        <div className="cg-panel-meta">
          {t.agent_count} agent{t.agent_count === 1 ? "" : "s"} ·
          {" "}{t.qualified_count > 0 ? `${t.qualified_count} qualified` : "no qualified yet"}
          {t.halted_count > 0 ? ` · ${t.halted_count} done` : ""}
        </div>
      </div>

      <div className="cg-panel-body" style={{ padding: 0 }}>
        <div style={{ overflowX: "auto" }}>
          <table className="cg-table" style={{ width: "100%", minWidth: 700, borderCollapse: "collapse", fontFamily: "JetBrains Mono, monospace", fontSize: 12 }}>
            <thead>
              <tr style={{ color: "var(--cg-muted)", letterSpacing: 1, fontSize: 10, textTransform: "uppercase" }}>
                <th style={{ textAlign: "left",  padding: "10px 14px" }}>Agent</th>
                <th style={{ textAlign: "right", padding: "10px 14px" }} title="Initial seed + realized PnL. Matches ACCOUNT_EQUITY at the top of the page. Includes value locked as margin on open positions.">Equity</th>
                <th style={{ textAlign: "right", padding: "10px 14px" }}>PnL</th>
                <th style={{ textAlign: "right", padding: "10px 14px" }}>PnL %</th>
                <th style={{ textAlign: "right", padding: "10px 14px" }}>Sharpe</th>
                <th style={{ textAlign: "right", padding: "10px 14px" }}>Drawdown</th>
                <th style={{ textAlign: "right", padding: "10px 14px" }}>Score</th>
                <th style={{ textAlign: "right", padding: "10px 14px" }}>Tier</th>
                <th style={{ textAlign: "right", padding: "10px 14px" }}>Trades</th>
              </tr>
            </thead>
            <tbody>
              {combined.agents.map(a => {
                const equity = Number(a.initial_balance || 0) + Number(a.total_pnl || 0);
                const free   = Number(a.current_balance || 0);
                const locked = Math.max(0, equity - free);
                return (
                <tr key={a.id} style={{ borderTop: "1px solid var(--border)" }}>
                  <td style={{ padding: "10px 14px", color: "var(--fg)" }}>
                    {a.name}{stateBadge(a.risk_state)}
                  </td>
                  <td style={{ padding: "10px 14px", textAlign: "right" }}>
                    <div>{fmtUsd(equity)}</div>
                    <div style={{ fontSize: 10, color: "var(--cg-muted)", marginTop: 2, letterSpacing: 0.3 }}>
                      free {fmtUsd(free)} · locked {fmtUsd(locked)}
                    </div>
                  </td>
                  <td className={pctClass(a.total_pnl)}      style={{ padding: "10px 14px", textAlign: "right" }}>{fmtUsd(a.total_pnl)}</td>
                  <td className={pctClass(a.total_pnl_pct)}  style={{ padding: "10px 14px", textAlign: "right" }}>{Number(a.total_pnl_pct).toFixed(2)}%</td>
                  <td style={{ padding: "10px 14px", textAlign: "right", color: sharpeTone(a.sharpe_ratio), fontWeight: 600 }}>{Number(a.sharpe_ratio).toFixed(2)}</td>
                  <td style={{ padding: "10px 14px", textAlign: "right" }}>{Number(a.max_drawdown).toFixed(1)}%</td>
                  <td style={{ padding: "10px 14px", textAlign: "right", color: a.credit_score >= 670 ? "var(--accent)" : "var(--fg)" }}>{a.credit_score || "—"}</td>
                  <td style={{ padding: "10px 14px", textAlign: "right", color: "var(--cg-muted)", textTransform: "capitalize" }}>{a.qualification_tier || "unrated"}</td>
                  <td style={{ padding: "10px 14px", textAlign: "right" }}>{a.total_trades}</td>
                </tr>
                );
              })}
              {/* Aggregate row */}
              {(() => {
                const tEquity = Number(t.initial_balance || 0) + Number(t.total_pnl || 0);
                const tFree   = Number(t.current_balance || 0);
                const tLocked = Math.max(0, tEquity - tFree);
                return (
              <tr style={{ borderTop: "2px solid var(--accent)", background: "rgba(29,158,117,0.06)" }}>
                <td style={{ padding: "12px 14px", color: "var(--accent)", letterSpacing: 1, fontSize: 11 }}>TOTAL</td>
                <td style={{ padding: "12px 14px", textAlign: "right", color: "var(--fg)", fontWeight: 600 }}>
                  <div>{fmtUsd(tEquity)}</div>
                  <div style={{ fontSize: 10, color: "var(--cg-muted)", marginTop: 2, fontWeight: 400, letterSpacing: 0.3 }}>
                    free {fmtUsd(tFree)} · locked {fmtUsd(tLocked)}
                  </div>
                </td>
                <td className={pctClass(t.total_pnl)}     style={{ padding: "12px 14px", textAlign: "right", fontWeight: 600 }}>{fmtUsd(t.total_pnl)}</td>
                <td className={pctClass(t.total_pnl_pct)} style={{ padding: "12px 14px", textAlign: "right", fontWeight: 600 }}>{Number(t.total_pnl_pct).toFixed(2)}%</td>
                <td style={{ padding: "12px 14px", textAlign: "right", color: sharpeTone(t.combined_sharpe), fontWeight: 700 }}>{Number(t.combined_sharpe).toFixed(2)}</td>
                <td style={{ padding: "12px 14px", textAlign: "right", color: "var(--cg-muted)" }}>worst {Number(t.worst_drawdown).toFixed(1)}%</td>
                <td style={{ padding: "12px 14px", textAlign: "right", color: t.best_score >= 670 ? "var(--accent)" : "var(--fg)", fontWeight: 700 }}>{t.best_score || "—"}</td>
                <td style={{ padding: "12px 14px", textAlign: "right", color: "var(--accent)", textTransform: "capitalize", fontWeight: 600 }}>{t.best_tier || "unrated"}</td>
                <td style={{ padding: "12px 14px", textAlign: "right", fontWeight: 600 }}>{t.total_trades}</td>
              </tr>
                );
              })()}
            </tbody>
          </table>
        </div>
        <div style={{ padding: "10px 14px", fontSize: 10, color: "var(--cg-muted)", lineHeight: 1.6, borderTop: "1px solid var(--border)" }}>
          Sharpe is the annualised risk-adjusted return per agent. Aggregate Sharpe is weighted by trade count. Higher is better; below zero means a net-losing strategy.
        </div>
      </div>
    </div>
  );
}


function AgentsGridPanel({ agents, onResume, onAdd, onReallocate }) {
  function statusBadge(a) {
    if (a.risk_state === "halted") {
      return <span style={{ fontSize: 10, letterSpacing: 1, padding: "2px 6px", color: "#fff", background: "var(--red, #b13b3b)", borderRadius: 3 }}>⛔ DONE</span>;
    }
    if (a.risk_state === "hard_cut") {
      return <span style={{ fontSize: 10, letterSpacing: 1, padding: "2px 6px", color: "var(--red, #b13b3b)", border: "1px solid rgba(177,59,59,0.45)", borderRadius: 3 }}>🛑 HARD</span>;
    }
    if (a.risk_state === "soft_cut") {
      return <span style={{ fontSize: 10, letterSpacing: 1, padding: "2px 6px", color: "var(--amber, #d29022)", border: "1px solid rgba(210,144,34,0.45)", borderRadius: 3 }}>⚠ SOFT</span>;
    }
    return <span className={"cg-agent-status " + a.status}>{a.status === "active" ? "● LIVE" : "⏸ PAUSED"}</span>;
  }

  return (
    <Card title={"YOUR_AGENTS — " + agents.length} right={<span className="cg-help">{agents.filter(a => a.status === "active" && a.risk_state !== "halted").length} active</span>}>
      <div className="cg-agents-grid">
        {agents.map(a => {
          const isDone = a.risk_state === "halted";
          return (
            <div key={a.id} className={
              "cg-agent-card" +
              (a.status === "paused" ? " paused" : "") +
              (isDone ? " done" : "")
            } style={isDone ? { opacity: 0.85, borderColor: "var(--red, #b13b3b)" } : null}>
              <div className="cg-agent-top">
                <span className="cg-agent-name">{a.name}</span>
                {statusBadge(a)}
              </div>
              <div className="cg-agent-type">{a.type} · Day {a.day}</div>
              <KyaBadge id={a.kyaId} status={a.kyaStatus} />
              <div className="cg-agent-reason">
                {isDone
                  ? `Trailing 12% floor breached. ${a.paperBalance > 0 ? "Move the remaining $" + a.paperBalance.toFixed(2) + " into a new or existing agent." : "Balance fully drawn down."}`
                  : a.reasoning}
              </div>
              <div className="cg-agent-foot">
                {isDone
                  ? <span className="cg-agent-pnl" style={{ color: "var(--cg-muted)" }}>Balance ${a.paperBalance.toFixed(2)}</span>
                  : <span className={"cg-agent-pnl " + (a.pnlToday >= 0 ? "pos" : "neg")}>{a.pnlToday >= 0 ? "+" : ""}${a.pnlToday} today</span>
                }
                {isDone
                  ? (a.paperBalance > 0
                      ? <button className="cg-agent-resume" onClick={() => onReallocate && onReallocate(a)}>MOVE CAPITAL →</button>
                      : <span className="cg-agent-open" style={{ color: "var(--cg-muted)" }}>—</span>)
                  : a.status === "paused"
                    ? <button className="cg-agent-resume" onClick={() => onResume(a.id)}>RESUME →</button>
                    : <span className="cg-agent-open">Open →</span>
                }
              </div>
            </div>
          );
        })}
        <button className="cg-agent-add" onClick={onAdd}>
          <span className="cg-agent-add-plus">+</span>
          <span>Add agent</span>
        </button>
      </div>
    </Card>
  );
}

/* ===============================================================
   REALLOCATE CAPITAL MODAL — for halted agents
   User picks a destination: existing owned agent (POST /agents/:id/
   transfer) OR a new agent (delegates to AddAgentModal with source
   pre-selected). Default amount is the full source balance.
   =============================================================== */


function ShareModal({ trade, user, onClose }) {
  const canvasRef = useRef(null);
  const toast = useToast && useToast();
  const t = normalizeTrade(trade);
  // Inside the Telegram Mini App, referrals should point at the bot's
  // deep link so the tap opens the Mini App natively (no browser hop,
  // start_param carries the referral code straight to /auth/telegram/mini-app).
  // On the open web we keep the classic /join?ref= URL so the marketing
  // landing captures the referral before signup.
  const AC = (typeof window !== "undefined" && window.AC) || {};
  const botUser = (typeof window !== "undefined" && window.AC_TG_BOT) || "Agenticscreditbot";
  const inTelegram = AC.platform && AC.platform.isTelegram;
  const refUrl = inTelegram
    ? `https://t.me/${botUser}/app?startapp=ref_${user.referralCode}`
    : `https://agentics.credit/join?ref=${user.referralCode}`;
  const shareText =
    `My AI agent ${user.agentName} just closed ${t.market} ${t.side} for ${t.profit ? "+" : ""}$${Math.abs(t.pnl).toLocaleString()} (${t.pnlPct > 0 ? "+" : ""}${t.pnlPct}%) on @agenticscredit Genesis Campaign 🟢\n\nBuild your own trading agent, get $1,000 paper USDT:`;

  // render the canvas whenever modal opens
  useEffect(() => {
    if (canvasRef.current) drawShareCard(canvasRef.current, trade, user);
  }, [trade, user]);

  async function getBlob() {
    return new Promise((resolve) => {
      if (!canvasRef.current) return resolve(null);
      canvasRef.current.toBlob(b => resolve(b), "image/png");
    });
  }

  async function copyImage() {
    try {
      const blob = await getBlob();
      if (blob && navigator.clipboard && window.ClipboardItem) {
        await navigator.clipboard.write([new window.ClipboardItem({ "image/png": blob })]);
        toast && toast.push({ title: "Image copied", meta: "Paste into X, Telegram, anywhere", duration: 3000 });
      } else {
        downloadImage();
      }
    } catch (_) { downloadImage(); }
  }

  async function downloadImage() {
    const blob = await getBlob();
    if (!blob) return;
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url; a.download = `agentics-${user.agentName}-${t.market}.png`;
    document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
    toast && toast.push({ title: "Image downloaded", meta: "Attach it to your post", duration: 3000 });
  }

  function shareX() {
    const u = `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(refUrl)}`;
    window.open(u, "_blank", "noopener,width=600,height=620");
    copyImage();
  }
  function shareTelegram() {
    // Inside the Mini App we use switchInlineQuery — pops the native
    // chat-picker so the user shares straight into a Telegram chat
    // without leaving the app. On web (or if the SDK is missing) fall
    // back to the classic t.me/share/url flow that opens a share popup.
    const tg = window.Telegram && window.Telegram.WebApp;
    if (inTelegram && tg && tg.switchInlineQuery) {
      try {
        tg.switchInlineQuery(shareText + "\n\n" + refUrl, ["users", "groups"]);
        copyImage();
        return;
      } catch (_) { /* fall through to browser flow */ }
    }
    const u = `https://t.me/share/url?url=${encodeURIComponent(refUrl)}&text=${encodeURIComponent(shareText)}`;
    window.open(u, "_blank", "noopener,width=600,height=620");
    copyImage();
  }
  function copyLink() {
    navigator.clipboard && navigator.clipboard.writeText(refUrl);
    toast && toast.push({ title: "Referral link copied", duration: 2200 });
  }

  return (
    <div className="cg-modal-backdrop" onClick={onClose}>
      <div className="cg-share-modal" onClick={e => e.stopPropagation()}>
        <div className="cg-share-head">
          <div>
            <div className="cg-modal-eyebrow">// SHARE_TRADE</div>
            <div className="cg-modal-title">Share your {t.profit ? "win" : "trade"}</div>
          </div>
          <button className="cg-modal-x" onClick={onClose}>×</button>
        </div>

        {/* live canvas preview of the actual image that gets shared */}
        <div className="cg-share-canvas-wrap">
          <canvas ref={canvasRef} className="cg-share-canvas" />
        </div>

        <div className="cg-share-hint">↑ This exact image is copied to your clipboard when you share. Paste it into the post.</div>

        <div className="cg-share-actions">
          <button className="cg-cta-primary" onClick={shareX}>SHARE ON X →</button>
          <button className="cg-cta-ghost" onClick={shareTelegram}>[TELEGRAM]</button>
          <button className="cg-cta-ghost" onClick={copyImage}>[COPY IMAGE]</button>
          <button className="cg-cta-ghost" onClick={downloadImage}>[DOWNLOAD]</button>
          <button className="cg-cta-ghost" onClick={copyLink}>[COPY LINK]</button>
        </div>
      </div>
    </div>
  );
}

/* ===============================================================
   GRADUATION SCREEN
   =============================================================== */

function GraduationScreen({ user, onDeploy, onKeepRunning }) {
  return (
    <div className="cg-grad">
      <div className="cg-grad-card">
        <div className="cg-grad-target">🎯 {user.agentName} <span className="accent">QUALIFIES</span></div>
        <div className="cg-grad-sub">After {user.day} days your agent has proven itself.</div>

        <div className="cg-grad-stats">
          <div><div className="k">SCORE</div><div className="v accent">{user.score}</div><div className="t">QUALIFIED</div></div>
          <div><div className="k">WIN_RATE</div><div className="v">{user.winRate}%</div></div>
          <div><div className="k">DRAWDOWN</div><div className="v">{user.drawdown}%</div></div>
          <div><div className="k">NET_PnL</div><div className="v pos">+${user.totalPnl.toLocaleString()}</div><div className="t">paper</div></div>
          <div><div className="k">DAY</div><div className="v">{user.day}</div></div>
        </div>

        <div className="cg-grad-eligible">YOU ARE ELIGIBLE TO BORROW</div>
        <div className="cg-grad-credit">
          <div className="cg-grad-credit-row"><span>${user.estLoanUsdc.toLocaleString()}</span><span className="muted">base (score-based)</span></div>
          <div className="cg-grad-credit-row"><span>+ ${user.creditBalance.toLocaleString()}</span><span className="muted">$CREDIT boost</span></div>
          <div className="cg-grad-credit-divider" />
          <div className="cg-grad-credit-total"><span>${(user.estLoanUsdc + user.creditBalance).toLocaleString()}</span><span className="muted">TOTAL CREDIT LINE</span></div>
        </div>
        <div className="cg-grad-terms">25% APR · 25% Revenue Share · 50% LTV</div>
        <div className="cg-grad-note">$CREDIT vests over 6 months from mainnet launch. Your trading history carries to the live protocol.</div>

        <div className="cg-grad-actions">
          <button className="cg-cta-primary" onClick={onDeploy}>DEPLOY ON MAINNET — GET FUNDED →</button>
          <button className="cg-cta-ghost">SHARE YOUR GRADUATION</button>
          <button className="cg-cta-ghost" onClick={onKeepRunning}>KEEP RUNNING</button>
        </div>
      </div>
    </div>
  );
}

/* ===============================================================
   AREA-LEVEL CHROME (Campaign top header)
   =============================================================== */

function CampaignHeader({ user, area, onArea, tab, onTab, onShare, onLogout }) {
  // Soft-launch nav: hide PROTOCOL / VAULTS / LEADERBOARD / DEVELOPER —
  // those areas still render mock data and the live signal from users
  // was that the mix was confusing. The campaign sub-nav stays in full
  // because every page under it now reads real data. DEV_TOOLS sub-tab
  // is hidden too (still shows mock API-key snippets — bring back when
  // the developer surface ships for real).
  return (
    <header className="cg-header">
      <div className="cg-header-left">
        <AgenticsLogo size={28} />
      </div>
      <div className="cg-header-areas">
        {[["campaign","CAMPAIGN"]].map(([k,l]) => (
          <button key={k} className={"cg-area" + (area === k ? " active" : "")} onClick={() => onArea(k)}>[{l}]</button>
        ))}
      </div>
      <div className="cg-header-right">
        <div className="cg-userbar">
          <span className="cg-userbar-rank">{user.rank ? "#" + user.rank : "—"}</span>
          <span className="cg-userbar-name">{user.username}</span>
          <span className="cg-userbar-bal">${user.paperBalance.toLocaleString()}</span>
          <button className="cg-cta-ghost cg-share-top" onClick={onShare}>[SHARE]</button>
          {onLogout && <button className="cg-cta-ghost cg-logout-top" onClick={onLogout}>[LOG_OUT]</button>}
        </div>
      </div>
      {area === "campaign" && (
        <div className="cg-subnav">
          {[["dashboard","DASHBOARD"], ["agent","MY_AGENT"], ["trades","TRADES"], ["credit","CREDIT"], ["leaderboard","LEADERBOARD"], ["referrals","REFERRALS"], ["settings","SETTINGS"]].map(([k,l]) => (
            <button key={k} className={"cg-subnav-tab" + (tab === k ? " active" : "")} onClick={() => onTab(k)}>[{l}]</button>
          ))}
        </div>
      )}
    </header>
  );
}

/* ===============================================================
   EXPORTS
   =============================================================== */
Object.assign(window, {
  CampaignDashboard, CampaignHeader,
  ShareModal, GraduationScreen, CloseTradeConfirm,
  ReasoningFeed, ScorePanel, AgentPanel, LoanProgressStrip,
  EligibilityPanel, CreditPanel, ScoreHistoryPanel,
  EquityChartPanel, ActiveTradesPanel, RecentTradesPanel,
  DecisionJournalPanel, CombinedAgentsPanel, AgentsGridPanel,
});
