// Dashboard · the emotional landing.
// Anchors: You Already Won → Round-Trip Risk → Holdings glance → Next exits.

function Dashboard({ navigate }) {
  const { state, prices } = useStore();
  const cur = state.settings.currency;
  const portfolio = React.useMemo(() => computePortfolio(state, prices), [state, prices]);
  const risk = React.useMemo(() => roundTripRisk(state, prices), [state, prices]);
  const peak = React.useMemo(() => peakValuation(state), [state]);

  // Build "you already won" candidates · top holding by gain pct where capital not yet recovered
  const recoveryCandidates = portfolio.rows
    .map(r => ({ r, rec: capitalRecovery(r, r.price) }))
    .filter(x => x.rec?.possible)
    .sort((a, b) => b.r.pnl - a.r.pnl);

  // Next exits · closest ladder targets not yet reached
  const upcoming = [];
  for (const r of portfolio.rows) {
    const l = state.ladders[r.coinId];
    if (!l) continue;
    for (const t of l.targets) {
      if (r.price < t.price) {
        const distance = (t.price - r.price) / r.price;
        upcoming.push({ row: r, target: t, distance });
      }
    }
  }
  upcoming.sort((a, b) => a.distance - b.distance);
  const nextExits = upcoming.slice(0, 4);

  return (
    <div className="exos-page">
      {/* HERO · dynamic state-driven hook */}
      <DashboardHero
        portfolio={portfolio} prices={prices} state={state}
        risk={risk} peak={peak} navigate={navigate}
      />

      {/* Hero cards · You Already Won + Risk gauge */}
      <div className="exos-grid cols-2" style={{ marginBottom: 14 }}>
        <YouAlreadyWonHero candidates={recoveryCandidates} currency={cur} navigate={navigate} />
        <RoundTripRiskCard risk={risk} portfolio={portfolio} currency={cur} navigate={navigate} />
      </div>

      {/* Behavioral insights · deterministic, free */}
      <BehavioralInsights portfolio={portfolio} state={state} navigate={navigate} />

      {/* Allocation + Next exits */}
      <div className="exos-grid cols-2" style={{ marginBottom: 14 }}>
        <Card>
          <SectionHeader
            kicker="ALLOCATION"
            title="What you're holding"
            right={<Button size="sm" variant="ghost" onClick={() => navigate("portfolio")} iconRight={<Icons.Chevron size={12} />}>Manage</Button>}
          />
          <div style={{ display: "flex", gap: 22, alignItems: "center" }}>
            <Donut data={portfolio.rows.map(r => ({ value: r.value, color: r.meta.color }))} size={150} thickness={20} />
            <div style={{ flex: 1, display: "flex", flexDirection: "column", gap: 9 }}>
              {portfolio.rows.slice(0, 6).map(r => (
                <div key={r.id} style={{ display: "flex", alignItems: "center", gap: 10, fontSize: 13 }}>
                  <span style={{ width: 8, height: 8, borderRadius: 2, background: r.meta.color }} />
                  <span style={{ width: 38, fontWeight: 500 }}>{r.meta.symbol}</span>
                  <span className="mono" style={{ color: "var(--text-mute)" }}>{r.alloc.toFixed(1)}%</span>
                  <span className="exos-spacer" />
                  <span className="mono">{fmtCurrency(r.value, cur, { compact: true })}</span>
                </div>
              ))}
            </div>
          </div>
        </Card>

        <Card>
          <SectionHeader
            kicker="NEXT EXITS"
            title="Closest targets ahead"
            right={<Button size="sm" variant="ghost" onClick={() => navigate("planner")} iconRight={<Icons.Chevron size={12} />}>Plan</Button>}
          />
          {nextExits.length === 0 ? (
            <EmptyState
              icon={<Icons.Planner size={20} />}
              title="No upcoming targets"
              sub="Build an exit ladder for each coin to see your next moves here."
              action={<Button size="sm" variant="primary" onClick={() => navigate("planner")}>Build ladder</Button>}
            />
          ) : (
            <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
              {nextExits.map(({ row, target, distance }, i) => {
                const proceeds = (row.amount * target.sellPercent / 100) * target.price;
                return (
                  <div key={i} className="exos-next-exit">
                    <CoinIcon coinId={row.coinId} size={32} />
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
                        <span style={{ fontWeight: 500 }}>{row.meta.symbol}</span>
                        <span style={{ color: "var(--text-mute)", fontSize: 12 }}>sell {target.sellPercent}% at</span>
                        <span className="mono" style={{ fontWeight: 500 }}>{fmtCurrency(target.price, cur)}</span>
                      </div>
                      <div style={{ marginTop: 4, height: 4, borderRadius: 2, background: "var(--line)", overflow: "hidden" }}>
                        <div style={{ height: "100%", width: `${Math.min(100, (row.price/target.price)*100)}%`, background: "var(--accent)", transition: "width .4s" }} />
                      </div>
                    </div>
                    <div style={{ textAlign: "right", minWidth: 84 }}>
                      <div className="mono" style={{ fontWeight: 500, fontSize: 13 }}>{fmtCurrency(proceeds, cur, { compact: true })}</div>
                      <div className="mono" style={{ fontSize: 11, color: "var(--text-mute)" }}>+{fmtPct(distance*100, { digits: 0 })}</div>
                    </div>
                  </div>
                );
              })}
            </div>
          )}
        </Card>
      </div>

      {/* Freedom number + emotional clarity strip */}
      <FreedomStrip portfolio={portfolio} currency={cur} navigate={navigate} />

      {/* AI Behavioral Mirror · headline AI feature */}
      <BehavioralMirror portfolio={portfolio} state={state} peak={peak} risk={risk} navigate={navigate} />
    </div>
  );
}

function executedNow(state, prices) {
  let net = 0;
  for (const h of state.holdings) {
    const ladder = state.ladders[h.coinId];
    if (!ladder) continue;
    const price = effectivePrice(h.coinId, prices, state.manualPrices);
    const l = computeLadder(h, ladder, price, state.settings.taxRate);
    // assume any target where current price >= target is "executable"
    for (const t of l.targets) {
      if (t.reached) net += t.net;
    }
  }
  return net;
}

// ---------- Dynamic headline generator (deterministic)
function dashboardHeadline(portfolio, state, peak, risk) {
  const cur = state.settings.currency;
  const pnlPct = portfolio.pnlPct || 0;
  const total = portfolio.total;

  if (portfolio.rows.length === 0) {
    return { lead: "Add your first coin.", hook: "Then build the plan that turns paper gains into protected capital." };
  }

  // plan coverage
  let plannedValue = 0;
  for (const r of portfolio.rows) {
    const l = state.ladders[r.coinId];
    const sumPct = l?.targets?.reduce((a,t)=>a+t.sellPercent,0) ?? 0;
    plannedValue += r.value * Math.min(sumPct, 100) / 100;
  }
  const coverage = total > 0 ? (plannedValue / total) * 100 : 0;
  const peakMultiple = total > 0 ? peak / total : 0;

  // Top gainer
  const topGain = [...portfolio.rows].sort((a,b) => b.pnlPct - a.pnlPct)[0];

  // Cases · most specific first
  if (pnlPct > 80 && coverage < 25) {
    return {
      lead: `You've turned ${fmtCurrency(portfolio.totalCost, cur, { compact: true })} into ${fmtCurrency(total, cur, { compact: true })}.`,
      hook: "The market doesn't owe you keeping it."
    };
  }
  if (pnlPct > 200) {
    return {
      lead: `Up ${Math.round(pnlPct)}% on cost basis.`,
      hook: coverage > 50 ? "Your plan reflects that you noticed." : "Cycles end. Plans close them."
    };
  }
  if (pnlPct < -15) {
    return {
      lead: `Down ${Math.round(Math.abs(pnlPct))}% from cost.`,
      hook: "The plan you build now matters more than the price today."
    };
  }
  if (risk.score >= 70) {
    const lost = total * 0.5;
    return {
      lead: `Round-trip risk is high.`,
      hook: `${fmtCurrency(lost, cur, { compact: true })} sits on the line if markets retrace 50%.`
    };
  }
  if (coverage < 20 && portfolio.rows.length >= 2) {
    return {
      lead: `${Math.round(100 - coverage)}% of your value rides without an exit.`,
      hook: "Decide before the moment · not in it."
    };
  }
  if (peakMultiple > 4) {
    return {
      lead: `Your projected peak is ${peakMultiple.toFixed(1)}× from here.`,
      hook: "Believe it, but build the exit while you can think straight."
    };
  }
  if (coverage >= 50 && pnlPct >= 50) {
    return {
      lead: `Up ${Math.round(pnlPct)}% with ${Math.round(coverage)}% of value protected.`,
      hook: "This is what a real exit plan looks like."
    };
  }
  if (topGain && topGain.pnlPct > 100) {
    return {
      lead: `${topGain.meta.symbol} is up ${Math.round(topGain.pnlPct)}% from your average.`,
      hook: `Your plan covers ${Math.round(coverage)}% of total value.`
    };
  }
  // default
  return {
    lead: `Portfolio ${pnlPct >= 0 ? "up" : "down"} ${Math.round(Math.abs(pnlPct))}% from cost.`,
    hook: "The plan does the deciding so you don't have to."
  };
}

// ---------- Dashboard Hero
function DashboardHero({ portfolio, prices, state, risk, peak, navigate }) {
  const cur = state.settings.currency;
  const headline = React.useMemo(() => dashboardHeadline(portfolio, state, peak, risk),
    [portfolio.total, portfolio.totalCost, peak, risk.score, state.ladders]);

  const topMovers = React.useMemo(() => {
    return [...portfolio.rows]
      .map(r => ({ ...r, change24h: prices[r.coinId]?.change24h ?? 0 }))
      .sort((a, b) => Math.abs(b.change24h) - Math.abs(a.change24h))
      .slice(0, 5);
  }, [portfolio.rows, prices]);

  const [clock, setClock] = React.useState(() => new Date());
  React.useEffect(() => {
    const id = setInterval(() => setClock(new Date()), 1000);
    return () => clearInterval(id);
  }, []);

  return (
    <div className="exos-dash-hero">
      <div className="exos-dash-hero-grain" />
      <div className="exos-dash-hero-row">
        <div className="exos-dash-hero-left">
          <div className="exos-kicker mono">DASHBOARD</div>
          <div className="exos-dash-hero-headline">
            <span className="exos-dash-hero-lead">{headline.lead}</span>
            <span className="exos-dash-hero-hook serif">{headline.hook}</span>
          </div>
        </div>
        <div className="exos-dash-hero-actions">
          <Button icon={<Icons.Refresh size={14} />} onClick={() => navigate("portfolio")}>Holdings</Button>
          <Button variant="primary" icon={<Icons.Plus size={14} />} onClick={() => navigate("planner")}>Build plan</Button>
        </div>
      </div>

      <div className="exos-dash-hero-figure">
        <div className="exos-dash-hero-num">
          <Ticker value={portfolio.total} format={v => fmtCurrency(v, cur, { compact: true })} flash={false} />
        </div>
        <div className="exos-dash-hero-deltas">
          <Pill tone={portfolio.pnl >= 0 ? "gain" : "loss"} dot>
            {fmtPct(portfolio.pnlPct)} all-time
          </Pill>
          <div className="exos-dash-hero-delta-sub mono">
            <span>{fmtCurrency(portfolio.pnl, cur, { compact: true })}</span>
            <span className="exos-dash-hero-delta-sep">·</span>
            <span style={{ color: "var(--text-mute)" }}>on {fmtCurrency(portfolio.totalCost, cur, { compact: true })} cost</span>
          </div>
        </div>
        <div className="exos-dash-hero-peakline">
          <span className="exos-label">CYCLE PEAK TARGET</span>
          <div style={{ display: "flex", alignItems: "baseline", gap: 6 }}>
            <span className="mono" style={{ fontSize: 18, fontWeight: 500 }}>{fmtCurrency(peak, cur, { compact: true })}</span>
            <span className="mono" style={{ fontSize: 12, color: "var(--accent)" }}>
              {peak > 0 && portfolio.total > 0 ? `${(peak / portfolio.total).toFixed(2)}× from here` : ""}
            </span>
          </div>
        </div>
      </div>

      <div className="exos-dash-pulse">
        <span className="exos-pulse-label mono">
          <span className="exos-pulse-dot" />
          LIVE · {clock.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" })}
        </span>
        <div className="exos-pulse-items">
          {topMovers.map(r => (
            <span key={r.id} className="exos-pulse-item">
              <span className="exos-pulse-coin-dot" style={{ background: r.meta.color }} />
              <span className="mono" style={{ fontWeight: 500 }}>{r.meta.symbol}</span>
              <Sparkline values={fakeSpark(r.coinId, r.change24h)} width={36} height={14} positive={r.change24h >= 0} />
              <span className="mono" style={{ color: r.change24h >= 0 ? "var(--gain)" : "var(--loss)", fontSize: 11 }}>
                {fmtPct(r.change24h)}
              </span>
            </span>
          ))}
        </div>
      </div>
    </div>
  );
}

// stable pseudo-spark for visual interest (real data would come from CG)
function fakeSpark(coinId, change24h) {
  let h = 0;
  for (let i = 0; i < coinId.length; i++) h = ((h << 5) - h) + coinId.charCodeAt(i);
  const out = [];
  for (let i = 0; i < 18; i++) {
    const t = i / 17;
    const noise = (Math.sin((h + i * 11) * 0.7) + 1) / 2;
    const trend = change24h / 100;
    out.push(0.5 + (trend * t) + (noise - 0.5) * 0.06);
  }
  return out;
}

function riskColor(score) {
  if (score >= 70) return "var(--loss)";
  if (score >= 40) return "var(--warn)";
  return "var(--gain)";
}

// ---------- You Already Won hero
function YouAlreadyWonHero({ candidates, currency, navigate }) {
  if (!candidates.length) {
    return (
      <Card>
        <SectionHeader kicker="YOU ALREADY WON" title="Recovery moment" />
        <div className="exos-yaw-empty">
          <Icons.Trophy size={28} />
          <p>No holdings have recovered their cost yet. Stay patient · your moment is coming.</p>
        </div>
      </Card>
    );
  }
  const [idx, setIdx] = React.useState(0);
  const { r, rec } = candidates[idx % candidates.length];
  return (
    <Card className="exos-yaw">
      <SectionHeader
        kicker="YOU ALREADY WON"
        title="Your recovery moment"
        right={candidates.length > 1 && (
          <div style={{ display: "flex", gap: 4 }}>
            {candidates.slice(0,5).map((_, i) => (
              <button key={i} className="exos-dot-btn" data-on={i === idx % candidates.length}
                onClick={() => setIdx(i)} />
            ))}
          </div>
        )}
      />
      <div className="exos-yaw-body">
        <div className="exos-yaw-stat">
          <CoinIcon coinId={r.coinId} size={36} />
          <div>
            <div style={{ display: "flex", gap: 6, alignItems: "baseline" }}>
              <span style={{ fontSize: 28, fontWeight: 500, letterSpacing: "-0.02em" }} className="mono">{rec.percentNeeded.toFixed(1)}%</span>
              <span style={{ color: "var(--text-mute)", fontSize: 13 }}>of your {r.meta.symbol}</span>
            </div>
            <div style={{ fontSize: 13, color: "var(--text-mute)", marginTop: 2 }}>
              sold at <span className="mono" style={{ color: "var(--text)" }}>{fmtCurrency(r.price, currency)}</span> recovers your full {fmtCurrency(r.cost, currency, { compact: true })} cost basis.
            </div>
          </div>
        </div>

        <div className="exos-yaw-line" />

        <div className="exos-yaw-after">
          <div className="exos-label">After recovery, you still hold</div>
          <div style={{ marginTop: 6, display: "flex", gap: 14, alignItems: "baseline" }}>
            <span className="mono" style={{ fontSize: 22, fontWeight: 500 }}>{fmtNum(rec.remainingCoins, 2)} {r.meta.symbol}</span>
            <span className="mono" style={{ color: "var(--text-mute)" }}>≈ {fmtCurrency(rec.remainingCoins * r.price, currency, { compact: true })}</span>
          </div>
          <div className="serif" style={{ marginTop: 14, fontSize: 17, color: "var(--text-mute)", lineHeight: 1.3, maxWidth: 360 }}>
            “Everything from here is a free ride.”
          </div>
        </div>
      </div>
      <div className="exos-yaw-foot">
        <Button size="sm" icon={<Icons.Sparkle size={13} />} variant="primary" onClick={() => navigate("planner")}>Lock this into the plan</Button>
        <Button size="sm" variant="ghost" onClick={() => setIdx(i => i + 1)}>Show another</Button>
      </div>
    </Card>
  );
}

// ---------- Round trip risk card
function RoundTripRiskCard({ risk, portfolio, currency, navigate }) {
  // Simulate -50% crash
  const crash = portfolio.total * 0.5;
  const lost = portfolio.total - crash;
  return (
    <Card>
      <SectionHeader
        kicker="ROUND-TRIP RISK"
        title="If markets reverse tomorrow"
        right={<Pill tone={risk.score >= 70 ? "loss" : risk.score >= 40 ? "warn" : "gain"} dot>{risk.label}</Pill>}
      />
      <div className="exos-rt-body">
        <RiskGauge score={risk.score} />
        <div style={{ flex: 1 }}>
          <div className="exos-rt-stat-row">
            <div>
              <div className="exos-label">Today</div>
              <div className="mono" style={{ fontSize: 20, fontWeight: 500 }}>{fmtCurrency(portfolio.total, currency, { compact: true })}</div>
            </div>
            <div style={{ color: "var(--text-dim)" }}>→</div>
            <div>
              <div className="exos-label">After 50% drop</div>
              <div className="mono" style={{ fontSize: 20, fontWeight: 500, color: "var(--loss)" }}>{fmtCurrency(crash, currency, { compact: true })}</div>
            </div>
          </div>
          <div style={{ marginTop: 12, fontSize: 13, color: "var(--text-mute)" }}>
            Unrealized wealth on the line: <span className="mono" style={{ color: "var(--text)" }}>{fmtCurrency(lost, currency, { compact: true })}</span>
          </div>
          {risk.reasons.length > 0 && (
            <ul className="exos-rt-reasons">
              {risk.reasons.slice(0, 2).map((rsn, i) => (
                <li key={i}><Icons.Warning size={11} /> <span>{rsn}</span></li>
              ))}
            </ul>
          )}
        </div>
      </div>
      <div className="exos-yaw-foot">
        <Button size="sm" onClick={() => navigate("regret")} iconRight={<Icons.Chevron size={12} />}>See the full simulation</Button>
      </div>
    </Card>
  );
}

function RiskGauge({ score }) {
  const angle = -90 + (score / 100) * 180;
  const color = score >= 70 ? "var(--loss)" : score >= 40 ? "var(--warn)" : "var(--gain)";
  return (
    <div className="exos-gauge">
      <svg width="120" height="76" viewBox="0 0 120 76">
        <path d="M10 70 A50 50 0 0 1 110 70" stroke="var(--line)" strokeWidth="6" fill="none" strokeLinecap="round" />
        <path d="M10 70 A50 50 0 0 1 110 70" stroke={color} strokeWidth="6" fill="none" strokeLinecap="round"
          strokeDasharray={`${(score/100)*157} 157`} />
        <g style={{ transformOrigin: "60px 70px", transform: `rotate(${angle}deg)`, transition: "transform .6s cubic-bezier(.16,1,.3,1)" }}>
          <line x1="60" y1="70" x2="60" y2="26" stroke="var(--text)" strokeWidth="2" strokeLinecap="round" />
          <circle cx="60" cy="70" r="4" fill="var(--text)" />
        </g>
      </svg>
      <div className="mono" style={{ fontSize: 11, color: "var(--text-mute)", textAlign: "center", marginTop: -4 }}>{score}/100</div>
    </div>
  );
}

// ---------- Behavioral Insights (deterministic, free)
function BehavioralInsights({ portfolio, state, navigate }) {
  if (portfolio.rows.length === 0) return null;
  const cur = state.settings.currency;

  // 1. Plan coverage (value-weighted)
  let plannedValue = 0, totalValue = 0;
  for (const r of portfolio.rows) {
    const l = state.ladders[r.coinId];
    const sumPct = l?.targets?.reduce((a,t)=>a+t.sellPercent,0) ?? 0;
    plannedValue += r.value * (Math.min(sumPct, 100) / 100);
    totalValue += r.value;
  }
  const coveragePct = totalValue > 0 ? (plannedValue / totalValue) * 100 : 0;
  const uncoveredPct = Math.max(0, 100 - coveragePct);

  // 2. Concentration at projected peak
  let maxPeakValue = 0, maxPeakCoin = null, totalPeak = 0;
  for (const r of portfolio.rows) {
    const p = state.scenarios.bullPeaks[r.coinId] ?? r.price * 2;
    const pv = r.amount * p;
    totalPeak += pv;
    if (pv > maxPeakValue) { maxPeakValue = pv; maxPeakCoin = r; }
  }
  const concentrationPct = totalPeak > 0 ? (maxPeakValue / totalPeak) * 100 : 0;

  // 3. House money · holdings whose plan reaches full cost recovery
  let recovered = 0;
  for (const r of portfolio.rows) {
    const l = state.ladders[r.coinId];
    if (!l?.targets?.length) continue;
    const cost = r.amount * r.avgBuyPrice;
    let cumNet = 0;
    for (const t of l.targets) {
      const coinsSold = r.amount * (t.sellPercent / 100);
      const proceeds = coinsSold * t.price;
      const gain = proceeds - coinsSold * r.avgBuyPrice;
      const tax = Math.max(0, gain) * state.settings.taxRate;
      cumNet += proceeds - tax;
      if (cumNet >= cost) { recovered++; break; }
    }
  }
  const total = portfolio.rows.length;

  return (
    <Card style={{ marginBottom: 14 }}>
      <SectionHeader
        kicker="BEHAVIORAL INSIGHTS"
        title="What your plan reveals"
        sub="Deterministic · no AI required"
        right={<Pill>auto</Pill>}
      />
      <div className="exos-insights-grid">
        <InsightCard
          tone={uncoveredPct > 70 ? "warn" : uncoveredPct > 40 ? "default" : "good"}
          metric={`${uncoveredPct.toFixed(0)}%`}
          metricSub="of value exposed"
          label="Riding without a plan"
          body={uncoveredPct < 5
            ? "Almost everything has an exit · you've removed the in-the-moment decision."
            : `Your ladders cover ${coveragePct.toFixed(0)}% of current value. The rest rides naked into whatever the market does next.`
          }
          actionLabel="Build ladders →"
          onClick={() => navigate("planner")}
        />
        <InsightCard
          tone={concentrationPct > 60 ? "warn" : concentrationPct > 40 ? "default" : "good"}
          metric={maxPeakCoin?.meta.symbol ?? "·"}
          metricSub={`${concentrationPct.toFixed(0)}% of peak`}
          label="Largest position at cycle peak"
          body={concentrationPct > 60
            ? `Your projected peak rests heavily on ${maxPeakCoin?.meta.symbol}. Does your plan reflect that level of conviction?`
            : `Reasonable spread. ${maxPeakCoin?.meta.symbol} won't make-or-break your cycle outcome.`
          }
          actionLabel="View planner →"
          onClick={() => navigate("planner")}
        />
        <InsightCard
          tone={recovered === total ? "good" : recovered > 0 ? "default" : "warn"}
          metric={`${recovered}/${total}`}
          metricSub="coins"
          label="Recover cost in plan"
          body={recovered === total
            ? "Every holding's ladder lifts you above cost basis. Everything after target #N is house money."
            : recovered === 0
              ? "No holding's current plan fully recovers what you put in. Add targets that bank capital first."
              : `${recovered} of ${total} ladders fully recover cost. Strengthen the rest before bigger targets.`
          }
          actionLabel="Strengthen plan →"
          onClick={() => navigate("planner")}
        />
      </div>
    </Card>
  );
}

function InsightCard({ tone = "default", metric, metricSub, label, body, actionLabel, onClick }) {
  const tones = { good: "var(--gain)", warn: "var(--warn)", default: "var(--text)" };
  const borderTone = { good: "oklch(from var(--gain) l c h / 0.28)", warn: "oklch(from var(--warn) l c h / 0.3)", default: "var(--line)" };
  return (
    <div className="exos-insight" style={{ borderColor: borderTone[tone] }} onClick={onClick}>
      <div className="exos-insight-top">
        <div className="exos-insight-metric mono" style={{ color: tones[tone] }}>{metric}</div>
        {metricSub && <div className="exos-insight-metric-sub">{metricSub}</div>}
      </div>
      <div className="exos-insight-label">{label}</div>
      <div className="exos-insight-body">{body}</div>
      {actionLabel && <div className="exos-insight-action">{actionLabel}</div>}
    </div>
  );
}

// ---------- Behavioral Mirror · deterministic first, AI as optional rewrite
function deterministicMirror({ portfolio, state, peak, risk }) {
  if (portfolio.rows.length === 0) {
    return { posture: "empty", paragraph: "No holdings yet. Add a coin and build a ladder before the mirror has anything to reflect." };
  }

  // ---- compute signals
  let plannedValue = 0;
  for (const r of portfolio.rows) {
    const l = state.ladders[r.coinId];
    const sumPct = l?.targets?.reduce((a,t)=>a+t.sellPercent,0) ?? 0;
    plannedValue += r.value * Math.min(sumPct, 100) / 100;
  }
  const coverage = portfolio.total > 0 ? (plannedValue / portfolio.total) * 100 : 0;

  let maxPeakValue = 0, maxCoin = null, totalPeak = 0;
  for (const r of portfolio.rows) {
    const p = state.scenarios.bullPeaks[r.coinId] ?? r.price * 2;
    const pv = r.amount * p;
    totalPeak += pv;
    if (pv > maxPeakValue) { maxPeakValue = pv; maxCoin = r; }
  }
  const concentration = totalPeak > 0 ? (maxPeakValue / totalPeak) * 100 : 0;

  let recovered = 0, totalWithPlan = 0;
  for (const r of portfolio.rows) {
    const l = state.ladders[r.coinId];
    if (!l?.targets?.length) continue;
    totalWithPlan++;
    const cost = r.amount * r.avgBuyPrice;
    let cumNet = 0;
    for (const t of l.targets) {
      const coinsSold = r.amount * (t.sellPercent / 100);
      const proceeds = coinsSold * t.price;
      const gain = proceeds - coinsSold * r.avgBuyPrice;
      const tax = Math.max(0, gain) * state.settings.taxRate;
      cumNet += proceeds - tax;
      if (cumNet >= cost) { recovered++; break; }
    }
  }

  const pnl = portfolio.pnlPct ?? 0;

  // ---- pick posture
  let posture;
  if (totalWithPlan === 0) posture = "no-plan";
  else if (coverage < 25 && pnl > 40) posture = "greed";
  else if (coverage > 88) posture = "fear";
  else if (totalWithPlan > 0 && recovered < Math.ceil(totalWithPlan * 0.4)) posture = "denial";
  else if (concentration > 60 && coverage < 55) posture = "lopsided-conviction";
  else if (coverage >= 40 && coverage <= 80 && recovered >= Math.floor(totalWithPlan * 0.6)) posture = "balanced";
  else posture = "in-progress";

  // ---- templates (deterministic, observational, no advice)
  const sym = maxCoin?.meta.symbol || "your largest position";
  const T = {
    "no-plan": `Holdings exist, ladders don't. Right now ${portfolio.rows.length} positions ride into whatever the market does next · no exits, no protected capital, no resolved decisions. The mirror has nothing to read because there's nothing pre-committed. That's not greed yet, that's pre-decision.`,

    "greed": `You're up ${Math.round(pnl)}% and only ${Math.round(coverage)}% of your value has a planned exit. The posture reads as "more is more" · common at this point in a cycle, and the exact behavior that turns paper gains into round trips. The math says: protect first, ride second.`,

    "fear": `Your ladders sell ${Math.round(coverage)}% of value · almost the entire position is staged to exit. That's defensive, not wrong, but it leaves limited upside if the cycle has more to give. Ask whether you're protecting capital or surrendering it.`,

    "denial": `Of ${totalWithPlan} positions with a plan, only ${recovered} fully recover your cost basis at completion. The pattern: targets set for upside without securing what you put in first. That's "this time is different" thinking, dressed up as a strategy.`,

    "lopsided-conviction": `${sym} is ${Math.round(concentration)}% of your projected cycle peak · a high-conviction concentration. But only ${Math.round(coverage)}% of value has exits planned. Strong belief without a protection layer reads as bias, not conviction.`,

    "balanced": `Plan covers ${Math.round(coverage)}% of value, ${recovered}/${totalWithPlan} ladders recover full cost basis, ${sym} sits at ${Math.round(concentration)}% of projected peak. This reads as deliberate · neither greedy nor fearful. The hard part now isn't planning, it's executing when targets actually hit.`,

    "in-progress": `Plan covers ${Math.round(coverage)}% of value with ${recovered}/${totalWithPlan} ladders reaching cost recovery. The structure is forming but not resolved. Add or tighten exits on the positions still riding naked, especially ${sym}.`,

    "empty": "No holdings yet.",
  };

  return { posture, paragraph: T[posture] || T["in-progress"], coverage, concentration, recovered, totalWithPlan };
}

function BehavioralMirror({ portfolio, state, peak, risk, navigate }) {
  const det = React.useMemo(() => deterministicMirror({ portfolio, state, peak, risk }),
    [portfolio, state.ladders, state.scenarios.bullPeaks, state.settings.taxRate, portfolio.total, portfolio.totalCost]);

  const [aiText, setAiText] = React.useState(null);
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState(null);

  const aiOk = aiAvailable(state);

  async function rewriteWithAi() {
    setLoading(true); setError(null);
    try {
      const summary = {
        currency: state.settings.currency,
        totalValue: Math.round(portfolio.total),
        cost: Math.round(portfolio.totalCost),
        pnlPct: Math.round(portfolio.pnlPct),
        peakIfHit: Math.round(peak),
        riskLabel: risk.label,
        posture: det.posture,
        coverage: Math.round(det.coverage),
        concentration: Math.round(det.concentration),
        recoveredCount: det.recovered,
        plansCount: det.totalWithPlan,
        holdings: portfolio.rows.map(r => ({
          symbol: r.meta.symbol, alloc: Math.round(r.alloc), pnlPct: Math.round(r.pnlPct),
          planCoverage: state.ladders[r.coinId]?.targets?.reduce((a,t)=>a+t.sellPercent,0) ?? 0,
        })),
      };
      const prompt = `Behavioral finance mirror · calm, observant, no advice. The user's plan has been classified as "${det.posture}". Given the JSON summary, write ONE paragraph (max 90 words) describing what their plan reveals about their behavioral posture. Quote one number. Use second person. No emojis, no all-caps, no disclaimers, no em dashes (—). Use periods and commas instead.\n\n${JSON.stringify(summary)}`;
      const { text } = await aiComplete(prompt, state);
      setAiText(text);
    } catch (e) {
      setError(e?.message || "AI rewrite unavailable.");
    } finally { setLoading(false); }
  }

  const labels = {
    greed: "Greed posture", fear: "Defensive posture", denial: "Denial posture",
    "lopsided-conviction": "Conviction without protection", balanced: "Balanced posture",
    "in-progress": "In-progress posture", "no-plan": "Pre-decision", empty: "·",
  };

  const tone = (det.posture === "balanced") ? "good"
             : (det.posture === "greed" || det.posture === "denial" || det.posture === "fear") ? "warn"
             : "default";

  return (
    <Card className={`exos-mirror-card tone-${tone}`}>
      <div className="exos-mirror-head">
        <div style={{ flex: 1, minWidth: 0 }}>
          <div className="exos-kicker mono">BEHAVIORAL MIRROR</div>
          <div className="exos-mirror-title">{labels[det.posture] || "Your plan reveals"}</div>
          <div className="exos-mirror-sub">
            What your exit plan implies about how you're holding. Calculated locally · no AI required.
          </div>
        </div>
        {aiOk && (
          <div style={{ display: "flex", flexDirection: "column", gap: 6, alignItems: "flex-end" }}>
            <Button variant="primary" icon={<Icons.Sparkle size={13} />} onClick={rewriteWithAi} disabled={loading}>
              {loading ? "Rewriting…" : aiText ? "Rewrite again" : "AI rewrite"}
            </Button>
            <div className="exos-mirror-usage mono">optional</div>
          </div>
        )}
      </div>

      <div className="exos-mirror-text">
        <div className="exos-mirror-quote">“</div>
        <p>{aiText || det.paragraph}</p>
      </div>

      {error && <div className="exos-mirror-error">{error}</div>}

      {!aiOk && (
        <div className="exos-mirror-aihint">
          <Icons.Sparkle size={12} />
          <span>Want a more nuanced, AI-written version? Add an Anthropic API key in Settings.</span>
          <Button size="sm" variant="ghost" onClick={() => navigate("settings")} iconRight={<Icons.Chevron size={11} />}>Open Settings</Button>
        </div>
      )}

      {aiText && (
        <div className="exos-mirror-detail">
          <span className="exos-label">deterministic baseline</span>
          <span>{det.paragraph}</span>
        </div>
      )}
    </Card>
  );
}

// ---------- Freedom strip
function FreedomStrip({ portfolio, currency, navigate }) {
  const items = [
    { label: "Years of $4k/mo living", value: portfolio.total / (4000 * 12), unit: "yrs" },
    { label: "Home down payment (20% on $500k)", value: portfolio.total / 100000, unit: "×" },
    { label: "Debt freedom (avg $90k)", value: portfolio.total / 90000, unit: "×" },
    { label: "Emergency fund (12mo)", value: portfolio.total / 60000, unit: "×" },
  ];
  return (
    <Card className="exos-freedom">
      <div className="exos-freedom-head">
        <div>
          <div className="exos-kicker mono">FREEDOM NUMBER</div>
          <div className="exos-freedom-title">What your portfolio buys you in real life</div>
        </div>
        <Button size="sm" variant="ghost" onClick={() => navigate("simulator")} iconRight={<Icons.Chevron size={12} />}>Simulate scenarios</Button>
      </div>
      <div className="exos-freedom-grid">
        {items.map((it, i) => (
          <div key={i} className="exos-freedom-item">
            <div className="mono exos-freedom-value">{it.value.toFixed(1)}<span style={{ fontSize: 13, color: "var(--text-mute)", marginLeft: 4 }}>{it.unit}</span></div>
            <div style={{ fontSize: 12, color: "var(--text-mute)" }}>{it.label}</div>
          </div>
        ))}
      </div>
    </Card>
  );
}

const DASH_STYLES = `
.exos-yaw { display: flex; flex-direction: column; }
.exos-yaw-body { display: flex; gap: 22px; align-items: stretch; padding: 6px 0 14px; }
.exos-yaw-body > * { min-width: 0; }
.exos-rt-stat-row > * { min-width: 0; }
.exos-rt-stat-row .mono { white-space: nowrap; }
.exos-yaw-line { width: 1px; background: var(--line); }
.exos-yaw-stat { display: flex; gap: 14px; align-items: flex-start; flex: 1; min-width: 0; }
.exos-yaw-after { flex: 1; min-width: 0; }
.exos-yaw-foot { display: flex; gap: 8px; margin-top: auto; padding-top: 8px; border-top: 1px solid var(--line); padding-top: 14px; }
.exos-yaw-empty { display: flex; flex-direction: column; align-items: center; padding: 30px 20px; text-align: center; color: var(--text-mute); gap: 10px; }
.exos-yaw-empty p { margin: 0; max-width: 320px; font-size: 13px; }
.exos-dot-btn { width: 6px; height: 6px; border-radius: 50%; background: var(--line); border: 0; cursor: pointer; padding: 0; transition: background .15s; }
.exos-dot-btn[data-on="true"] { background: var(--accent); }

.exos-rt-body { display: flex; gap: 22px; align-items: center; padding: 8px 0 12px; }
.exos-gauge { display: flex; flex-direction: column; gap: 4px; }
.exos-rt-stat-row { display: flex; gap: 18px; align-items: center; }
.exos-rt-reasons { margin: 12px 0 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--text-mute); }
.exos-rt-reasons li { display: flex; gap: 6px; align-items: center; }
.exos-rt-reasons svg { color: var(--warn); flex-shrink: 0; }

.exos-next-exit { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 8px; background: var(--bg-elev-2); transition: background .12s; }
.exos-next-exit:hover { background: var(--bg-hover); }

.exos-freedom-head { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: 18px; }
.exos-freedom-title { font-size: 18px; font-weight: 500; letter-spacing: -0.01em; margin-top: 4px; }
.exos-freedom-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0; border-top: 1px solid var(--line); }
.exos-freedom-item { padding: 16px 14px; border-right: 1px solid var(--line); }
.exos-freedom-item:last-child { border-right: 0; }
.exos-freedom-value { font-size: 26px; font-weight: 500; letter-spacing: -0.02em; }

/* ===== Dashboard Hero ===== */
.exos-dash-hero {
  position: relative;
  background: linear-gradient(135deg, var(--bg-elev) 0%, var(--bg-elev-2) 100%);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 26px 28px 22px;
  margin-bottom: 14px;
  overflow: hidden;
  box-shadow: var(--shadow-md);
}
.exos-dash-hero::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 2px;
  background: linear-gradient(90deg, var(--accent), transparent 60%);
}
.exos-dash-hero-grain {
  position: absolute; inset: 0;
  background-image: radial-gradient(oklch(from var(--accent) l c h / 0.06) 1px, transparent 1px);
  background-size: 12px 12px;
  opacity: 0.4;
  pointer-events: none;
  mask: linear-gradient(135deg, black 0%, transparent 65%);
  -webkit-mask: linear-gradient(135deg, black 0%, transparent 65%);
}
.exos-dash-hero-row { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; position: relative; }
.exos-dash-hero-left { flex: 1; min-width: 0; }
.exos-dash-hero-actions { display: flex; gap: 8px; flex-shrink: 0; }

.exos-dash-hero-headline {
  display: flex; flex-direction: column; gap: 4px;
  margin-top: 8px;
  max-width: 720px;
}
.exos-dash-hero-lead {
  font-size: 22px;
  font-weight: 500;
  letter-spacing: -0.018em;
  color: var(--text);
  line-height: 1.25;
}
.exos-dash-hero-hook {
  font-size: 22px;
  font-style: italic;
  font-family: "Instrument Serif", "Iowan Old Style", Georgia, serif;
  color: var(--text-mute);
  line-height: 1.25;
  letter-spacing: 0.005em;
}

.exos-dash-hero-figure {
  display: flex; align-items: flex-end; gap: 24px; flex-wrap: wrap;
  margin-top: 22px;
  position: relative;
  padding-top: 18px;
  border-top: 1px solid var(--line);
}
.exos-dash-hero-num {
  font-family: "Geist Mono", monospace;
  font-size: 56px;
  font-weight: 500;
  letter-spacing: -0.03em;
  line-height: 0.95;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.exos-dash-hero-deltas {
  display: flex; flex-direction: column; gap: 6px;
  margin-bottom: 6px;
}
.exos-dash-hero-delta-sub {
  display: flex; gap: 6px; align-items: center;
  font-size: 12px; color: var(--text);
}
.exos-dash-hero-delta-sep { color: var(--line-strong); }
.exos-dash-hero-peakline {
  margin-left: auto;
  text-align: right;
  margin-bottom: 4px;
}
.exos-dash-hero-peakline .exos-label { display: block; margin-bottom: 4px; }

.exos-dash-pulse {
  display: flex; align-items: center; gap: 16px;
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
  overflow-x: auto;
  position: relative;
}
.exos-pulse-label {
  font-size: 10px;
  letter-spacing: 0.14em;
  color: var(--text-mute);
  display: flex; align-items: center; gap: 6px;
  flex-shrink: 0;
}
.exos-pulse-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--gain);
  animation: pulse-dot 1.8s ease-in-out infinite;
}
@keyframes pulse-dot {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.4; transform: scale(0.85); }
}
.exos-pulse-items { display: flex; gap: 14px; flex-wrap: wrap; }
.exos-pulse-item {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 10px;
  background: var(--bg-elev);
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 12px;
  white-space: nowrap;
}
.exos-pulse-coin-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }

@media (max-width: 760px) {
  .exos-dash-hero { padding: 20px 18px; }
  .exos-dash-hero-num { font-size: 38px; }
  .exos-dash-hero-lead, .exos-dash-hero-hook { font-size: 18px; }
  .exos-dash-hero-peakline { margin-left: 0; text-align: left; width: 100%; }
}

/* ===== Behavioral Insights ===== */
.exos-insights-grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;
}
.exos-insight {
  padding: 16px 16px 14px;
  background: var(--bg-elev-2); border: 1px solid var(--line); border-radius: 12px;
  display: flex; flex-direction: column; gap: 8px;
  cursor: pointer; transition: transform .15s, box-shadow .15s, background .15s;
  min-width: 0;
}
.exos-insight:hover { background: var(--bg-hover); transform: translateY(-1px); box-shadow: var(--shadow-md); }
.exos-insight-top { display: flex; align-items: baseline; gap: 8px; }
.exos-insight-metric { font-size: 32px; font-weight: 500; letter-spacing: -0.03em; line-height: 1; }
.exos-insight-metric-sub { font-size: 11px; color: var(--text-mute); }
.exos-insight-label { font-size: 13px; font-weight: 500; }
.exos-insight-body { font-size: 12.5px; color: var(--text-mute); line-height: 1.5; flex: 1; }
.exos-insight-action { font-size: 11px; font-weight: 500; color: var(--accent); margin-top: auto; padding-top: 6px; }
@media (max-width: 880px) {
  .exos-insights-grid { grid-template-columns: 1fr; }
}

/* ===== Behavioral Mirror (AI) ===== */
.exos-mirror-card {
  background: linear-gradient(135deg, var(--bg-elev) 0%, var(--bg-elev-2) 100%);
  position: relative; overflow: hidden;
  margin-top: 14px;
}
.exos-mirror-card::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 2px;
  background: linear-gradient(90deg, var(--accent), transparent 80%);
}
.exos-mirror-card.off { opacity: 0.85; }
.exos-mirror-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; flex-wrap: wrap; }
.exos-mirror-title { font-size: 20px; font-weight: 500; letter-spacing: -0.015em; margin-top: 6px; }
.exos-mirror-sub { font-size: 13px; color: var(--text-mute); margin-top: 6px; max-width: 580px; line-height: 1.55; }
.exos-mirror-usage { font-size: 10px; color: var(--text-dim); letter-spacing: 0.06em; }
.exos-mirror-text {
  margin-top: 16px; padding: 18px 22px;
  background: var(--bg-elev-2); border: 1px solid var(--accent-line);
  border-radius: 12px; position: relative;
  display: flex; gap: 14px; align-items: flex-start;
}
.exos-mirror-quote {
  font-family: "Instrument Serif", serif;
  font-size: 56px; line-height: 0.6; color: var(--accent);
  margin-top: 14px; flex-shrink: 0;
}
.exos-mirror-text p {
  margin: 0; font-family: "Instrument Serif", "Iowan Old Style", Georgia, serif;
  font-size: 19px; line-height: 1.45; color: var(--text); font-style: italic;
}
.exos-mirror-error {
  margin-top: 14px; padding: 10px 14px;
  background: var(--bg-elev-2); border: 1px solid var(--line);
  border-radius: 8px; font-size: 12.5px; color: var(--text-mute);
}
.exos-mirror-placeholder {
  margin-top: 16px; padding: 18px 22px;
  border: 1px dashed var(--line); border-radius: 12px;
  color: var(--text-mute); font-size: 16px; line-height: 1.45; max-width: 720px;
}
.exos-mirror-aihint {
  margin-top: 14px; padding: 10px 14px;
  background: var(--bg-elev-2); border: 1px solid var(--line);
  border-radius: 8px;
  display: flex; align-items: center; gap: 8px;
  font-size: 12.5px; color: var(--text-mute);
}
.exos-mirror-aihint svg { color: var(--accent); flex-shrink: 0; }
.exos-mirror-aihint > span { flex: 1; }
.exos-mirror-detail {
  margin-top: 14px; padding: 10px 14px;
  background: var(--bg-elev-2); border-radius: 8px;
  font-size: 12px; color: var(--text-mute); line-height: 1.55;
}
.exos-mirror-detail .exos-label { display: block; margin-bottom: 4px; color: var(--text-dim); }
.exos-mirror-card.tone-warn::before { background: linear-gradient(90deg, var(--warn), transparent 80%); }
.exos-mirror-card.tone-good::before { background: linear-gradient(90deg, var(--gain), transparent 80%); }
@media (max-width: 760px) {
  .exos-yaw-body { flex-direction: column; gap: 14px; }
  .exos-yaw-line { width: 100%; height: 1px; }
  .exos-rt-body { flex-direction: column; gap: 14px; align-items: flex-start; }
  .exos-freedom-grid { grid-template-columns: repeat(2, 1fr); }
  .exos-freedom-item:nth-child(2n) { border-right: 0; }
  .exos-freedom-item:nth-child(-n+2) { border-bottom: 1px solid var(--line); }
}
`;
(function(){ if(document.getElementById("exos-dash-styles")) return; const t=document.createElement("style"); t.id="exos-dash-styles"; t.textContent=DASH_STYLES; document.head.appendChild(t); })();

window.Dashboard = Dashboard;
