// ============================================================
// 18% CLUB · TRADE DESK · v6
// tradeplan.jsx — Trade-plan workbench
// Calendar + tab nav (Market Data / Pre-market Plan / Order Execution)
// Exports <TradePlanWorkbench> to window for app.jsx
// ============================================================

const { useState: tpUseState, useEffect: tpUseEffect, useRef: tpUseRef, useMemo: tpUseMemo } = React;

// ---------- date helpers ----------
const MONTHS_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const MONTHS_LONG = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

// Parse a plan id like "18May_L_T1" → "2026-05-18"
function parsePlanDate(id, fallbackYear) {
  const m = /^(\d{1,2})([A-Za-z]{3})_/.exec(id);
  if (!m) return null;
  const d = parseInt(m[1], 10);
  const monIdx = MONTHS_SHORT.findIndex((x) => x.toLowerCase() === m[2].toLowerCase());
  if (monIdx < 0) return null;
  const yyyy = String(fallbackYear || 2026);
  const mm = String(monIdx + 1).padStart(2, '0');
  const dd = String(d).padStart(2, '0');
  return `${yyyy}-${mm}-${dd}`;
}

function isoFromYMD(y, m, d) {
  return `${y}-${String(m + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
}

// Build a 6-row × 7-col calendar matrix for given month (0-indexed)
function buildMonthMatrix(year, monthIdx) {
  const first = new Date(year, monthIdx, 1);
  const lastDay = new Date(year, monthIdx + 1, 0).getDate();
  const startCol = first.getDay(); // 0 = Sun
  const cells = [];
  // leading blanks
  for (let i = 0; i < startCol; i++) cells.push(null);
  for (let d = 1; d <= lastDay; d++) cells.push({ y: year, m: monthIdx, d });
  while (cells.length % 7 !== 0) cells.push(null);
  // pad to 6 rows max
  while (cells.length < 42) cells.push(null);
  return cells.slice(0, 42);
}

// ---------- execution-model + trading-session helpers ----------
// Two execution models live in the dataset, split by the 27 May plan-integration
// cutover but detectable per-trade from the plan's own machine condition:
//   • daily-close swing   — entry confirmed at the 16:00 ET cash close
//   • first-hour breakout — entry fires inside the first ~90 min after the open
function execModel(trade) {
  if (!trade || !trade.plan) return 'unknown';
  const s = `${trade.plan.condition_machine || ''} ${trade.plan.validity || ''}`;
  if (/daily_close|swing|session/i.test(s)) return 'daily-close';
  if (/5min_close|first\s*60|first\s*hour|60\s*min|90\s*min/i.test(s)) return 'first-hour';
  return 'unknown';
}

// Snap a (possibly weekend / holiday / missing) date to the last trading session
// on or before it that actually has hourly bars — fixes entry/exit dates that
// landed on non-trading days in the source data (e.g. Saturday fills).
function resolveSession(hourly, date) {
  const days = [...new Set((hourly || []).map((b) => b.t.slice(0, 10)))].sort();
  if (days.includes(date)) return date;
  let res = null;
  for (const d of days) {if (d <= date) res = d;else break;}
  return res || date;
}

// The hourly bar that best represents the entry on a session, BY MODEL:
//   daily-close → the 16:00 ET cash-close bar (fall back to the last RTH bar)
//   first-hour  → the first RTH bar after the 09:30 open (~10:00 ET)
// This deliberately avoids price-in-range matching, which was catching overnight
// bars (00:00–03:00 ET) that merely traded through the same level hours before
// the real fill — the cause of the wrong entry time on the chart.
function sessionEntryBar(hourly, day, model) {
  const bars = (hourly || []).filter((b) => b.t.slice(0, 10) === day);
  if (!bars.length) return null;
  const at = (hhmm) => bars.find((b) => b.t.slice(11, 16) === hhmm);
  const hourOf = (b) => parseInt(b.t.slice(11, 13), 10);
  if (model === 'first-hour') {
    return at('10:00') || at('11:00') ||
    bars.find((b) => hourOf(b) >= 10 && hourOf(b) <= 11) || bars[0];
  }
  // daily-close (and unknown): anchor on the cash close
  return at('16:00') || at('15:00') ||
  [...bars].reverse().find((b) => hourOf(b) >= 13 && hourOf(b) <= 16) ||
  bars[bars.length - 1];
}

// ============================================================
// PLAN CALENDAR — two months side by side
// ============================================================
function PlanCalendar({ plansByDate, selectedDate, onPick, leftMonth, rightMonth, tradingDays, tradingDayRange, untriggeredPlans }) {
  // ----- Month paging -----
  // The calendar shows a 2-month window. Data can span more than two months,
  // so the window is pageable. leftKey = year*12 + monthIdx of the left month.
  const initLeftKey = leftMonth.y * 12 + leftMonth.m;
  const [leftKey, setLeftKey] = tpUseState(initLeftKey);
  const fromKey = (k) => ({ y: Math.floor(k / 12), m: k % 12 });
  // Data bounds → clamp paging so we never wander far past the data.
  const bounds = tpUseMemo(() => {
    if (!tradingDayRange) return null;
    const [ay, am] = tradingDayRange.min.split('-').map(Number);
    const [by, bm] = tradingDayRange.max.split('-').map(Number);
    return { minKey: ay * 12 + (am - 1), maxKey: by * 12 + (bm - 1) };
  }, [tradingDayRange]);
  const minLeft = bounds ? bounds.minKey : initLeftKey;
  const maxLeft = bounds ? Math.max(bounds.minKey, bounds.maxKey - 1) : initLeftKey;
  const curLeft = Math.min(Math.max(leftKey, minLeft), maxLeft);
  const L = fromKey(curLeft);
  const Rm = fromKey(curLeft + 1);
  const canPrev = curLeft > minLeft;
  const canNext = curLeft < maxLeft;

  // Today's ISO date in US Eastern (the market's timezone) — so the calendar's
  // "today" marker matches the trading calendar regardless of the viewer's locale.
  // en-CA formats as YYYY-MM-DD.
  const today = new Date().toLocaleDateString('en-CA', { timeZone: 'America/New_York' });

  const renderMonth = (year, monthIdx) => {
    const cells = buildMonthMatrix(year, monthIdx);
    // Filter to Mon–Fri only (drop Sun + Sat columns)
    const filtered = cells.filter((c, i) => {
      const col = i % 7;
      return col !== 0 && col !== 6; // 0 = Sun, 6 = Sat
    });
    return (
      <div className="cal-month">
        <div className="cal-month-h">{MONTHS_LONG[monthIdx]} {year}</div>
        <div className="cal-dow">
          {['Mon', 'Tue', 'Wed', 'Thu', 'Fri'].map((d) => <span key={d}>{d}</span>)}
        </div>
        <div className="cal-grid">
          {filtered.map((c, i) => {
            if (!c) return <div key={i} className="cal-cell empty" />;
            const iso = isoFromYMD(c.y, c.m, c.d);
            const isToday = iso === today;
            const plans = plansByDate[iso] || [];
            const hasPlans = plans.length > 0;
            // "Closed" only if we have evidence the market was open then — i.e.
            // the day falls inside the OHLC data window but isn't in tradingDays.
            // Days outside the window (before data starts, after today) render as
            // plain Mon–Fri cells without the misleading "closed" label.
            const inDataRange = tradingDayRange &&
            iso >= tradingDayRange.min && iso <= tradingDayRange.max;
            const marketClosed = inDataRange && tradingDays && !tradingDays.has(iso);
            const outOfRange = tradingDayRange && !inDataRange;
            // Plan published but no position taken — either never triggered or
            // activated-but-missed. Distinct muted treatments, never clickable.
            const planMeta = !hasPlans && !marketClosed && untriggeredPlans ? untriggeredPlans[iso] : undefined;
            const noTrigger = !!planMeta && planMeta.status !== 'missed';
            const missed = !!planMeta && planMeta.status === 'missed';
            const planNote = planMeta ? planMeta.note : '';
            const totalPnl = plans.reduce((s, p) => s + (p.result === 'open' ? p.unrealized_pnl_usd || 0 : p.realized_pnl_usd || 0), 0);
            const anyWin = plans.some((p) => p.result === 'win');
            const anyLoss = plans.some((p) => p.result === 'loss');
            const anyOpen = plans.some((p) => p.result === 'open');
            const tone = !hasPlans ? '' : anyOpen && !anyLoss ? 'open' : anyLoss && totalPnl < 0 ? 'loss' : anyWin || totalPnl > 0 ? 'win' : 'flat';
            const isSelected = plans.some((p) => p.id === selectedDate);
            const sign = totalPnl >= 0 ? '+' : '−';
            const abs = Math.abs(totalPnl);
            const dollarStr = abs >= 1000 ? `$${(abs / 1000).toFixed(2)}K` : `$${abs}`;
            return (
              <button
                key={i}
                className={`cal-cell ${hasPlans ? 'has-plans' : ''} ${tone} ${isSelected ? 'sel' : ''} ${marketClosed ? 'closed' : ''} ${outOfRange ? 'out-of-range' : ''} ${noTrigger ? 'no-trigger' : ''} ${missed ? 'missed' : ''} ${isToday ? 'today' : ''}`}
                onClick={() => hasPlans && onPick(plans[0].id)}
                disabled={!hasPlans}
                title={hasPlans ? plans.map((p) => p.id).join(' · ') : noTrigger ? `Plan published · not triggered — ${planNote}` : missed ? `Plan published · activated but missed — ${planNote}` : marketClosed ? 'Market closed' : isToday ? 'Today' : ''}>
                
                {isToday && <span className="cal-today-tag">today</span>}
                <span className="cal-d">{c.d}</span>
                {hasPlans &&
                <div className="cal-meta">
                    <div className="cal-pnl">{sign}{dollarStr}</div>
                    <div className="cal-sub">{plans.length} plan{plans.length === 1 ? '' : 's'}</div>
                  </div>
                }
                {!hasPlans && noTrigger && <div className="cal-meta"><div className="cal-notrig">no trigger</div></div>}
                {!hasPlans && missed && <div className="cal-meta"><div className="cal-missed">missed</div></div>}
                {!hasPlans && !noTrigger && !missed && marketClosed && <div className="cal-meta"><div className="cal-closed">closed</div></div>}
              </button>);

          })}
        </div>
      </div>);

  };

  return (
    <div className="cal-wrap">
      <div className="cal-head">
        <div className="cal-legend">
          <span className="cl-i"><span className="cl-dot win" />win</span>
          <span className="cl-i"><span className="cl-dot loss" />loss</span>
          <span className="cl-i"><span className="cl-dot open" />open</span>
          <span className="cl-i"><span className="cl-dot no-trigger" />planned · not triggered</span>
          <span className="cl-i"><span className="cl-dot missed" />activated · missed</span>
          <span className="cl-i"><span className="cl-dot today" />today</span>
        </div>
        <div className="cal-nav">
          <button
            className="cal-nav-btn"
            onClick={() => setLeftKey(curLeft - 1)}
            disabled={!canPrev}
            aria-label="Previous month">‹</button>
          <span className="cal-nav-range">{MONTHS_SHORT[L.m]} – {MONTHS_SHORT[Rm.m]} {Rm.y}</span>
          <button
            className="cal-nav-btn"
            onClick={() => setLeftKey(curLeft + 1)}
            disabled={!canNext}
            aria-label="Next month">›</button>
        </div>
      </div>
      <div className="cal-months">
        {renderMonth(L.y, L.m)}
        {renderMonth(Rm.y, Rm.m)}
      </div>
    </div>);

}

// ============================================================
// SELECTED PLAN HEADER
// ============================================================
function SelectedPlanHeader({ trade, plansOnDate, onSwitchPlan }) {
  const planDate = parsePlanDate(trade.id);
  const dateLabel = planDate ? (() => {
    const [y, m, d] = planDate.split('-');
    return `${MONTHS_LONG[parseInt(m, 10) - 1]} ${parseInt(d, 10)}`;
  })() : '';
  const dirFull = trade.direction === 'long' ? 'Long' : 'Short';
  const tierFull = trade.tier === 'tier-1' ? 'Tier 1' : trade.tier === 'tier-2' ? 'Tier 2' : trade.tier === 'aggressive' ? 'Aggressive' : trade.tier;
  const hasCascade = !!(trade.analysis && trade.plan && trade.key_level);
  const qqqZone = hasCascade ? trade.key_level.qqq : null;
  const setupSummary = !hasCascade ?
  'Pre-cutover trade · execution-summary only (no archived pre-market plan)' :
  qqqZone ?
  `${qqqZone.label} · QQQ ${qqqZone.low.toFixed(2)}–${qqqZone.high.toFixed(2)} (${trade.plan.rulebook_section}) · valid first ${trade.plan.validity}` :
  `Break above QQQ ${trade.plan.activator_level.toLocaleString()} (${trade.plan.rulebook_section}) · valid first ${trade.plan.validity} of session`;

  const status = trade.result === 'win' ? 'closed · win' : trade.result === 'loss' ? 'closed · loss' : 'open · live';
  const pnl = trade.result === 'open' ? trade.unrealized_pnl_usd || 0 : trade.realized_pnl_usd || 0;
  const sign = pnl >= 0 ? '+' : '−';

  return (
    <div className="splan-head">
      <div className="splan-eb">selected pre-market plan</div>
      <div className="splan-row">
        <div className="splan-title">
          <div className="splan-title-main">
            <span className="splan-d">{dateLabel}</span>
            <span className="splan-sep">·</span>
            <span className="splan-dir">{dirFull}</span>
            <span className="splan-sep">·</span>
            <span className="splan-tier">{tierFull}</span>
          </div>
          <div className="splan-setup">{setupSummary}</div>
        </div>
        <div className={`splan-pnl ${trade.result}`}>
          <div className="splan-pnl-v">{sign}${Math.abs(pnl).toLocaleString()}</div>
          <div className="splan-status">{status}</div>
        </div>
      </div>
      {plansOnDate.length > 1 &&
      <div className="splan-switcher">
          <span className="splan-sw-eb">other plans on this day:</span>
          {plansOnDate.map((p) => {
          const ptierFull = p.tier === 'tier-1' ? 'Tier 1' : p.tier === 'tier-2' ? 'Tier 2' : p.tier === 'aggressive' ? 'Aggressive' : p.tier;
          const pdirFull = p.direction === 'long' ? 'Long' : 'Short';
          return (
            <button
              key={p.id}
              className={`splan-sw-chip ${p.id === trade.id ? 'sel' : ''} ${p.result}`}
              onClick={() => onSwitchPlan(p.id)}>
              
                {pdirFull} · {ptierFull}
              </button>);

        })}
        </div>
      }
    </div>);

}

// ============================================================
// PLAN STAGE NAV — three stages along an arrow
// ============================================================
function PlanStageNav({ active, onPick }) {
  const stages = [
  { id: 'data', label: 'Market raw data', n: '1' },
  { id: 'plan', label: 'Pre-market plan', n: '2' },
  { id: 'order', label: 'Order execution', n: '3' }];

  return (
    <div className="psn-wrap" role="tablist" aria-label="Plan walkthrough stages">
      {stages.map((s) =>
      <button
        key={s.id}
        className={`psn-stage ${active === s.id ? 'on' : ''}`}
        role="tab"
        aria-selected={active === s.id}
        onClick={() => onPick(s.id)}>
        
          <span className="psn-num">{s.n}</span>
          <span className="psn-label">{s.label}</span>
        </button>
      )}
    </div>);

}

// ============================================================
// EXECUTION-ONLY NOTE — graceful placeholder for the Market-data and
// Pre-market-plan tabs of PRE-CUTOVER trades that carry no analysis cascade.
// (Live plan integration began 27 May 2026; earlier trades are execution-only.)
// ============================================================
function ExecutionOnlyNote({ stage, trade }) {
  const isOpen = trade.result === 'open';
  const pnl = isOpen ? trade.unrealized_pnl_usd || 0 : trade.realized_pnl_usd || 0;
  const sign = pnl >= 0 ? '+' : '−';
  const tierFull = trade.tier === 'tier-1' ? 'Tier 1' : trade.tier === 'tier-2' ? 'Tier 2' : trade.tier === 'aggressive' ? 'Aggressive' : trade.tier;
  const which = stage === 'data' ? 'market raw data' : 'pre-market plan';
  return (
    <div className="eo-note">
      <div className="eo-note-eb">no archived {which}</div>
      <div className="eo-note-h">Pre-cutover trade · execution-summary only</div>
      <p className="eo-note-d">
        This {trade.direction} {tierFull} position predates live plan integration
        (which began <strong>27 May 2026</strong>). Its pre-market analysis was not
        captured in the structured pipeline, so only the verified execution record
        is shown. Open the <strong>Order execution</strong> tab for the full fill,
        chart and P&amp;L.
      </p>
      <div className="eo-note-facts">
        <span><b>NQ entry</b> {trade.entry_price ? trade.entry_price.toLocaleString() : '—'}</span>
        <span><b>{isOpen ? 'Mark P&L' : 'Result'}</b> {isOpen ? `${sign}$${Math.abs(pnl).toLocaleString()} · open` : `${trade.result} · ${sign}$${Math.abs(pnl).toLocaleString()}`}</span>
        <span><b>Size</b> {trade.size} contract{trade.size === 1 ? '' : 's'}</span>
      </div>
    </div>);

}

// ============================================================
// MARKET DATA PANEL
// ============================================================
function MarketDataPanel({ trade }) {
  const [openMod, setOpenMod] = tpUseState(null);
  tpUseEffect(() => setOpenMod(null), [trade.id]);
  // Pre-cutover trades carry no pre-market analysis cascade — only live
  // integration (27 May onward) does. Degrade gracefully instead of crashing.
  if (!trade.analysis) return <ExecutionOnlyNote stage="data" trade={trade} />;
  const mods = trade.analysis.modules;
  const modKeys = Object.keys(mods);
  const cls = (s) => s === 'BULL' ? 'bull' : s === 'BEAR' ? 'bear' : 'neut';
  const verify = (trade.analysis.verify || []).filter((g) => g.rows && g.rows.length);
  return (
    <div className="md-panel">
      <div className="md-grid">
        <div className="md-card md-data">
          <div className="md-h">
            <span className="md-eb">data verification · {verify.length ? 'raw indicator sheet' : 'by module'}</span>
            <span className="md-t">{verify.length ? 'Click a module to expand every indicator — reading, status & confidence' : 'Module readings — reading, status & confidence'}</span>
          </div>
          {verify.length ?
          <div className="mdv-list">
            {verify.map((g, gi) => {
              const open = openMod === gi;
              return (
                <div key={gi} className={`mdv-group ${open ? 'open' : ''}`}>
                  <button className="mdv-head" onClick={() => setOpenMod(open ? null : gi)} aria-expanded={open}>
                    <span className="mdv-name">{g.mod}</span>
                    <span className="mdv-head-r">
                      <span className={`mdv-badge ${g.scoreCls}`}>{g.score}</span>
                      <span className="mdv-caret">{open ? '▲' : '▼'}</span>
                    </span>
                  </button>
                  {open &&
                  <div className="mdv-detail">
                    <div className="mdf-row mdf-head-row">
                      <span>Indicator</span><span>Reading</span><span className="c">Status</span><span className="c">Conf.</span>
                    </div>
                    {g.rows.map((r, ri) =>
                    <div key={ri} className="mdf-row">
                        <span className="mdf-ind">{r[0]}</span>
                        <span className="mdf-read">{r[1]}</span>
                        <span className={`mdf-stat ${r[3]}`}>{r[2]}</span>
                        <span className={`mdf-conf ${r[5]}`}>{r[4]}</span>
                      </div>
                    )}
                  </div>
                  }
                </div>);
            })}
            {trade.analysis.verify_source && <div className="mdf-src">source · {trade.analysis.verify_source}</div>}
          </div> :

          <div className="mdv-list">
            {modKeys.map((k) => {
              const m = mods[k];
              const st = cls(m.status);
              const open = openMod === k;
              return (
                <div key={k} className={`mdv-group ${open ? 'open' : ''}`}>
                  <button className="mdv-head" onClick={() => setOpenMod(open ? null : k)} aria-expanded={open}>
                    <span className="mdv-name">{m.name || MODULE_NAMES[k]}</span>
                    <span className="mdv-head-r">
                      <span className={`mdv-badge ${st}`}>{m.score >= 0 ? '+' : ''}{m.score} {m.status}</span>
                      <span className="mdv-caret">{open ? '▲' : '▼'}</span>
                    </span>
                  </button>
                  {open &&
                  <div className="mdv-detail">
                      <div className="mdv-row mdv-row-head">
                        <span>Reading</span>
                        <span>Status</span>
                        <span>Conf.</span>
                      </div>
                      <div className="mdv-row">
                        <span className="mdv-read">{m.reading}</span>
                        <span className={`mdv-stat ${st}`}>{m.status}</span>
                        <span className={`mdv-conf ${m.conf === 'HIGH' ? 'hi' : 'med'}`}>{m.conf}</span>
                      </div>
                    </div>
                  }
                </div>);
            })}
          </div>
          }
        </div>
      </div>
    </div>);

}

// ============================================================
// PRE-MARKET PLAN PANEL — risk vs opportunity diagram
// ============================================================
const MODULE_NAMES = {
  trend: 'Trend (§3)',
  structure: 'Structure (§4)',
  deriv: 'Derivatives (§5)',
  macro: 'Macro (§6)',
  breadth: 'Breadth (§7)',
  leader: 'Leadership (§8)',
  sentiment: 'Sentiment (§9)'
};

// Short briefing for WHY a key level matters, derived from the plan's own zone
// label (call wall / put wall / gamma flip / max-pain / expected-move band).
function keyLevelReason(qqq) {
  if (!qqq || qqq.approx) return null;
  const s = (qqq.label || '').replace(/^\d{1,2}\/\d{1,2}\s*/, '').replace(/\s*\(below\)\s*/i, '').trim();
  const l = s.toLowerCase();
  if (/flip reclaim/.test(l)) return { tag: 'Gamma-flip reclaim', text: 'Reclaiming the gamma-flip level — above it dealer hedging dampens moves, below it amplifies them. A daily close back above flips the tape supportive.' };
  if (/wall continuation/.test(l)) return { tag: 'Call-wall continuation', text: 'Holding above the call-wall and pressing higher — dealers chase short-gamma toward the next strike.' };
  if (/wall reclaim|reclaim/.test(l)) return { tag: 'Call-wall reclaim', text: 'Reclaiming the call-wall — the strike where dealer call-gamma caps price. Closing back above removes the ceiling and re-opens upside.' };
  if (/wall break/.test(l)) return { tag: 'Call-wall break', text: 'Breaking above the call-wall — clearing dealers’ upper hedging band opens the path to the next strike.' };
  if (/ath break/.test(l)) return { tag: 'All-time-high break', text: 'Clearing the prior all-time high — no overhead supply, breakout continuation in play.' };
  if (/breakout/.test(l)) return { tag: 'Breakout', text: 'Breakout above the call-wall — momentum continuation through dealers’ upper band.' };
  if (/max-?pain/.test(l)) return { tag: 'Max-pain pin', text: 'The options-weighted max-pain level — where open interest pins price into expiry; a reclaim leans bullish.' };
  if (/pullback/.test(l)) return { tag: 'Pullback support', text: 'Buying the pullback into the lower expected-move band — mean-reversion support beneath spot.' };
  if (/em lower|expected/.test(l)) return { tag: 'Expected-move floor', text: 'The 1-day expected-move lower band — the statistical floor where dip-buyers typically step in.' };
  return { tag: s, text: `Plan level: ${s}.` };
}

function PremarketPlanPanel({ trade }) {
  if (!trade.analysis || !trade.key_level || !trade.plan)
  return <ExecutionOnlyNote stage="plan" trade={trade} />;
  const paths = trade.analysis.paths || null;
  const basePathKey = paths ? (paths.find((p) => p.key === 'drift') || paths[0]).key : null;
  const [expanded, setExpanded] = tpUseState(null);
  const [selPath, setSelPath] = tpUseState(null);
  const [tssOpen, setTssOpen] = tpUseState(false);
  tpUseEffect(() => {setExpanded(null);setSelPath(null);setTssOpen(false);}, [trade.id]);
  const klReason = keyLevelReason(trade.key_level.qqq);

  const modKeys = Object.keys(trade.analysis.modules);
  const risk = modKeys.filter((k) => trade.analysis.modules[k].score <= 0);
  const opp = modKeys.filter((k) => trade.analysis.modules[k].score > 0);

  const tssPos = trade.analysis.tss.position;
  // TSS bar split into thirds: 0-33 = overbought (risk), 33-66 = natural, 66-100 = oversold (opp for long)
  const tssZone = tssPos < 33 ? 'risk' : tssPos > 66 ? 'opportunity' : 'natural';
  const isLong = trade.direction === 'long';
  const tssVerdict = isLong ?
  tssZone === 'opportunity' ? 'in opportunity area' : tssZone === 'risk' ? 'in risk area' : 'neutral' :
  tssZone === 'risk' ? 'in opportunity area' : tssZone === 'opportunity' ? 'in risk area' : 'neutral';

  return (
    <div className="pp-panel">
      <div className="pp-cols">
        <div className="pp-col-h risk">Risk</div>
        <div className="pp-col-h opp">Opportunity</div>
      </div>

      {/* KEY LEVEL ROW */}
      <div className="pp-kl-row">
        <div className="pp-kl-risk">
          <div className="pp-kl-eb">Plan invalidated</div>
          <div className="pp-kl-h">Closes below {trade.key_level.qqq.low.toFixed(2)}</div>
          <div className="pp-kl-d">stop · −${trade.key_level.stop_loss_usd.toLocaleString()}</div>
        </div>
        <div className="pp-kl-hub">
          <div className="pp-kl-hub-eb">key level</div>
          <div className="pp-kl-hub-v">{trade.key_level.qqq.low.toFixed(2)}–{trade.key_level.qqq.high.toFixed(2)}</div>
          {klReason && <div className="pp-kl-hub-d">{klReason.tag}</div>}
        </div>
        <div className="pp-kl-opp">
          <div className="pp-kl-eb">Plan activation condition</div>
          <div className="pp-kl-h">Closes above {trade.key_level.qqq.high.toFixed(2)}</div>
          <div className="pp-kl-d">{trade.plan.rulebook_section} · first {trade.plan.validity}</div>
        </div>
      </div>
      {klReason &&
      <div className="pp-kl-why">
        <span className="pp-kl-why-eb">why this level</span>
        <span className="pp-kl-why-t">{klReason.text}</span>
      </div>
      }

      {/* MODULE HUB — click a module to expand its detail inline below it */}
      <div className="pp-mod-row">
        <div className="pp-mods risk">
          <div className="pp-mods-h">{risk.length} risk module{risk.length === 1 ? '' : 's'}</div>
          {risk.length === 0 && <div className="pp-mods-empty">no risk flags fired</div>}
          {risk.map((k) => {
            const m = trade.analysis.modules[k];
            const open = expanded === k;
            return (
              <div key={k} className={`pp-mod-item ${open ? 'open' : ''}`}>
                <button className={`pp-mod risk ${open ? 'sel' : ''}`} onClick={() => setExpanded(open ? null : k)} aria-expanded={open}>
                  <span className="pp-mod-n">{m.name || MODULE_NAMES[k]}</span>
                  <span className="pp-mod-s">{m.score}</span>
                </button>
                {open &&
                <div className="pp-mod-detail risk">
                    <div className="pp-md-eb">risk flag · {m.section}</div>
                    <div className="pp-md-d">{m.note}</div>
                  </div>
                }
              </div>);

          })}
        </div>

        <div className="pp-mid-divider" aria-hidden="true" />

        <div className="pp-mods opp">
          <div className="pp-mods-h">{opp.length} opportunity module{opp.length === 1 ? '' : 's'}</div>
          {opp.length === 0 && <div className="pp-mods-empty">no positive scores</div>}
          {opp.map((k) => {
            const m = trade.analysis.modules[k];
            const open = expanded === k;
            return (
              <div key={k} className={`pp-mod-item ${open ? 'open' : ''}`}>
                <button className={`pp-mod opp ${open ? 'sel' : ''}`} onClick={() => setExpanded(open ? null : k)} aria-expanded={open}>
                  <span className="pp-mod-n">{m.name || MODULE_NAMES[k]}</span>
                  <span className="pp-mod-s">+{m.score}</span>
                </button>
                {open &&
                <div className="pp-mod-detail opp">
                    <div className="pp-md-eb">opportunity score · {m.section}</div>
                    <div className="pp-md-d">{m.note}</div>
                  </div>
                }
              </div>);

          })}
        </div>
      </div>

      {/* TSS BAR */}
      <div className="pp-tss">
        <div className="pp-tss-eb">trader-state spectrum · where this plan sits <span className="pp-tss-hint">{tssOpen ? '▲ hide' : '▼ why'}</span></div>
        <button
          type="button"
          className={`pp-tss-bar ${tssOpen ? 'open' : ''}`}
          onClick={() => setTssOpen((v) => !v)}
          aria-expanded={tssOpen}
          aria-label="Toggle trader-state reasoning">
          <div className="pp-tss-zone risk-zone" />
          <div className="pp-tss-zone nat-zone" />
          <div className="pp-tss-zone opp-zone" />
          <div className="pp-tss-marker" style={{ left: `${tssPos}%` }}>
            <span className="pp-tss-marker-dot" />
            <span className="pp-tss-marker-label">{trade.analysis.tss.label}</span>
          </div>
        </button>
        <div className="pp-tss-scale" aria-hidden="true">
          <span className="pp-tss-sc l">Overbought</span>
          <span className="pp-tss-sc m">Natural</span>
          <span className="pp-tss-sc r">Oversold</span>
        </div>
        {tssOpen &&
        <div className={`pp-tss-detail ${tssZone}`}>
          <div className="pp-tss-detail-h">
            <b>{trade.analysis.tss.label}</b>
            {trade.analysis.tss.firm && <span className="pp-tss-firm">{trade.analysis.tss.firm} firm</span>}
            <span className="pp-tss-verdict-tag">{tssVerdict} · {trade.direction}</span>
          </div>
          <div className="pp-tss-detail-d">{trade.analysis.tss.reason}</div>
        </div>
        }
      </div>

      {/* MARKOV ROADMAP — real 4-path forward roadmap; click a path for detail */}
      <div className="pp-roadmap">
        <div className="pp-roadmap-eb">Markov HMM · forward roadmap</div>
        {paths ?
        <>
          <div className="pp-roadmap-bar">
            {paths.map((p) => {
              const open = selPath === p.key;
              return (
                <button
                  key={p.key}
                  className={`pp-rm-seg ${p.key} ${open ? 'sel' : ''}`}
                  style={{ flex: p.pct }}
                  onClick={() => setSelPath(p.key)}
                  aria-pressed={open}
                  title={`${p.label} · ${p.pct}%`}>
                  <span className="pp-rm-pct">{p.pct}%</span>
                  <em className="pp-rm-lab">{{ flush: 'Flush', retest: 'Retest', drift: 'Base', breakout: 'Breakout' }[p.key] || p.label}</em>
                </button>);
            })}
          </div>
          {(() => {
            const p = paths.find((x) => x.key === selPath);
            if (!p) return (
              <div className="pp-rm-hint">Click a path above to see its mechanics, probability, and price footprint.</div>);
            return (
              <div className={`pp-rm-detail ${p.key}`}>
                <div className="pp-rm-detail-h"><b>{p.label}</b><span className="pp-rm-detail-pct">{p.pct}%</span></div>
                <div className="pp-rm-detail-d">{p.mech}</div>
                {p.footprint && <div className="pp-rm-detail-foot">{p.footprint}</div>}
              </div>);
          })()}
        </> :
        <>
          <div className="pp-roadmap-bar">
            <div className="pp-rm-seg retest" style={{ flex: trade.analysis.roadmap.bear_pct }}><span className="pp-rm-pct">{trade.analysis.roadmap.bear_pct}%</span><em className="pp-rm-lab">bear</em></div>
            <div className="pp-rm-seg drift" style={{ flex: trade.analysis.roadmap.neutral_pct }}><span className="pp-rm-pct">{trade.analysis.roadmap.neutral_pct}%</span><em className="pp-rm-lab">base</em></div>
            <div className="pp-rm-seg breakout" style={{ flex: trade.analysis.roadmap.bull_pct }}><span className="pp-rm-pct">{trade.analysis.roadmap.bull_pct}%</span><em className="pp-rm-lab">bull</em></div>
          </div>
          <div className="pp-rm-foot">{(() => {
              const r = trade.analysis.roadmap;
              const skew = r.bull_pct - r.bear_pct;
              const tilt = skew > 4 ? 'bullish' : skew < -4 ? 'bearish' : 'balanced';
              const band = trade.analysis.sbs_band || 'mid';
              const s = trade.analysis.sbs_score;
              return `15-day forward forecast: ${r.neutral_pct}% base · ${r.bull_pct}% bull · ${r.bear_pct}% bear — ${tilt} skew (${skew >= 0 ? '+' : ''}${skew} pts). HMM regime ${band}-conviction at ${s >= 0 ? '+' : ''}${s} / 7 SBS.`;
            })()}</div>
        </>
        }
        {trade.analysis.roadmap && trade.analysis.roadmap.markov_caption &&
        <div className="pp-rm-markov">{trade.analysis.roadmap.markov_caption.text}</div>}
      </div>
    </div>);

}

// ============================================================
// MINI NQ CHART — area chart, replay, planned key-level ZONE
// Trade-only window. Per design feedback: key level renders as a
// translucent zone (not a single line), ±15 NQ points around the planned
// activator (~30-point band) so it reads as a "plan zone", not a price.
// ============================================================
function MiniNqChart({ chart, trade }) {
  const containerRef = tpUseRef(null);
  const chartRef = tpUseRef(null);
  const seriesRef = tpUseRef(null);
  const sliceRef = tpUseRef([]);
  const [replay, setReplay] = tpUseState({ active: false, step: 0, total: 0, day: null, pnl: 0, x: null, y: null });
  const [zoneCoords, setZoneCoords] = tpUseState(null);
  const timerRef = tpUseRef(null);
  const zoneTimerRef = tpUseRef(null);

  const tMarks = chart.marks.filter((m) => m.id === trade.id);
  const entryMark = tMarks.find((m) => m.kind === 'entry' || m.kind === 'open');
  const exitMark = tMarks.find((m) => m.kind === 'win' || m.kind === 'loss');

  // Zone bounds = the PLANNED QQQ activation band, in NQ. Use the stored
  // qqq.nq_low / nq_high directly: they're derived at the documented per-trade
  // ratio (~41.2) and match the rationale text, and they correctly sit BELOW the
  // fill on breakout trades (price closed above the band to trigger). The old
  // code re-anchored zoneTop to the entry price, which dragged the whole band up
  // onto the fill and hid the breakout — that was the wrong zone drawing.
  // Fall back to an entry-anchored ratio, then to ±25, only if nq levels are absent.
  const q = trade.key_level && trade.key_level.qqq || {};
  const kl = trade.key_level || {};
  const hasZone = !!trade.key_level;
  const ratioFallback = q.high && trade.entry_price ? trade.entry_price / q.high : null;
  const zoneTop =
  q.nq_high != null ? q.nq_high :
  ratioFallback ? q.high * ratioFallback :
  kl.value != null ? kl.value + 25 : 0;
  const zoneBottom =
  q.nq_low != null ? q.nq_low :
  ratioFallback ? q.low * ratioFallback :
  kl.value != null ? kl.value - 25 : 0;
  const planDateIso = parsePlanDate(trade.id);
  const planDateLbl = planDateIso ? (() => {
    const [, m, d] = planDateIso.split('-');
    return `${MONTHS_SHORT[parseInt(m, 10) - 1]} ${parseInt(d, 10)}`;
  })() : '';

  const recomputeZone = () => {
    const inst = chartRef.current;
    const slc = sliceRef.current || [];
    if (!inst || !slc.length) return;
    if (!hasZone) {setZoneCoords(null);return;}
    // Position the band with pure pixel math from the slice's price span and the
    // chart's known plot geometry — NOT priceToCoordinate, which returns null for
    // off-range prices AND (in a degenerate single-day window) even in-range ones,
    // which was leaving the band unrendered. This path always resolves.
    const H = 420,CLIP = 406;
    const plotTop = H * 0.16; // matches rightPriceScale.scaleMargins.top
    const plotBot = H * (1 - 0.14); // matches scaleMargins.bottom (~361)
    let vMin = Infinity,vMax = -Infinity;
    for (const bar of slc) {if (bar.l < vMin) vMin = bar.l;if (bar.h > vMax) vMax = bar.h;}
    const span = vMax - vMin || 1;
    const yFor = (price) => {
      const c = Math.max(vMin, Math.min(vMax, price));
      return plotBot - (c - vMin) / span * (plotBot - plotTop);
    };
    let t = Math.min(yFor(zoneTop), yFor(zoneBottom));
    let b = Math.max(yFor(zoneTop), yFor(zoneBottom));
    // Zone fully outside the window collapses to one edge — show a visible sliver.
    if (b - t < 6) {
      if (t >= plotBot - 1) {t = plotBot - 12;b = plotBot;} // below window → sliver at bottom
      else {b = t + 12;} // above window → sliver at top
    }
    b = Math.min(CLIP, b);
    if (t > CLIP - 6) t = CLIP - 12;
    t = Math.max(0, t);
    let left = 0;
    const startBar = slc.find((bar) => bar.day >= planDateIso) || slc[0];
    if (startBar && inst.timeScale) {
      const x = inst.timeScale().timeToCoordinate(startBar.time);
      if (x != null) left = Math.max(0, x);
    }
    const W = containerRef.current ? containerRef.current.clientWidth : 0;
    setZoneCoords({ top: t, height: Math.max(6, b - t), left, width: Math.max(0, W - 56 - left) });
  };

  tpUseEffect(() => {
    const LW = window.LightweightCharts;
    if (!LW || !containerRef.current) return;

    // Hourly NQ bars (real 1h OHLC from the Post-Market Reviewer dataset).
    // Datetimes are ET ("YYYY-MM-DD HH:MM") — parse to a UTC timestamp so the
    // axis labels read back as the same clock time.
    const toTs = (s) => {
      const [d, t] = s.split(' ');
      const [y, mo, da] = d.split('-').map(Number);
      const [hh, mm] = t.split(':').map(Number);
      return Math.floor(Date.UTC(y, mo - 1, da, hh, mm) / 1000);
    };
    const hb = (chart.hourly || []).map((b) => ({
      time: toTs(b.t), t: b.t, o: b.o, h: b.h, l: b.l, c: b.c,
      day: b.t.slice(0, 10), label: b.t.slice(5)
    }));
    if (hb.length === 0) return;

    const lastDay = hb[hb.length - 1].day;
    const model = execModel(trade);
    // Snap entry/exit to the real trading session (some source fills are dated on
    // weekends/holidays with no bars) so markers always land on the candles.
    const entryDate = resolveSession(chart.hourly, entryMark ? entryMark.date : trade.date);
    const exitDate = exitMark ? resolveSession(chart.hourly, exitMark.date) : lastDay;

    // Window = lead-in sessions → entry day → exit day. The lead-in starts a few
    // trading sessions BEFORE entry so the planned key zone (which breakout trades
    // close ABOVE) stays on-screen with its approach, instead of sitting off the
    // bottom of the post-entry run. The zone band itself still starts at plan date.
    const days = [...new Set(hb.map((b) => b.day))].sort();
    const entrySesIdx = days.indexOf(entryDate);
    const LEAD = 4;
    const startDay = entrySesIdx > 0 ? days[Math.max(0, entrySesIdx - LEAD)] : entryDate;
    let s0 = hb.findIndex((b) => b.day === startDay);
    if (s0 < 0) s0 = hb.findIndex((b) => b.day >= startDay);
    if (s0 < 0) s0 = 0;
    let s1 = -1;
    for (let k = hb.length - 1; k >= 0; k--) {if (hb[k].day <= exitDate) {s1 = k;break;}}
    if (s1 < 0) s1 = hb.length - 1;
    const PAD = 2;
    const slice = hb.slice(Math.max(0, s0 - PAD), Math.min(hb.length - 1, s1 + PAD) + 1);
    if (slice.length === 0) return;
    sliceRef.current = slice;

    // Marker placement is MODEL-AWARE, not price-in-range matching (which caught
    // overnight bars that traded through the level long before the real fill).
    // entry → first-hour bar for breakout trades / 16:00 close bar for swings;
    // exit  → the 16:00 cash-close bar of the exit session.
    const barByT = (raw) => raw && slice.find((b) => b.t === raw.t) || null;
    const entryBar = entryMark ? barByT(sessionEntryBar(chart.hourly, entryDate, model)) : null;
    const exitBar = exitMark ? barByT(sessionEntryBar(chart.hourly, exitDate, 'daily-close')) : null;

    const W = containerRef.current.clientWidth || 320;
    const bs = slice.length > 120 ? 3.5 : slice.length > 60 ? 5 : slice.length > 30 ? 8 : 14;
    // Hourly bars are US-ET clock times stored as UTC-of-ET, so UTC getters
    // read back the ET wall-clock. Format axis + tooltip explicitly as ET.
    const etFull = (ts) => {
      const d = new Date(ts * 1000);
      const hh = String(d.getUTCHours()).padStart(2, '0');
      return `${d.getUTCMonth() + 1}/${d.getUTCDate()} ${hh}:00 ET`;
    };
    const etTick = (ts) => {
      const d = new Date(ts * 1000);
      const h = d.getUTCHours();
      return h === 0 ? `${d.getUTCMonth() + 1}/${d.getUTCDate()}` : `${String(h).padStart(2, '0')}:00`;
    };
    const inst = LW.createChart(containerRef.current, {
      width: W, height: 420, autoSize: true,
      layout: { background: { type: 'solid', color: '#FFFFFF' }, textColor: '#5E5E5E', fontFamily: 'ui-monospace, monospace', fontSize: 11, attributionLogo: false },
      localization: { timeFormatter: etFull },
      rightPriceScale: { borderColor: '#DBD3CD', scaleMargins: { top: 0.16, bottom: 0.14 } },
      timeScale: { borderColor: '#DBD3CD', timeVisible: true, secondsVisible: false, fixLeftEdge: true, fixRightEdge: true, rightOffset: 2, barSpacing: bs, tickMarkFormatter: etTick },
      grid: { vertLines: { color: 'rgba(219,211,205,0.45)' }, horzLines: { color: 'rgba(219,211,205,0.65)' } },
      crosshair: { mode: LW.CrosshairMode ? LW.CrosshairMode.Normal : 1 },
      handleScroll: false, handleScale: false
    });
    chartRef.current = inst;

    const series = inst.addCandlestickSeries({
      upColor: '#6E7F67', downColor: '#AB6A57',
      borderUpColor: '#6E7F67', borderDownColor: '#AB6A57',
      wickUpColor: '#8C9384', wickDownColor: '#C08C7C',
      priceLineVisible: false, lastValueVisible: true
    });
    seriesRef.current = series;
    series.setData(slice.map((b) => ({ time: b.time, open: b.o, high: b.h, low: b.l, close: b.c })));

    // Entry + exit markers
    const markers = [];
    if (entryBar) markers.push({
      time: entryBar.time, position: 'belowBar', color: '#AB6A57',
      shape: 'arrowUp', text: `BUY ${trade.entry_price.toLocaleString()}`, size: 1.4
    });
    if (exitBar) markers.push({
      time: exitBar.time, position: 'aboveBar',
      color: trade.result === 'win' ? '#606C5A' : '#AB6A57',
      shape: trade.result === 'win' ? 'arrowDown' : 'square',
      text: trade.result.toUpperCase(), size: 1.4
    });
    series.setMarkers(markers);

    inst.timeScale().fitContent();
    recomputeZone();

    inst.timeScale().subscribeVisibleTimeRangeChange(() => recomputeZone());

    const ro = new ResizeObserver(() => {
      if (containerRef.current && chartRef.current) {
        chartRef.current.applyOptions({ width: containerRef.current.clientWidth });
        recomputeZone();
      }
    });
    ro.observe(containerRef.current);

    return () => {
      ro.disconnect();
      if (zoneTimerRef.current) {clearTimeout(zoneTimerRef.current);zoneTimerRef.current = null;}
      try {inst.remove();} catch (_) {}
      chartRef.current = null;
      seriesRef.current = null;
    };
  }, [trade.id]);

  // ---------- replay ----------
  const stopReplay = () => {
    if (timerRef.current) {clearTimeout(timerRef.current);timerRef.current = null;}
    setReplay({ active: false, step: 0, total: 0, day: null, pnl: 0, x: null, y: null });
  };
  tpUseEffect(() => () => stopReplay(), []);
  tpUseEffect(() => {stopReplay();}, [trade.id]);

  const startReplay = () => {
    const slice = sliceRef.current;
    const series = seriesRef.current;
    const inst = chartRef.current;
    if (!slice.length || !series || !inst) return;

    const model = execModel(trade);
    const entryDate = resolveSession(chart.hourly, entryMark ? entryMark.date : trade.date);
    const exitDate = exitMark ? resolveSession(chart.hourly, exitMark.date) : slice[slice.length - 1].day;
    // Start the replay at the model-correct ORDER bar (first-hour vs cash close),
    // matched into the visible slice by timestamp — not a price-in-range guess.
    const orderRaw = sessionEntryBar(chart.hourly, entryDate, model);
    const orderBar = orderRaw && slice.find((b) => b.t === orderRaw.t) ||
    slice.find((b) => b.day === entryDate);
    let startIdx = orderBar ? slice.indexOf(orderBar) : slice.findIndex((b) => b.day === entryDate);
    if (startIdx < 0) startIdx = 0;
    let endIdx = -1;
    for (let k = slice.length - 1; k >= 0; k--) {if (slice[k].day <= exitDate) {endIdx = k;break;}}
    if (endIdx < 0) endIdx = slice.length - 1;
    if (startIdx > endIdx) return;

    const POINT_VALUE = 20;
    const dir = trade.direction === 'long' ? 1 : -1;
    const total = endIdx - startIdx + 1;
    // Keep the whole sweep ~9s regardless of how many hourly bars are in range.
    const interval = Math.max(45, Math.round(9000 / total));
    let i = startIdx;

    const tick = () => {
      if (i > endIdx) {stopReplay();return;}
      const bar = slice[i];
      const pnl = Math.round((bar.c - trade.entry_price) * POINT_VALUE * (trade.size || 1) * dir);
      const ts = inst.timeScale();
      const x = ts.timeToCoordinate(bar.time);
      const y = series.priceToCoordinate ? series.priceToCoordinate(bar.c) : null;
      setReplay({
        active: true, step: i - startIdx + 1, total, day: bar.label, pnl,
        x: x != null ? x : null, y: y != null ? y : null
      });
      i++;
      timerRef.current = setTimeout(tick, interval);
    };
    tick();
  };

  const planDateLabel = (d) => d || '';

  return (
    <div className="mini-nq-wrap">
      <div className="mini-nq-toolbar">
        {!replay.active ?
        <button
          className="mini-nq-replay"
          onClick={startReplay}
          disabled={!entryMark}
          aria-label="Replay this trade day by day">
            ▶ Replay this trade
          </button> :

        <button className="mini-nq-replay live" onClick={stopReplay}>■ Stop replay</button>
        }
        {replay.active &&
        <div className="mini-nq-status">
            <span className="mnq-eb">DAY {replay.step}/{replay.total}</span>
            <span className="mnq-day">{planDateLabel(replay.day)}</span>
            <span className={`mnq-pnl ${replay.pnl >= 0 ? 'pos' : 'neg'}`}>
              {replay.pnl >= 0 ? '+' : '−'}${Math.abs(replay.pnl).toLocaleString()}
            </span>
          </div>
        }
      </div>
      <div className="mini-nq-stage">
        <div ref={containerRef} className="mini-nq" style={{ width: '100%', height: 420 }} />
        {/* Planned key-level ZONE — translucent band, QQQ-derived, from plan date → right edge */}
        {zoneCoords &&
        <div
          className="mnq-zone"
          style={{ top: zoneCoords.top, height: zoneCoords.height, left: zoneCoords.left, width: zoneCoords.width, right: 'auto' }}
          aria-hidden="true">
            <span className="mnq-zone-label">key zone · QQQ {q.low != null ? q.low.toFixed(2) : ''}–{q.high != null ? q.high.toFixed(2) : ''}</span>
          </div>
        }
        {/* Plan-date marker — where the zone begins */}
        {zoneCoords && planDateLbl &&
        <div className="mnq-planline" style={{ left: zoneCoords.left }} aria-hidden="true">
            <span className="mnq-planline-lbl">plan · {planDateLbl}</span>
          </div>
        }
        {/* Live replay floating chip */}
        {replay.active && replay.x != null &&
        <>
            <div className="mnq-vline" style={{ left: replay.x }} aria-hidden="true" />
            {replay.y != null &&
          <div
            className={`mnq-dot ${replay.pnl >= 0 ? 'pos' : 'neg'}`}
            style={{ left: replay.x, top: replay.y }}
            aria-hidden="true" />

          }
            <div className="mnq-chip" style={{ left: replay.x, top: Math.max(8, (replay.y || 40) - 44) }}>
              <div className="mnq-chip-d">{planDateLabel(replay.day)}</div>
              <div className={`mnq-chip-v ${replay.pnl >= 0 ? 'pos' : 'neg'}`}>
                {replay.pnl >= 0 ? '+' : '−'}${Math.abs(replay.pnl).toLocaleString()}
              </div>
            </div>
          </>
        }
      </div>
    </div>);

}

// ============================================================
// ORDER EXECUTION PANEL
// ============================================================
function OrderExecutionPanel({ chart, trade }) {
  const planDate = parsePlanDate(trade.id);
  const isOpen = trade.result === 'open';
  const model = execModel(trade);

  // Evidence timestamps — all US Eastern (ET), the market's timezone.
  // The pre-market plan is committed before the 09:30 open. The ORDER time then
  // depends on the execution model: daily-close swing trades confirm at the
  // 16:00 ET cash close; first-hour breakout trades fire inside the first ~90
  // min of the session. The order day is also snapped to the real trading
  // session (some source fills are dated on weekends with no market).
  const entryDate = resolveSession(chart.hourly, trade.date);
  const orderBar = sessionEntryBar(chart.hourly, entryDate, model);
  const orderClock = orderBar ? orderBar.t.slice(11, 16) : model === 'first-hour' ? '10:30' : '16:00';
  const planTs = `${planDate} 08:00 ET`;
  const orderTs = `${entryDate} ${orderClock} ET`;
  const orderBasis = model === 'first-hour' ? 'first-hour breakout' : 'daily close';
  const closeTs = trade.exit_price ? (() => {
    const exitMark = chart.marks.find((m) => m.id === trade.id && (m.kind === 'win' || m.kind === 'loss'));
    return exitMark ? `${resolveSession(chart.hourly, exitMark.date)} 16:00 ET` : '—';
  })() : '—';

  // Pre-cutover trades have no plan cascade — render execution facts only and
  // skip the plan-derived rows (trigger condition, rulebook, planned stop/target).
  const hasPlan = !!(trade.plan && trade.key_level);
  // QQQ activation level — the REAL plan edge from the key zone, not an NQ/ratio guess.
  const qz = trade.key_level && trade.key_level.qqq || {};
  const qqqTrigger = qz.high != null ? qz.high.toFixed(2) : hasPlan ? (trade.plan.activator_level / 41.2).toFixed(2) : null;
  const sign = trade.result === 'open' ? trade.unrealized_pnl_usd >= 0 ? '+' : '−' : trade.realized_pnl_usd >= 0 ? '+' : '−';
  const pnl = trade.result === 'open' ? trade.unrealized_pnl_usd : trade.realized_pnl_usd;

  return (
    <div className="oe-panel">
      <div className="oe-right oe-chart-top">
        <div className="oe-chart-h">
          <span className="oe-chart-eb">NQ Hourly Chart · this trade only</span>
          {hasPlan && <span className="oe-chart-t">key zone planned</span>}
        </div>
        <MiniNqChart chart={chart} trade={trade} />
        <div className="oe-chart-legend">
          <span className="ocl-i"><span className="ocl-zone" />planned key zone</span>
          <span className="ocl-i"><span className="ocl-dot terra" />buy / sell marker</span>
          <span className="ocl-i"><span className="ocl-dot sage" />replay sweeps hour by hour</span>
        </div>
      </div>

      <div className="oe-grid">
        <div className="oe-left">
          <div className="oe-timeline">
            <div className="oe-step">
              <div className="oe-step-dot" />
              <div className="oe-step-body">
                <div className="oe-step-eb">plan created</div>
                <div className="oe-step-h">{planTs}</div>
                <div className="oe-step-d">{hasPlan ?
                  <>Pre-market plan committed and timestamped. Rulebook {trade.plan.rulebook_section} · activator armed at <em>{trade.plan.activator_level.toLocaleString()}</em>.</> :
                  <>Pre-cutover trade — no archived pre-market plan. Tracked at execution-summary level (live plan integration began 27 May).</>}</div>
              </div>
            </div>
            <div className="oe-step">
              <div className="oe-step-dot fire" />
              <div className="oe-step-body">
                <div className="oe-step-eb">order triggered{hasPlan ? ` · ${orderBasis}` : ''}</div>
                <div className="oe-step-h">{orderTs}</div>
                <div className="oe-step-d">{hasPlan ? trade.plan.condition_human || `Daily close through QQQ ${qqqTrigger} / NQ ${trade.plan.activator_level.toLocaleString()}.` : `NQ entry filled at ${trade.entry_price.toLocaleString()}.`}</div>
                <div className="oe-step-meta">
                  <span><b>NQ entry</b> {trade.entry_price.toLocaleString()}</span>
                  <span><b>Size</b> {trade.size} contract{trade.size === 1 ? '' : 's'}</span>
                  <span><b>Direction</b> {trade.direction.toUpperCase()}</span>
                </div>
              </div>
            </div>
            <div className={`oe-step ${isOpen ? 'open' : ''}`}>
              <div className={`oe-step-dot ${isOpen ? 'open' : trade.result === 'win' ? 'win' : 'loss'}`} />
              <div className="oe-step-body">
                <div className="oe-step-eb">{isOpen ? 'still open · live mark' : `order closed · ${trade.result}`}</div>
                <div className="oe-step-h">{closeTs}</div>
                <div className="oe-step-d">
                  {isOpen ?
                  hasPlan ?
                  <>Position held open. Trailing stop at <em>{trade.key_level.stop_loss_price.toLocaleString()}</em>, target at <em>{trade.key_level.target_price.toLocaleString()}</em>. Held {trade.hold_duration}.</> :
                  <>Position held open at live mark. Held {trade.hold_duration}.</> :
                  <>NQ exit at <em>{trade.exit_price.toLocaleString()}</em>. Hold {trade.hold_duration}. P&amp;L {sign}${Math.abs(pnl).toLocaleString()}.</>
                  }
                </div>
              </div>
            </div>
          </div>
        </div>

        <div className="oe-summary">
            {hasPlan &&
          <div className="oe-sum-row">
              <span className="oe-sum-k">trigger condition</span>
              <span className="oe-sum-v mono">QQQ &gt; {qqqTrigger} · NQ &gt; {trade.plan.activator_level.toLocaleString()}</span>
            </div>}
            {hasPlan &&
          <div className="oe-sum-row">
              <span className="oe-sum-k">validity window</span>
              <span className="oe-sum-v">{trade.plan.validity}</span>
            </div>}
            <div className="oe-sum-row">
              <span className="oe-sum-k">NQ entry price</span>
              <span className="oe-sum-v mono pos">{trade.entry_price.toLocaleString()}</span>
            </div>
            <div className="oe-sum-row">
              <span className="oe-sum-k">NQ exit price</span>
              <span className={`oe-sum-v mono ${trade.exit_price ? trade.result === 'win' ? 'pos' : 'neg' : 'mute'}`}>{trade.exit_price ? trade.exit_price.toLocaleString() : 'open at mark'}</span>
            </div>
            <div className="oe-sum-row">
              <span className="oe-sum-k">P&amp;L</span>
              <span className={`oe-sum-v mono ${pnl >= 0 ? 'pos' : 'neg'}`}>{sign}${Math.abs(pnl).toLocaleString()}</span>
            </div>
          </div>
      </div>
    </div>);

}

// ============================================================
// ROOT — TradePlanWorkbench
// ============================================================
function TradePlanWorkbench({ data, currentTradeId, onSelect }) {
  const [activeTab, setActiveTab] = tpUseState('order');
  // Keep the last-selected trade in state so the details panel can play its
  // slide-OUT animation with the right content still rendered after a deselect.
  const [lastTradeId, setLastTradeId] = tpUseState(null);
  tpUseEffect(() => {if (currentTradeId) setLastTradeId(currentTradeId);}, [currentTradeId]);

  // Build plansByDate (keyed by plan-creation date)
  const plansByDate = tpUseMemo(() => {
    const map = {};
    data.trades.forEach((t) => {
      const pd = parsePlanDate(t.id);
      if (!pd) return;
      if (!map[pd]) map[pd] = [];
      map[pd].push(t);
    });
    return map;
  }, [data.trades]);

  // Default calendar window anchors on the LATEST trade month — not "today" or
  // the latest OHLC month — so the 2-month view always lands on real activity
  // (e.g. June bars may exist before any June trade fires). Window = [last-1, last].
  const defaultMonths = tpUseMemo(() => {
    const keys = Object.keys(plansByDate).sort();
    const last = keys[keys.length - 1];
    if (!last) return { left: { y: 2026, m: 3 }, right: { y: 2026, m: 4 } };
    const [y, m] = last.split('-').map(Number);
    const rightKey = y * 12 + (m - 1);
    const leftKey = rightKey - 1;
    return {
      left: { y: Math.floor(leftKey / 12), m: leftKey % 12 },
      right: { y: Math.floor(rightKey / 12), m: rightKey % 12 }
    };
  }, [plansByDate]);

  // Days where a pre-market plan WAS published but no position ended up in the
  // book. Two sub-cases, both rendered as muted cells so the discipline (and
  // the honesty about missed fills) is visible without faking a P&L result:
  //   • no_trigger — the setup never cleared its activation gate, no entry.
  //   • missed     — the setup activated, but the fill was never logged.
  // Keyed by ISO date → { status, note }.
  const untriggeredPlans = tpUseMemo(() => {
    const map = {};
    (data.untriggered_plans || []).forEach((u) => {
      map[u.date] = { status: u.status || 'no_trigger', note: u.note || '' };
    });
    return map;
  }, [data.untriggered_plans]);

  // Trading days come from the OHLC stream — anything not in there (weekends,
  // holidays like Memorial Day) is a market-closed cell.
  const tradingDays = tpUseMemo(
    () => new Set((data.chart?.ohlc || []).map((d) => d.date)),
    [data.chart?.ohlc]
  );
  // Min/max ISO date in the OHLC stream — dates outside this range render
  // as plain cells (we have no data to claim "closed" with confidence).
  const tradingDayRange = tpUseMemo(() => {
    const ohlc = data.chart?.ohlc || [];
    if (!ohlc.length) return null;
    const dates = ohlc.map((d) => d.date).sort();
    return { min: dates[0], max: dates[dates.length - 1] };
  }, [data.chart?.ohlc]);

  const renderTrade = data.trades.find((t) => t.id === (currentTradeId || lastTradeId)) || data.trades[0];
  const currentPlanDate = parsePlanDate(renderTrade.id);
  const plansOnDate = currentPlanDate ? plansByDate[currentPlanDate] || [] : [];

  // Reset to default tab when plan switches
  tpUseEffect(() => {if (currentTradeId) setActiveTab('order');}, [currentTradeId]);

  return (
    <section className="tpw" id="trade-workbench">
      <div className="tpw-section-h">
        <h2 className="hd-h">Pick a day. Walk the <em>data-to-plan-to-execution</em> trail.</h2>
      </div>

      <div className={`tpw-group ${currentTradeId ? 'has-selection' : ''}`}>
        <PlanCalendar
          plansByDate={plansByDate}
          selectedDate={currentTradeId}
          onPick={onSelect}
          tradingDays={tradingDays}
          tradingDayRange={tradingDayRange}
          untriggeredPlans={untriggeredPlans}
          leftMonth={defaultMonths.left}
          rightMonth={defaultMonths.right} />
        

        {/* Detail panel: slides in when a date is selected, slides out when toggled off.
                                 Inner content keeps rendering after deselect so the slide-out animation
                                 has something to animate — the collapsed wrapper hides it. */}
        <div className={`tpw-details ${currentTradeId ? 'open' : 'closed'}`} aria-hidden={!currentTradeId}>
          <div className="tpw-details-inner">
            {/* Re-key on trade id so switching plans re-fires the slide-in
                                   animation on the inner content (not just the wrapper). */}
            <div className="tpw-details-content" key={renderTrade.id}>
              <SelectedPlanHeader
                trade={renderTrade}
                plansOnDate={plansOnDate}
                onSwitchPlan={onSelect} />

              <PlanStageNav active={activeTab} onPick={setActiveTab} />

              <div className="tpw-tab-body">
                {activeTab === 'data' && <MarketDataPanel trade={renderTrade} />}
                {activeTab === 'plan' && <PremarketPlanPanel trade={renderTrade} />}
                {activeTab === 'order' && <OrderExecutionPanel chart={data.chart} trade={renderTrade} />}
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>);

}

// ---------- export ----------
Object.assign(window, {
  TradePlanWorkbench,
  parsePlanDate
});