'use client'
import { Fragment, useCallback, useEffect, useRef, useState } from 'react'

type Sell = { ts: number; qty: number; price: number; pnl: number; trim?: boolean }  // trim = a manual reduce
// `manual` marks an open the OPERATOR placed rather than the ladder: it consumed no tranche and
// it moved the blended average the close is priced off, so history has to distinguish it.
type Play = { symbol: string; ts: number; action?: string; pnl: number | null; qty: number | null; price: number | null; manual?: boolean }
type Txn = { ts: number; action: string; qty: number; price: number; pnl?: number; avg?: number; rung?: number; manual?: boolean }
type ProfStat = { symbol: string; profile?: string; plays: number; realized: number; maxCapital: number }
type DayStat = { date: string; realized: number; plays: number }
type HourStat = { hour: number; buys: number; sells: number }
type Settings = {
  cash_budget: number; reverse_inventory: Record<string, number>; place_within_pct?: number
  // cash-constrained picker: long profiles allowed NEW buys while the budget can't fund everyone
  constrained_play?: string[]; constrained_cancel?: boolean; constrained_until?: number | null
  constrained_rejoined?: string[]   // put back in play by the bot when their position closed
}
type FundTxn = { ts: number; type: string; amount: number; price: number; units: number; memo?: string }
type Investor = { name: string; units: number; deposited: number; withdrawn: number; value: number; gain: number; gainPct: number; note?: string; todayChange?: number; annualPct?: number | null; heldDays?: number; txns?: FundTxn[] }
// todayChange is the NAV move since this Pacific day opened, exact across mid-day cash
// moves (a deposit lifts `value` without being a gain). dayOpenPrice is the reference.
type Fund = { price: number; baseCapital: number; pnlSinceInception: number; inceptionTs?: number; totalValue: number; dayOpenPrice?: number; todayChange?: number; todayChangePct?: number; annualPct?: number | null; investors: Investor[] }
type TokenStatus = { authTs: number; expiresTs: number }
type CapitalMember = { profile: string; cycles: number; realized: number; mtm: number
                       openCapital: number; openDays: number; dollarDays: number }

type CapitalRow = {
  symbol: string; direction: 'long' | 'short'; basis: 'cash' | 'inventory'
  profiles: number; members: CapitalMember[]
  cycles: number; realized: number; dollarDays: number; ladderAlloc: number
  avgDeployed: number; perKDay: number | null; annDeployed: number | null
  medHoldDays: number; p90HoldDays: number; medProfit: number; liveDays: number
  peakCapital: number; annOnPeak: number | null
  tradingDays: number; cyclesPerDay: number | null
  openCapital: number; openDays: number; openUnrealized: number; dollarDaysOpen: number
  mtm: number; annDeployedClosed: number | null; annDeployedAll: number | null
  ciLo: number | null; ciHi: number | null; annOnPeakLo: number | null
  confidence: 'tight' | 'medium' | 'wide' | null
  window: null | { cfgSince: number; cfgDays: number; cycles: number; insufficient?: boolean
                   realized?: number; utilPct?: number; annDeployed?: number | null; annReserved?: number }
}

type Analytics = {
  perProfile: ProfStat[]
  windows?: Record<string, ProfStat[]>
  byDay: DayStat[]
  byHour: HourStat[]
  byHourWindows?: Record<string, HourStat[]>
  tradingDaysByWindow?: Record<string, number>
  tradingDaysPerYear?: number
  totals: { plays: number; realized: number; buys: number }
}

const ANALYTICS_WINDOWS: { key: string; label: string }[] = [
  { key: 'today', label: 'Today' },
  { key: '24h', label: '24h' },
  { key: '7d', label: '7d' },
  { key: '30d', label: '30d' },
  { key: 'all', label: 'All' },
]
type Profile = {
  symbol: string
  profile: string
  phase: string | null
  shares: number
  avg: number | null
  sellTarget: number | null
  highWater: number | null
  lastPrice: number | null
  restingBuys: number[]
  restingBuysSh?: number[]
  sellPricesSh?: number[]
  maxShares?: number
  closePrice?: number | null
  closeHandoff?: boolean
  expiry?: number | null
  disabledReason?: string | null
  pendingRename?: string | null
  deferredBuys?: number[]
  filledCount: number
  cap: number | null
  staggerTier?: number | null
  staggerCount?: number | null
  staggerSiblings?: number | null
  staggerGap?: number | null
  staggerStepGap?: number | null
  staggerGapSet?: boolean | null
  staggerOffsetPct?: number | null
  staggerCeilingEst?: number | null
  ladderRef?: number | null
  realizedPnl: number
  lastPnl: number | null
  lastSell: Sell | null
  unrealizedPnl: number
  tranche: number | null
  firstStepPct: number | null
  stepPct: number | null
  profitPct: number | null
  scalePct: number | null
  maxPrice: number | null
  maxCapital?: number
  realized24h: number
  realized7d: number
  realized30d: number
  realizedAll?: number
  realizedToday: number
  updatedMs: number | null
  txnCount?: number   // records come from /api/trades/<profile>/transactions on demand
  funded?: boolean
  priority?: number | null
  recon?: Recon | null
  halted?: boolean
  pausedUntil?: number | null
  pauseHold?: boolean
  awaitPrice?: number | null
  disabled?: boolean
  frozen?: boolean
  extra?: boolean
  direction?: string
  shadow?: boolean
  netSold?: number
  soldRungs?: number
  soldAvg?: number | null
  sellPrices?: number[]
  deferredSells?: number[]
  coverTarget?: number | null
  lowWater?: number | null
  poolUsed?: number | null
  poolCap?: number | null
  minPrice?: number | null
  manual?: ManualLot[]
  manualShares?: number
  manualAvg?: number | null
  manualOpen?: number
  manualAnchored?: boolean
  reduceReserved?: number   // shares held out of the close for live manual reduces
  reducedShares?: number    // shares trimmed by manual reduces this cycle
  cashConstrained?: boolean // the cash picker is restricting new buys right now
  notInPlay?: boolean       // ...and this profile isn't one of the picked ones
  heldLots?: [number, number][]  // [price, qty] filled lots, in the order a reduce trims them
}
// The rung you handed off for an overnight EXTO order. Editing that order in the brokerage gives
// it a NEW order id, so the bot's tracked rung goes REPLACED and drops out of the resting list —
// the row would simply vanish, which reads like the order was lost. Keep it visible as an amber
// placeholder for as long as the hand-off is active, the same way the close hand-off keeps the
// sell target on screen with a badge.
const AwaitedRung = ({ price, any }: { price: number; any: boolean }) => (
  <span className="text-amber-600" title="Handed off to you for an overnight (EXTO) order. The bot is not managing this level until it fills or is reclaimed at the next session open.">
    {any ? ', ' : ''}${price.toFixed(2)} <span className="text-[10px]">await</span>
  </span>
)

// Marks a fill that came from a manual lot rather than a ladder rung, in both history lists.
const ManualChip = () => (
  <span className="ml-1.5 align-middle rounded-full bg-violet-500/15 px-1.5 py-px text-[9px] font-medium text-violet-600"
    title="Placed by you as a manual lot — it took no tranche, and it moved the average the close is priced off.">
    manual
  </span>
)

// A MANUAL LOT is an order you created for a profile (limit price + qty). The bot places it on
// the normal session; it joins that profile's ledger — so it closes with the position and blends
// into the average the close is priced off — but never occupies a ladder rung. To take one
// overnight, swap that order's session to EXTO in the broker app: the broker replaces it with a
// new order id and the bot re-claims the replacement, so the lot stays counted.
type ManualLot = {
  id: string
  side: string
  price: number
  qty: number
  status: string          // PENDING | WORKING | FILLED | CANCELED | REJECTED
  repriceTo?: number | null  // price edit waiting for the old order's cancel to land
  session?: string        // 'EXTO' once you swap it to overnight in the brokerage
  fillQty?: number | null
  fillPrice?: number | null
  fillTs?: number | null
  createdTs?: number | null
  watching?: boolean      // the bot is still eligible to re-claim a replacement for this lot
  reduce?: boolean        // a REDUCE: trims the position on the closing side (cheapest lots first)
  error?: string | null
}
const MANUAL_DONE = new Set(['FILLED', 'CANCELED', 'REJECTED', 'EXPIRED', 'REPLACED'])

type Recon = {
  ts: number; match: boolean
  iShares: number; bShares: number; iAvg: number; bAvg: number; dShares: number; dAvg: number
  untracked?: number; symSigma?: number; symBroker?: number
}

// A ticker may carry several profiles; lead with the profile key (unique), then the ticker.
const profLabel = (p: { symbol: string; profile?: string }) =>
  p.profile && p.profile !== p.symbol ? `${p.profile} (${p.symbol})` : p.symbol

const money = (n: number) => `${n >= 0 ? '+' : '-'}$${Math.abs(n).toFixed(2)}`
const tone = (n: number) => (n >= 0 ? 'text-emerald-600' : 'text-red-500')
const Money = ({ n }: { n: number }) => <span className={tone(n)}>{money(n)}</span>

const PIE_COLORS = ['#10b981', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316', '#84cc16', '#06b6d4']

const fmtHour = (h: number) => `${h % 12 === 0 ? 12 : h % 12}${h < 12 ? 'a' : 'p'}`

function ago(ms: number | null, now: number) {
  if (!ms) return ''
  const s = Math.max(0, Math.round((now - ms) / 1000))
  if (s < 60) return `${s}s ago`
  if (s < 3600) return `${Math.round(s / 60)}m ago`
  if (s < 86400) return `${Math.round(s / 3600)}h ago`
  return `${Math.round(s / 86400)}d ago`
}

export function TradeProfiles() {
  const [profiles, setProfiles] = useState<Profile[] | null>(null)
  const [plays, setPlays] = useState<Play[]>([])
  const [analytics, setAnalytics] = useState<Analytics | null>(null)
  const [settings, setSettings] = useState<Settings | null>(null)
  const [fund, setFund] = useState<Fund | null>(null)
  const [fundHist, setFundHist] = useState<string | null>(null)  // investor whose deposit history is expanded
  const [token, setToken] = useState<TokenStatus | null>(null)
  const [editBudgets, setEditBudgets] = useState(false)
  const [playDraft, setPlayDraft] = useState<string[]>([])      // cash picker: picked profile keys
  const [playCancelDraft, setPlayCancelDraft] = useState(false) // ...cancel unpicked resting buys
  const [playUntilDraft, setPlayUntilDraft] = useState('')      // ...optional expiry (datetime-local)
  const [budgetDraft, setBudgetDraft] = useState<Record<string, string>>({})
  const [showHistory, setShowHistory] = useState(false)
  const [showAnalytics, setShowAnalytics] = useState(false)
  const [capital, setCapital] = useState<CapitalRow[]>([])
  const [showCapital, setShowCapital] = useState(false)
  // Disabled profiles are parked, not running: they place no opening orders and are excluded
  // from the allocation total already. Hidden by default so the board shows what is live.
  const [showDisabled, setShowDisabled] = useState(false)
  // A profile is born disabled, which would hide it the moment you created it — exactly when
  // you still need to tune and enable it. One that has never traded therefore stays visible
  // regardless. Better than a timer: nothing to time out mid-setup, no creation timestamp to
  // invent, and it clears itself the moment the profile does something.
  const neverRan = (p: Profile) => !!p.disabled && !p.realizedAll && !p.shares && !(p.netSold || 0)
  // Disabled while still holding: the bot keeps its sell (cover on a short) resting and places no
  // new opening orders, so it is WINDING DOWN, not parked — it stays on screen as "closing out"
  // until the position is flat, then becomes a plain disabled profile and hides with the rest.
  const closingOut = (p: Profile) => !!p.disabled && (p.shares > 0 || (p.netSold || 0) > 0)
  const parkedOff = (p: Profile) => !!p.disabled && !closingOut(p)
  const [showEquity, setShowEquity] = useState(false)
  const [showUnreal, setShowUnreal] = useState(false)
  const [showPotential, setShowPotential] = useState(false)
  const [dayLimit, setDayLimit] = useState(14)
  // What the resting close order is worth if it fills: the gap between the target and the
  // basis, times the position. The target price alone says where it closes, not what that is
  // worth — and the two diverge as a ladder averages down, which is exactly when it matters.
  const closeGain = (p: Profile, short: boolean) => {
    if (short) {
      const q = p.netSold ?? 0
      if (!q || p.coverTarget == null || p.soldAvg == null) return null
      return q * (p.soldAvg - p.coverTarget)          // sold high, buy back lower
    }
    if (!p.shares || p.sellTarget == null || p.avg == null) return null
    return p.shares * (p.sellTarget - p.avg)
  }
  const [capSort, setCapSort] = useState<string>('peakLo')
  const [capDesc, setCapDesc] = useState(true)
  const [normalize, setNormalize] = useState(false)
  const [analyticsWindow, setAnalyticsWindow] = useState('all')
  const [expandedSym, setExpandedSym] = useState<string | null>(null)
  const [hourWindow, setHourWindow] = useState('all')
  const [txShowBuys, setTxShowBuys] = useState(true)
  const [histLimit, setHistLimit] = useState(15)  // rows shown in transaction history (expandable)
  const [txFilter, setTxFilter] = useState('')
  const [pending, setPending] = useState<Record<string, string>>({})
  const [savedMsg, setSavedMsg] = useState<Record<string, string>>({})  // transient "✓ queued" per profile
  const [editSym, setEditSym] = useState<string | null>(null)
  const [closeEdit, setCloseEdit] = useState<string | null>(null)  // profile key whose close-price is being set
  const [closeDraft, setCloseDraft] = useState('')
  const [manualEdit, setManualEdit] = useState<string | null>(null)  // profile key whose manual-lot form is open
  const [manualQty, setManualQty] = useState('')
  const [manualPx, setManualPx] = useState('')
  const [manualReduce, setManualReduce] = useState(false)  // the open form trims instead of adds
  const [draft, setDraft] = useState<Record<string, string>>({})
  const [draftBase, setDraftBase] = useState<Record<string, string>>({})  // draft as opened, to detect real edits

  const sendCommand = async (profile: string, action: string, price?: number, config?: Record<string, number | null>) => {
    setPending(p => ({ ...p, [profile]: action }))
    try {
      await fetch('/api/trades-command', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ profile, action, price, config }),
      })
    } catch { /* will show in badge state once the mini applies it */ }
  }

  // MANUAL LOT: your own order on a profile — limit price + qty, placed by the bot (or by you,
  // when EXTO/overnight is forced) and folded into the profile's ledger without taking a rung.
  const sendManual = async (profile: string, side: string, price: number, qty: number) => {
    setPending(p => ({ ...p, [profile]: 'manual_order' }))
    try {
      await fetch('/api/trades-command', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action: 'manual_order', profile, side, price, qty }),
      })
    } catch { /* appears on the next poll */ }
  }
  // New limit price for a lot. The bot re-prices it safely: a working order is cancelled first and
  // re-placed only once the cancel is confirmed with nothing filled.
  const editManual = async (profile: string, id: string, price: number) => {
    setPending(p => ({ ...p, [profile]: 'edit_manual' }))
    try {
      await fetch('/api/trades-command', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action: 'edit_manual', profile, id, price }),
      })
    } catch { /* appears on the next poll */ }
  }
  const cancelManual = async (profile: string, id: string) => {
    setPending(p => ({ ...p, [profile]: 'cancel_manual' }))
    try {
      await fetch('/api/trades-command', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action: 'cancel_manual', profile, id }),
      })
    } catch { /* appears on the next poll */ }
  }

  // first free key of the form <base>b, <base>c, ... then <base>2, 3, … (collision-proof default)
  const nextFreeKey = (base: string) => {
    const keys = new Set((profiles || []).map(p => p.profile))
    for (const s of 'bcdefghij') if (!keys.has(base + s)) return base + s
    let n = 2; while (keys.has(base + n)) n++; return base + n
  }
  const keyTaken = (k: string) => (profiles || []).some(p => p.profile === k)
  const cleanKey = (s: string) => s.trim().toUpperCase().replace(/[^A-Z0-9_-]/g, '').slice(0, 24)

  const sendDuplicate = async (profile: string, newProfile: string) => {
    setPending(p => ({ ...p, [profile]: 'duplicate' }))
    try {
      await fetch('/api/trades-command', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action: 'duplicate', profile, newProfile }),
      })
    } catch { /* appears on the next poll */ }
  }
  const sendCreate = async (newProfile: string, newSymbol: string) => {
    setPending(p => ({ ...p, [newProfile]: 'create' }))
    try {
      await fetch('/api/trades-command', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action: 'create', profile: newProfile, newProfile, newSymbol }),
      })
    } catch { /* appears on the next poll */ }
  }
  const sendRename = async (profile: string, newProfile: string, autoDisable = false) => {
    setPending(p => ({ ...p, [profile]: 'rename' }))
    try {
      await fetch('/api/trades-command', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action: 'rename', profile, newProfile, autoDisable }),
      })
    } catch { /* appears on the next poll */ }
  }

  const sendSettings = async (cashBudget?: number, reverseInventory?: Record<string, number>, placeWithinPct?: number, staggerGap?: Record<string, number>, extra?: Record<string, unknown>) => {
    try {
      await fetch('/api/trades-command', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action: 'set_settings', cashBudget, reverseInventory, placeWithinPct, staggerGap, ...(extra || {}) }),
      })
    } catch { /* applies on the next poll */ }
  }
  const flashSaved = (key: string, msg: string) => {
    setSavedMsg(m => ({ ...m, [key]: msg }))
    window.setTimeout(() => setSavedMsg(m => { const n = { ...m }; delete n[key]; return n }), 4000)
  }
  const submitStagger = (profile: string, groupKey: string, raw: string) => {
    setStaggerEditSym(null)
    const g = Number(raw)
    if (!isFinite(g) || g < 0 || g > 100) { flashSaved(profile, '✕ invalid — 0–100 only'); return }
    // groupKey = "SYMBOL:direction" so a ticker's long + short groups are independent.
    // ALWAYS send (mini merge is idempotent) so a stale local value can't drop the edit;
    // skip only the confirmation flash when it matches what we're already showing.
    const cur = (profiles || []).find(p => p.profile === profile)?.staggerGap ?? null
    sendSettings(undefined, undefined, undefined, { [groupKey]: g })
    if (cur == null || g !== cur) flashSaved(profile, `✓ gap ${g}% queued · applies ~15s`)
  }
  // Effective tier = optimistic value (held until the server catches up) over the server value.
  const effTier = (p: Profile) => (p.profile in tierOpt ? tierOpt[p.profile] : (p.staggerTier ?? 0))
  // Setting a tier is a REORDER: move this profile to the requested position among its
  // participating siblings and renumber everyone contiguously (1..N) — so tiers stay UNIQUE and
  // WYSIWYG (a raw value would only be a sort hint; ties/gaps wouldn't match what you typed).
  // Optimistic local tiers keep rapid successive edits consistent (each builds on the last, not on
  // the stale 15s poll) and update the display instantly.
  const submitTier = async (profile: string, raw: string) => {
    setTierEditKey(null)
    const want = Math.round(Number(raw))
    if (!isFinite(want) || want < 1) { flashSaved(profile, '✕ invalid tier'); return }
    const me = (profiles || []).find(p => p.profile === profile)
    if (!me) return
    if (want === effTier(me)) return  // unchanged — close silently, queue nothing
    const sibs = (profiles || [])
      .filter(p => p.symbol === me.symbol && p.direction === me.direction && effTier(p) >= 1)
      .sort((a, b) => effTier(a) - effTier(b))
    const order = sibs.map(p => p.profile).filter(k => k !== profile)
    const idx = Math.min(Math.max(want, 1), order.length + 1) - 1
    order.splice(idx, 0, profile)  // insert at the desired position
    const newTiers: Record<string, number> = {}
    order.forEach((k, i) => { newTiers[k] = i + 1 })
    setTierOpt(prev => ({ ...prev, ...newTiers }))  // optimistic: reflect immediately + persist
    flashSaved(profile, `✓ tier ${idx + 1} queued · applies ~15s`)
    for (const k of order) {  // send only the profiles whose tier changed, sequentially
      if ((sibs.find(p => p.profile === k) ? effTier(sibs.find(p => p.profile === k)!) : null) !== newTiers[k])
        await sendCommand(k, 'set_config', undefined, { stagger_tier: newTiers[k] })
    }
  }
  const submitStepGap = (profile: string, raw: string) => {
    setGapEditKey(null)
    const g = Number(raw)
    if (!isFinite(g) || g < 0 || g > 100) { flashSaved(profile, '✕ invalid — 0–100 only'); return }
    // ALWAYS send (idempotent on the mini) so a stale local value can't silently drop the edit;
    // skip only the confirmation flash when it matches what we're already showing.
    const me = (profiles || []).find(p => p.profile === profile)
    const cur = me?.staggerStepGap ?? me?.staggerGap ?? null
    sendCommand(profile, 'set_config', undefined, { stagger_gap: g })  // per-tier override
    if (cur == null || g !== cur) flashSaved(profile, `✓ gap ${g}% queued · applies ~15s`)
  }
  const clearStepGap = (profile: string) => {
    sendCommand(profile, 'set_config', undefined, { stagger_gap: null })  // revert to the group gap
    flashSaved(profile, '✓ gap → group · applies ~15s')
  }
  const setStaggerMembership = (profile: string, include: boolean) => {
    sendCommand(profile, 'set_config', undefined, { stagger_tier: include ? null : 0 })
    flashSaved(profile, include ? '✓ included · applies ~15s' : '✓ excluded · applies ~15s')
  }
  const sendExpiry = async (profile: string, expiry: number | null) => {
    setPending(p => ({ ...p, [profile]: expiry ? 'set_expiry' : 'clear_expiry' }))
    try {
      await fetch('/api/trades-command', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action: expiry ? 'set_expiry' : 'clear_expiry', profile, expiry }),
      })
    } catch { /* applies on the next poll */ }
  }
  const sendFund = async (action: string, payload: { investor?: string; newName?: string; amount?: number; all?: boolean; note?: string; memo?: string; ts_txn?: number }) => {
    setFundMsg('sending…')
    try {
      const r = await fetch('/api/trades-command', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action, ...payload }),
      })
      if (!r.ok) {
        const e = await r.json().catch(() => ({}))
        setFundMsg(`⚠ not sent: ${(e as { error?: string }).error || `HTTP ${r.status}`}`)
        return
      }
      setFundMsg('✓ queued — applies on the next poll (~15s)')
      window.setTimeout(() => setFundMsg(m => (m && m.startsWith('✓') ? null : m)), 8000)
    } catch {
      setFundMsg('⚠ network error — not sent')
    }
  }
  const openBudgetEdit = () => {
    const d: Record<string, string> = {
      cash_budget: settings?.cash_budget != null ? String(settings.cash_budget) : '',
      place_within_pct: settings?.place_within_pct != null ? String(settings.place_within_pct) : '',
    }
    for (const [k, v] of Object.entries(settings?.reverse_inventory || {})) d[`rev_${k}`] = String(v)
    setBudgetDraft(d); setEditBudgets(true)
    // a profile the bot put back in play after its close shows ticked — saving keeps it, unticking drops it
    setPlayDraft([...new Set([...(settings?.constrained_play || []), ...(settings?.constrained_rejoined || [])])])
    setPlayCancelDraft(!!settings?.constrained_cancel)
    const u = settings?.constrained_until
    if (u) {
      const dt = new Date(u * 1000)
      const pad = (n: number) => String(n).padStart(2, '0')
      setPlayUntilDraft(`${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())}T${pad(dt.getHours())}:${pad(dt.getMinutes())}`)
    } else setPlayUntilDraft('')
  }
  const saveBudgets = () => {
    const cash = Number(budgetDraft.cash_budget)
    const place = Number(budgetDraft.place_within_pct)
    const reverseInventory: Record<string, number> = {}
    for (const [k, v] of Object.entries(budgetDraft)) {
      if (!k.startsWith('rev_')) continue
      const n = Number(v)
      if (isFinite(n) && n >= 0) reverseInventory[k.slice(4)] = n
    }
    const untilMs = playUntilDraft ? new Date(playUntilDraft).getTime() : NaN
    sendSettings(
      isFinite(cash) && cash > 0 ? cash : undefined,
      Object.keys(reverseInventory).length ? reverseInventory : undefined,
      isFinite(place) && place > 0 && place <= 100 ? place : undefined,
      undefined,
      {
        constrainedPlay: playDraft,
        constrainedCancel: playCancelDraft,
        constrainedUntil: playDraft.length && isFinite(untilMs) ? Math.floor(untilMs / 1000) : null,
      },
    )
    setEditBudgets(false)
  }

  // Map config edits to/from the mini's override keys. The price field is direction-aware:
  // long profiles cap with max_price (ceiling), short profiles floor with min_price.
  const configFields = (short?: boolean): { key: string; label: string; nullable?: boolean }[] => [
    { key: 'tranche', label: 'Tranche $' },
    { key: 'tranche_scale_pct', label: 'Scale +%/step' },
    { key: 'first_step_pct', label: 'First step %' },
    { key: 'step_pct', label: 'Step %' },
    { key: 'profit_pct', label: 'Profit %' },
    { key: 'max_rungs', label: 'Cap (rungs)' },
    short ? { key: 'min_price', label: 'Min price $', nullable: true }
          : { key: 'max_price', label: 'Max price $', nullable: true },
  ]
  const openEdit = (p: Profile) => {
    const short = p.direction === 'short'
    setEditSym(p.profile)
    const opened = {
      tranche: p.tranche != null ? String(p.tranche) : '',
      tranche_scale_pct: String(p.scalePct ?? 0),
      first_step_pct: p.firstStepPct != null ? String(p.firstStepPct) : '',
      step_pct: p.stepPct != null ? String(p.stepPct) : '',
      profit_pct: p.profitPct != null ? String(p.profitPct) : '',
      max_rungs: p.cap != null ? String(p.cap) : '',
      [short ? 'min_price' : 'max_price']:
        short ? (p.minPrice != null ? String(p.minPrice) : '')
              : (p.maxPrice != null ? String(p.maxPrice) : ''),
    }
    setDraft(opened)
    setDraftBase(opened)   // anything differing from this is an unsaved edit
  }

  // Which panel fields you actually changed since it opened (by label, for the prompts).
  const changedFields = (short: boolean) =>
    configFields(short).filter(f => (draft[f.key] ?? '') !== (draftBase[f.key] ?? '')).map(f => f.label)

  // Appended to an action's confirm so it's explicit that your edits ride along with it.
  const pendingNote = (short: boolean) => {
    const c = changedFields(short)
    return c.length ? `\n\nYour unsaved edits (${c.join(', ')}) will be saved first.` : ''
  }

  // Queue the panel's pending field edits, then return the labels saved ([] if nothing changed).
  // Mirrors saveEdit's parsing but without its confirm — the action's own prompt covers it.
  const commitEdits = async (profileKey: string, short: boolean) => {
    const changed = changedFields(short)
    if (!changed.length) return []
    const cfg: Record<string, number | null> = {}
    for (const { key, nullable } of configFields(short)) {
      const s = (draft[key] ?? '').trim()
      if (s === '') { if (nullable) cfg[key] = null; continue }
      const n = Number(s)
      if (!isFinite(n) || n < 0) continue
      cfg[key] = n
    }
    if (!Object.keys(cfg).length) return []
    await sendCommand(profileKey, 'set_config', undefined, cfg)
    return changed
  }

  // Run an edit-panel action, committing pending field edits FIRST so clicking Enable / Freeze /
  // Duplicate / ... never silently discards what you typed (it used to: the draft lives in local
  // state and only Save transmitted it). The two commands are AWAITED in sequence — the command
  // queue is a read-modify-write of one JSON file, so firing both at once can drop one.
  const runPanelAction = async (p: Profile, label: string, act: () => void) => {
    const saved = await commitEdits(p.profile, p.direction === 'short')
    act()
    setEditSym(null)
    if (saved.length) flashSaved(p.profile, `✓ saved ${saved.join(', ')} + ${label} · applies ~15s`)
  }
  const saveEdit = (profileKey: string) => {
    const short = profiles?.find(pp => pp.profile === profileKey)?.direction === 'short'
    const fields = configFields(short)
    const cfg: Record<string, number | null> = {}
    for (const { key, nullable } of fields) {
      const s = (draft[key] ?? '').trim()
      if (s === '') { if (nullable) cfg[key] = null; continue }
      const n = Number(s)
      if (!isFinite(n) || n < 0) continue
      cfg[key] = n
    }
    if (!confirm(`Update ${profileKey} configuration?\n\n` +
      fields.map(f => `${f.label}: ${cfg[f.key] ?? (f.nullable ? 'none' : '—')}`).join('\n') +
      `\n\nTakes effect on the next poll cycle (~15s).`)) return
    sendCommand(profileKey, 'set_config', undefined, cfg)
    setEditSym(null)
  }
  // MANUAL LOTS row — orders you created for this profile. Rendered apart from Resting buys /
  // Resting sells because a manual lot is LADDER-NEUTRAL: it joins the ledger (closes with the
  // position, blends into the average the close is priced off) but takes no tranche and never
  // moves a rung. "EXTO" mode places nothing — you place it yourself for the overnight session
  // (the Schwab API can't reach it) and the bot adopts the fill.
  const manualRow = (p: Profile) => {
    const short = p.direction === 'short'
    const lots = p.manual || []
    const open = manualEdit === p.profile
    const held = p.manualShares || 0
    // REDUCE = a lot on the CLOSING side (SELL on a long, buy-back on a short). Booked at average
    // cost, so the average and the close don't move. The bot enforces the same rails; checking
    // here just stops you sending one it will reject.
    const reducing = open && manualReduce
    const side = short !== reducing ? 'SELL' : 'BUY'   // add: long BUY / short SELL; reduce: the reverse
    const position = Math.floor(short ? (p.netSold || 0) : (p.shares || 0))
    const reserved = p.reduceReserved || 0
    const maxReduce = Math.max(0, position - reserved - 1)   // the close always keeps >= 1 share
    const closeAt = p.closePrice ?? (short ? p.coverTarget : p.sellTarget) ?? null
    const basisAvg = short ? p.soldAvg : p.avg
    const canReduce = maxReduce >= 1 && closeAt != null && !p.closeHandoff && !p.frozen && !p.shadow
    const qty = Number(manualQty), px = Number(manualPx)
    const basic = isFinite(qty) && qty >= 1 && isFinite(px) && px > 0
    const reduceErr = !reducing || !basic ? null
      : qty > maxReduce
        ? `max ${maxReduce} sh — holds ${position}${reserved ? `, ${reserved} already held for other reduces` : ''}, and the close keeps at least 1`
        : closeAt != null && (short ? px <= closeAt : px >= closeAt)
          ? `at/past the ${short ? 'buy-back' : 'close'} ($${closeAt.toFixed(2)}) — the close would fill first`
          : null
    const valid = basic && !reduceErr
    // a reduce sits on the closing side, so it is marketable in the opposite direction to an add
    const marketable = valid && p.lastPrice != null
      && ((side === 'BUY') ? px >= p.lastPrice : px <= p.lastPrice)
    // CHEAPEST FIRST: a trim takes the cheapest lots (richest sells on a short), books against
    // what those cost, and the average left behind moves — so the close re-prices (up on a long,
    // down on a short) unless a manual close price is pinned.
    let lotCost: number | null = null
    if (reducing && valid) {
      const lotsHeld = p.heldLots || []
      let left = Math.round(qty), c = 0
      for (const [lp, lq] of lotsHeld) { const k = Math.min(lq, left); c += k * lp; left -= k; if (!left) break }
      lotCost = lotsHeld.length && !left ? c : (basisAvg != null ? basisAvg * qty : null)
    }
    const estPnl = lotCost != null ? (short ? lotCost - px * qty : px * qty - lotCost) : null
    const lotPx = lotCost != null ? lotCost / qty : null
    const newAvg = lotCost != null && basisAvg != null && position - qty > 0
      ? (basisAvg * position - lotCost) / (position - qty) : null
    const pct = (p.profitPct ?? 0) / 100
    const newClose = p.closePrice != null ? p.closePrice
      : newAvg != null ? Math.round(newAvg * (short ? 1 - pct : 1 + pct) * 100) / 100 : null
    const badge = (l: ManualLot) => {
      // A lot the bot is still WATCHING went terminal without filling because you edited the
      // order in the broker (an EXTO swap replaces it with a new id) — it is not finished. One
      // you simply cancelled is finished, and must not be dressed up as an edit.
      const edited = !!l.watching && !l.fillQty
      const tone = l.status === 'FILLED' ? 'text-emerald-600'
        : l.status === 'REJECTED' ? 'text-red-500'
        : edited ? 'text-amber-600'
        : MANUAL_DONE.has(l.status) ? 'text-muted-foreground/60' : 'text-sky-600'
      return (
        <span className={`text-[10px] ${tone}`}
          title={edited
            ? 'This order was replaced in the broker (an EXTO swap does that). The bot is tracking the replacement by size, so the lot still counts.'
            : l.error || undefined}>
          {edited ? 'edited in broker' : l.status.toLowerCase()}
        </span>
      )
    }
    return (
      <>
        <span className="text-muted-foreground"
          title={'Your own orders on this profile. Counted in the ledger and closed with the position, but they never take a ladder rung.'
            + (p.manualAnchored && p.ladderRef != null
              ? ` A filled manual lot is this cycle's entry, so the ladder is frozen off it (reference $${p.ladderRef.toFixed(2)}) instead of trailing the market.`
              : '')}>Manual</span>
        <span className="text-right tabular-nums">
          {held ? <>{held} sh{p.manualAvg != null ? ` @ $${p.manualAvg.toFixed(2)}` : ''}</>
            : (p.manualOpen ? <span className="text-muted-foreground">{p.manualOpen} open, none filled</span>
                            : (p.reducedShares ? null : <span className="text-muted-foreground">—</span>))}
          {p.reducedShares ? (
            <span className={held ? 'ml-1.5 text-muted-foreground' : 'text-muted-foreground'}
              title="Shares trimmed by manual reduces this cycle (cheapest lots first)">
              {held ? '· ' : ''}−{p.reducedShares} trimmed
            </span>
          ) : null}
          {pending[p.profile] === 'manual_order' || pending[p.profile] === 'cancel_manual' || pending[p.profile] === 'edit_manual'
            ? <span className="ml-2 text-[10px] text-muted-foreground">applying…</span>
            : !open && (
              <>
                <button type="button" className="ml-2 text-[10px] text-violet-600 hover:underline"
                  onClick={() => {
                    setManualEdit(p.profile); setManualReduce(false); setManualQty('')
                    setManualPx(p.lastPrice != null ? p.lastPrice.toFixed(2) : '')
                  }}
                  title={`Create your own ${short ? 'SELL' : 'BUY'} order for ${profLabel(p)}`}>+ add</button>
                {canReduce && (
                  <button type="button" className="ml-2 text-[10px] text-violet-600 hover:underline"
                    onClick={() => {
                      setManualEdit(p.profile); setManualReduce(true); setManualQty('')
                      setManualPx(p.lastPrice != null ? p.lastPrice.toFixed(2) : '')
                    }}
                    title={`Trim ${profLabel(p)} with your own ${short ? 'buy-back' : 'SELL'} (up to ${maxReduce} sh) — takes the ${short ? 'richest sells' : 'cheapest lots'} first`}>− reduce</button>
                )}
              </>
            )}
        </span>
        {lots.length > 0 && (
          <span className="col-span-2 flex flex-col gap-0.5 text-xs">
            {lots.map(l => (
              <span key={l.id} className="flex items-center justify-between gap-2">
                <span className="tabular-nums">
                  {l.reduce && (
                    <span className="mr-1 rounded-full bg-violet-500/15 px-1.5 py-px text-[9px] font-medium text-violet-600"
                      title={`A reduce: trims the ${short ? 'richest sells' : 'cheapest lots'} first and books against what they ${short ? 'sold for' : 'cost'}, so the average and the ${short ? 'buy-back' : 'close'} move.`}>reduce</span>
                  )}
                  {l.side} {l.qty} @ ${l.price.toFixed(2)}
                  {l.repriceTo != null && (
                    <span className="ml-1 text-sky-600" title="Re-pricing: the old order is being cancelled, and the lot re-places at the new price once that's confirmed with nothing filled. If it filled first, the fill stands.">
                      → ${l.repriceTo.toFixed(2)}
                    </span>
                  )}
                  {l.session === 'EXTO' && (
                    <span className="ml-1 text-[10px] text-amber-600"
                      title="Sitting on the overnight session. EXTO is always a DAY order, so if it doesn't fill it lapses overnight — the bot re-places it on the normal session in the morning, and you can swap it to overnight again.">
                      overnight
                    </span>
                  )}
                  {l.fillQty ? <span className="ml-1 text-muted-foreground">filled {l.fillQty}{l.fillPrice != null ? ` @ $${l.fillPrice.toFixed(2)}` : ''}</span> : null}
                </span>
                <span className="flex items-center gap-2">
                  {badge(l)}
                  {l.error && <span className="text-[10px] text-muted-foreground/70" title={l.error}>why?</span>}
                  {['PENDING', 'WORKING', 'REJECTED'].includes(l.status) && !l.fillQty && !pending[p.profile] && (
                    <button type="button" className="text-[10px] text-violet-600 hover:underline"
                      title={l.status === 'REJECTED' ? 'Re-submit this lot at a new price' : 'Change the limit price — a working order is cancelled and re-placed once the cancel lands'}
                      onClick={() => {
                        const raw = window.prompt(`New limit price for the manual ${l.reduce ? 'reduce ' : ''}${l.side} of ${l.qty} on ${profLabel(p)}`
                          + (l.status === 'REJECTED' ? ' (re-submits it)' : '')
                          + (l.session === 'EXTO' ? '\n\nIt re-places on the normal session — swap it to overnight again after.' : ''),
                          (l.repriceTo ?? l.price).toFixed(2))
                        if (raw == null) return
                        const px = Math.round(parseFloat(raw.replace(/[$,\s]/g, '')) * 100) / 100
                        if (!(px > 0)) { alert('Enter a positive price.'); return }
                        if (px === (l.repriceTo ?? l.price) && l.status !== 'REJECTED') return
                        editManual(p.profile, l.id, px)
                      }}>edit</button>
                  )}
                  {!MANUAL_DONE.has(l.status) && !pending[p.profile] && (
                    <button type="button" className="text-[10px] text-muted-foreground hover:underline"
                      onClick={() => { if (confirm(`Cancel the manual ${l.reduce ? 'reduce ' : ''}${l.side} of ${l.qty} @ $${l.price.toFixed(2)} on ${profLabel(p)}?`
                        + (l.reduce ? `\n\nIts shares go back into the ${short ? 'buy-back' : 'close'}.` : ''))) cancelManual(p.profile, l.id) }}>cancel</button>
                  )}
                </span>
              </span>
            ))}
          </span>
        )}
        {open && (
          <span className="col-span-2 flex flex-col gap-1 rounded border border-border/60 p-1.5 text-xs">
            <span className="flex items-center gap-1.5">
              <span className="text-muted-foreground">{reducing ? `reduce · ${side}` : side}</span>
              <input type="number" inputMode="numeric" step="1" min="1" value={manualQty} autoFocus
                onChange={e => setManualQty(e.target.value)} placeholder="qty"
                className="w-16 rounded border bg-background px-1.5 py-0.5 tabular-nums text-right" />
              <span className="text-muted-foreground">@ $</span>
              <input type="number" inputMode="decimal" step="any" value={manualPx}
                onChange={e => setManualPx(e.target.value)} placeholder="limit"
                className="w-20 rounded border bg-background px-1.5 py-0.5 tabular-nums text-right" />
              <button type="button" disabled={!valid}
                className="text-[10px] text-violet-600 hover:underline disabled:text-muted-foreground/50 disabled:no-underline"
                onClick={() => {
                  if (!valid) return
                  const note = marketable
                    ? `\n\nWARNING: $${px.toFixed(2)} is at/through the market ($${p.lastPrice!.toFixed(2)}) — it will execute immediately, not rest.`
                    : `\n\nFor overnight, swap this order's session to EXTO in the broker once it's placed — the bot follows the replacement and keeps counting it.`
                  if (reducing) {
                    const left = position - reserved - Math.round(qty)
                    if (!confirm(`Reduce ${profLabel(p)}: ${side} ${qty} ${p.symbol} @ $${px.toFixed(2)}?\n\n`
                      + `Trims the ${short ? 'RICHEST sells' : 'CHEAPEST lots'} first`
                      + (estPnl != null && lotPx != null
                        ? `: about ${estPnl >= 0 ? '+' : '−'}$${Math.abs(estPnl).toFixed(2)} realized `
                          + `(${short ? `$${lotPx.toFixed(2)} sold − $${px.toFixed(2)}` : `$${px.toFixed(2)} − $${lotPx.toFixed(2)} paid`} × ${qty}).`
                        : '.')
                      + (newAvg != null
                        ? ` The ${short ? 'sold' : ''} average moves $${basisAvg!.toFixed(2)} → $${newAvg.toFixed(2)}, so the `
                          + `${short ? 'buy-back' : 'close'} ${p.closePrice != null ? `stays pinned at $${p.closePrice.toFixed(2)}` : `moves $${closeAt!.toFixed(2)} → $${newClose!.toFixed(2)}`} once it fills.`
                        : '')
                      + ` A rung sold off entirely opens up to be bought again.\n\n`
                      + `While it rests, the ${short ? 'buy-back' : 'close'} shrinks to ${left} sh to free these shares; the bot places `
                      + `the reduce a poll after that. If the ${short ? 'buy-back' : 'close'} price moves through it, the reduce is `
                      + `cancelled and the ${short ? 'buy-back' : 'close'} takes the shares back.`
                      + note)) return
                    sendManual(p.profile, side, px, Math.round(qty))
                    setManualEdit(null)
                    return
                  }
                  const anchors = (p.filledCount || 0) === 0 && !p.manualAnchored
                  if (!confirm(`Manual ${side} ${qty} ${p.symbol} @ $${px.toFixed(2)} on ${profLabel(p)}?\n\n`
                    + `It joins this profile's ledger — closed with the position, blended into the average the `
                    + `${short ? 'buy-back' : 'sell'} is priced off — and takes NO tranche.\n\n`
                    + (anchors
                      ? `No tranche has filled, so this fill becomes the cycle's ENTRY: the ladder stops trailing `
                        + `the market and anchors on your fill price, with the first rung one step `
                        + `${short ? 'above' : 'below'} it.`
                      : `The ladder is already anchored, so the resting ${short ? 'sells' : 'buys'} stay exactly `
                        + `where they are.`)
                    + note)) return
                  sendManual(p.profile, side, px, Math.round(qty))
                  setManualEdit(null)
                }}>place</button>
              <button type="button" className="text-[10px] text-muted-foreground hover:underline"
                onClick={() => setManualEdit(null)}>cancel</button>
            </span>
            {reduceErr && <span className="text-[10px] text-red-500">{reduceErr}</span>}
            {reducing && !reduceErr && (
              <span className="text-[10px] text-muted-foreground">
                up to {maxReduce} sh · {short ? 'above the buy-back' : 'below the close'} {closeAt != null ? `$${closeAt.toFixed(2)}` : ''}
                {estPnl != null ? ` · ≈ ${estPnl >= 0 ? '+' : '−'}$${Math.abs(estPnl).toFixed(2)} vs the ${short ? 'richest sells' : 'cheapest lots'}` : ''}
                {newClose != null && closeAt != null && p.closePrice == null ? ` · ${short ? 'buy-back' : 'close'} → $${newClose.toFixed(2)}` : ''}
              </span>
            )}
            {marketable && (
              <span className="text-[10px] text-amber-600">at/through the market — this will fill now, not rest</span>
            )}
          </span>
        )}
      </>
    )
  }
  // Manual close-out price control shown on the sell/buy-back target row.
  const closeControl = (p: Profile) => {
    const short = p.direction === 'short'
    if (closeEdit === p.profile) {
      const px = Number(closeDraft)
      const mkt = p.lastPrice
      const immediate = isFinite(px) && px > 0 && mkt != null && (short ? px <= mkt : px >= mkt)
      return (
        <span className="inline-flex flex-col items-end gap-0.5 ml-2 align-top">
          <span className="inline-flex items-center gap-1">
            $<input type="number" inputMode="decimal" step="any" value={closeDraft} autoFocus
              onChange={e => setCloseDraft(e.target.value)}
              className="w-20 rounded border bg-background px-1.5 py-0.5 tabular-nums text-right text-xs" />
            <button type="button" className="text-[10px] text-violet-600 hover:underline"
              onClick={() => { if (isFinite(px) && px > 0) { sendCommand(p.profile, 'set_close', px); setCloseEdit(null) } }}>set</button>
            <button type="button" className="text-[10px] text-muted-foreground hover:underline"
              onClick={() => setCloseEdit(null)}>✕</button>
          </span>
          {immediate && <span className="text-[9px] text-amber-600">⚠ fills now (market {short ? 'buy-back' : 'sell'})</span>}
        </span>
      )
    }
    if (p.closePrice != null) {
      return (
        <span className="ml-2 text-[10px] whitespace-nowrap">
          <button type="button" className="text-violet-600 hover:underline"
            title={`Manual close override at $${p.closePrice.toFixed(2)} — click to change it`}
            onClick={() => { setCloseEdit(p.profile); setCloseDraft(String(p.closePrice)) }}>manual</button>
          <span className="text-muted-foreground"> · </span>
          <button type="button" className="text-violet-600 hover:underline"
            title="Clear the override and return to the profit-target formula"
            onClick={() => { if (confirm(`Clear the manual close price on ${profLabel(p)} ($${p.closePrice!.toFixed(2)}) and return to the ${short ? 'buy-back' : 'sell'} target formula?`)) sendCommand(p.profile, 'clear_close') }}>clear</button>
        </span>
      )
    }
    return (
      <button type="button" className="ml-2 text-[10px] text-muted-foreground hover:text-violet-600 hover:underline"
        title="Set a manual close-out price for this position (pinned until it fills or a new rung fills)"
        onClick={() => { setCloseEdit(p.profile); setCloseDraft(String((short ? p.coverTarget : p.sellTarget) ?? p.lastPrice ?? '')) }}>set</button>
    )
  }
  const [txnSym, setTxnSym] = useState<string | null>(null)
  const [txnAll, setTxnAll] = useState(false)  // show every txn vs the capped initial set
  // Transaction history is fetched per profile, only while its panel is open. The 15s poll
  // used to carry all 24 ledgers (~110KB of a 181KB response) to render at most one of them.
  const [txnData, setTxnData] = useState<Record<string, Txn[]>>({})
  const [txnBusy, setTxnBusy] = useState(false)
  const txnLoaded = useRef<Set<string>>(new Set())
  const [staggerEditSym, setStaggerEditSym] = useState<string | null>(null)  // ticker whose stagger gap is being edited
  const [tierEditKey, setTierEditKey] = useState<string | null>(null)  // profile whose stagger tier is being edited
  const [gapEditKey, setGapEditKey] = useState<string | null>(null)  // profile whose per-tier step gap is being edited
  const [tierOpt, setTierOpt] = useState<Record<string, number>>({})  // optimistic tiers, held until the server catches up
  const [ingestedAt, setIngestedAt] = useState<number | null>(null)
  const [now, setNow] = useState(() => Date.now())
  const [err, setErr] = useState<string | null>(null)
  const [flash, setFlash] = useState<string | null>(null)  // profile key to briefly highlight after a jump
  const [showDiag, setShowDiag] = useState(false)
  // The investors card starts closed. It is the longest block on the page — one
  // row per investor, each with its own history and note — and it is reference
  // material, not something being watched: the numbers that matter while the bot
  // is running are above it. Collapsed by default, with the fund's two headline
  // figures left on the header so closing it costs nothing at a glance.
  const [showInvestors, setShowInvestors] = useState(false)

  // "c" folds the whole board back up. With a dozen panels and a card expansion per profile,
  // getting back to a clean view otherwise means hunting down every toggle you opened.
  const collapseAll = useCallback(() => {
    setShowHistory(false); setShowAnalytics(false); setShowCapital(false)
    setShowEquity(false); setShowUnreal(false); setShowPotential(false)
    setShowDisabled(false); setShowDiag(false); setShowInvestors(false)
    setExpandedSym(null); setTxnSym(null); setFundHist(null)
    setTxnAll(false); setHistLimit(15); setDayLimit(14)
    window.scrollTo({ top: 0, behavior: 'smooth' })
  }, [])

  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (e.key !== 'c' && e.key !== 'C') return
      // Never swallow Cmd/Ctrl+C, and never fire while a field has focus — the ticker filter
      // and the note editors are both places where "c" is just a letter.
      if (e.metaKey || e.ctrlKey || e.altKey) return
      const t = e.target as HTMLElement | null
      if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return
      e.preventDefault()
      collapseAll()
    }
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
  }, [collapseAll])
  const [fundForm, setFundForm] = useState<{ mode: 'new' | 'deposit' | 'withdraw' | 'base'; investor?: string } | null>(null)
  const [fundName, setFundName] = useState('')
  const [fundAmt, setFundAmt] = useState('')
  const [fundMsg, setFundMsg] = useState<string | null>(null)
  const [fundMemo, setFundMemo] = useState('')      // memo for the deposit/withdrawal being entered
  const [txnEdit, setTxnEdit] = useState<string | null>(null)   // "<investor>:<ts>" being memo-edited
  const [txnMemo, setTxnMemo] = useState('')
  const [noteEdit, setNoteEdit] = useState<string | null>(null)   // investor whose note is open
  const [noteDraft, setNoteDraft] = useState('')
  const [nameEdit, setNameEdit] = useState<string | null>(null)   // investor being renamed
  const [nameDraft, setNameDraft] = useState('')

  // Scroll to a profile card (by key) and flash it — used by history rows + summary references.
  const scrollToProfile = (key?: string) => {
    if (!key) return
    const el = document.getElementById(`profile-${key}`)
    if (!el) return
    el.scrollIntoView({ behavior: 'smooth', block: 'center' })
    setFlash(key)
    window.setTimeout(() => setFlash(f => (f === key ? null : f)), 1600)
  }
  // Render a list of profiles as comma-joined clickable links that jump to each card.
  const profLinks = (list: { symbol: string; profile?: string }[], cls: string) =>
    list.map((p, i) => (
      <span key={p.profile ?? i}>
        {i > 0 ? ', ' : ''}
        <button type="button" onClick={() => scrollToProfile(p.profile)} className={`${cls} hover:underline`}>{profLabel(p)}</button>
      </span>
    ))

  useEffect(() => {
    let active = true
    const load = async () => {
      try {
        const r = await fetch('/api/trades', { cache: 'no-store' })
        if (!r.ok) throw new Error(r.status === 401 ? 'Not logged in' : `HTTP ${r.status}`)
        const j = await r.json()
        if (!active) return
        const ps: Profile[] = j.profiles || []
        setProfiles(ps)
        // drop optimistic tiers the server has caught up to (keeps the rest sticky)
        setTierOpt(prev => {
          if (!Object.keys(prev).length) return prev
          const next = { ...prev }
          for (const p of ps) if (p.profile in next && p.staggerTier === next[p.profile]) delete next[p.profile]
          return next
        })
        setPlays(j.plays || [])
        setAnalytics(j.analytics || null)
        setCapital(Array.isArray(j.capital) ? j.capital : [])
        setSettings(j.settings || null)
        setFund(j.fund || null)
        setToken(j.token || null)
        setIngestedAt(j.ingestedAt || null)
        setNow(j.now || Date.now())
        setErr(j.error || null)
        // clear optimistic "pending" once the mini has applied the command
        setPending(prev => {
          if (!Object.keys(prev).length) return prev
          const next = { ...prev }
          for (const p of ps) {
            const act = next[p.profile]
            if (!act) continue
            const paused = (!!p.pausedUntil && p.pausedUntil * 1000 > Date.now()) || !!p.pauseHold
            if ((act === 'pause' || act === 'await_manual') && paused) delete next[p.profile]
            if ((act === 'resume' || act === 'cancel_await') && !paused) delete next[p.profile]
            // config edits / anchor reset / enable-disable land within a cycle; clear on next load
            if (act === 'set_config' || act === 'clear_config' || act === 'reset_anchor'
                || act === 'disable' || act === 'enable' || act === 'duplicate' || act === 'rename'
                || act === 'freeze' || act === 'unfreeze' || act === 'set_close' || act === 'clear_close'
                || act === 'handoff_close' || act === 'cancel_handoff' || act === 'reclaim_close'
                || act === 'set_expiry' || act === 'clear_expiry'
                || act === 'manual_order' || act === 'cancel_manual' || act === 'edit_manual') delete next[p.profile]
          }
          return next
        })
      } catch (e) {
        if (active) setErr((e as Error).message)
      }
    }
    load()
    const id = setInterval(load, 15000)
    return () => { active = false; clearInterval(id) }
  }, [])

  // Fetch the open profile's transactions, and keep them fresh on the same 15s beat so a new
  // fill still appears within a cycle in the list you are actually looking at. A closed panel
  // costs nothing. Cached per profile, so re-opening one is instant and only the refresh is live.
  useEffect(() => {
    const sym = txnSym
    if (!sym) return
    let active = true
    setTxnBusy(!txnLoaded.current.has(sym))
    const loadTxns = async () => {
      try {
        const r = await fetch(`/api/trades/${encodeURIComponent(sym)}/transactions`, { cache: 'no-store' })
        if (!r.ok) return
        const j = await r.json()
        if (!active) return
        txnLoaded.current.add(sym)
        setTxnData(d => ({ ...d, [sym]: j.transactions || [] }))
      } catch {
        /* keep whatever is cached; the next tick retries */
      } finally {
        if (active) setTxnBusy(false)
      }
    }
    loadTxns()
    const id = setInterval(loadTxns, 15000)
    return () => { active = false; clearInterval(id) }
  }, [txnSym])

  if (err && !profiles) return <p className="text-sm text-red-500 px-1">{err}</p>
  if (!profiles) return <p className="text-sm text-muted-foreground px-1">Loading…</p>
  if (profiles.length === 0)
    return <p className="text-sm text-muted-foreground px-1">No data yet{err ? ` (${err})` : ''}.</p>

  const sum = (f: (p: Profile) => number) => profiles.reduce((a, p) => a + (f(p) || 0), 0)
  const totalRealized = sum(p => p.realizedPnl)
  const totalUnreal = sum(p => p.unrealizedPnl)
  const holding = profiles.filter(p => p.phase === 'HOLDING' && p.shares > 0)
  // Anything with a position ON — long shares held, or short shares distributed. `holding`
  // above is the LONG set and drives equity value; this is the honest "in play" count, which
  // was reading 13 while TSLA_R had 104 shares out.
  const inPlay = profiles.filter(p => p.shares > 0 || (p.netSold || 0) > 0)
  // Current market value of all open positions (shares × last price).
  const equityValue = holding.reduce((a, p) => a + (p.lastPrice != null ? p.shares * p.lastPrice : 0), 0)
  // P&L if every open position's sell target filled right now.
  // Longs close by SELLING above their average; covered shorts close by BUYING BACK below
  // the price they sold at. Counting only the long side understated this by the whole short
  // book — TSLA_R alone has $397 resting on its buy-back.
  const potential = profiles.reduce((a, p) => {
    if (p.shares > 0 && p.sellTarget != null && p.avg != null) return a + p.shares * (p.sellTarget - p.avg)
    if ((p.netSold || 0) > 0 && p.coverTarget != null && p.soldAvg != null) return a + p.netSold! * (p.soldAvg - p.coverTarget)
    return a
  }, 0)
  // Most recent completed sale across all profiles.
  const sold = profiles.filter(p => p.lastSell).sort((a, b) => b.lastSell!.ts - a.lastSell!.ts)[0]
  const recent = sold ? { sym: sold.symbol, key: sold.profile, sell: sold.lastSell! } : null
  const stale = ingestedAt != null && now - ingestedAt > 120_000
  const tokenDaysLeft = token ? (token.expiresTs * 1000 - now) / 86400000 : null

  // Automated health check — codifies the manual triage (freshness/hung, token, halts, ledger
  // reconciliation, per-ticker oversell, budget, profile states) against the live pushed data.
  type Diag = { level: 'crit' | 'warn' | 'info' | 'ok'; label: string; detail?: string }
  const runDiagnostics = (): Diag[] => {
    const out: Diag[] = []
    // 1. Data freshness (a stale push = the mini stopped pushing — down or hung)
    const ageSec = ingestedAt != null ? (now - ingestedAt) / 1000 : null
    if (ageSec == null) out.push({ level: 'warn', label: 'No data pushed yet' })
    else if (ageSec > 300) out.push({ level: 'crit', label: 'Data stale', detail: `last push ${Math.round(ageSec / 60)}m ago — the mini may be down or hung` })
    else if (ageSec > 90) out.push({ level: 'warn', label: 'Data slightly stale', detail: `last push ${Math.round(ageSec)}s ago` })
    else out.push({ level: 'ok', label: 'Data fresh', detail: `pushed ${Math.round(ageSec)}s ago` })
    // 2. Schwab token
    if (tokenDaysLeft != null) {
      if (tokenDaysLeft <= 0) out.push({ level: 'crit', label: 'Schwab token EXPIRED', detail: 'bot cannot trade — run ./reauth-remote.sh' })
      else if (tokenDaysLeft < 2) out.push({ level: 'warn', label: 'Schwab token expiring', detail: `${tokenDaysLeft.toFixed(1)}d left — re-auth soon` })
      else out.push({ level: 'ok', label: 'Schwab token', detail: `${tokenDaysLeft.toFixed(1)}d left` })
    }
    // 3. Halted profiles
    const halted = profiles.filter(p => p.halted)
    if (halted.length) out.push({ level: 'crit', label: `${halted.length} profile(s) HALTED`, detail: `${halted.map(profLabel).join(', ')} — investigate, then restart the bot` })
    else out.push({ level: 'ok', label: 'No halted profiles' })
    // 4. Ledger reconciliation (per profile)
    const diverged = profiles.filter(p => p.recon && !p.recon.match)
    if (diverged.length) out.push({ level: 'crit', label: `${diverged.length} ledger divergence(s)`, detail: diverged.map(p => `${profLabel(p)} Δ${p.recon!.dShares}sh`).join(', ') })
    else if (profiles.some(p => p.recon)) out.push({ level: 'ok', label: 'Ledgers reconcile with broker' })
    // 5. Per-ticker oversell (Σ ledgers > broker) — the guardrail's own signal
    const over = profiles.filter(p => p.recon && p.recon.symSigma != null && p.recon.symBroker != null && p.recon.symSigma > p.recon.symBroker + 0.5)
    const overSyms = [...new Set(over.map(p => p.symbol))]
    if (overSyms.length) out.push({ level: 'crit', label: 'Σ ledgers exceed broker (oversell risk)', detail: overSyms.map(s => { const p = over.find(x => x.symbol === s)!; return `${s}: Σ${p.recon!.symSigma} > ${p.recon!.symBroker}` }).join(', ') })
    // 5b. Ladders with every rung filled — no dry powder left, they can only wait for the bounce
    const exhausted = profiles.filter(p => p.cap && ((p.netSold || 0) > 0 ? (p.soldRungs || 0) : p.filledCount) >= p.cap && (p.shares || p.netSold))
    if (exhausted.length) {
      const tied = exhausted.reduce((a, p) => a + ((p.netSold || 0) > 0 ? p.netSold! * (p.soldAvg || 0) : (p.shares || 0) * (p.avg || 0)), 0)
      out.push({
        level: 'warn',
        label: `${exhausted.length} ladder(s) exhausted`,
        detail: `${exhausted.map(p => `${profLabel(p)} ${p.filledCount}/${p.cap}`).join(', ')} — every rung filled, `
          + `$${Math.round(tied).toLocaleString()} committed with nothing left to average down`,
      })
    }
    // 6. On hold (budget)
    const onhold = profiles.filter(p => p.funded === false && !p.notInPlay)   // not-in-play is your picker's choice, not a fault
    if (onhold.length) out.push({ level: 'warn', label: `${onhold.length} on hold (budget)`, detail: `${onhold.map(profLabel).join(', ')} — cash budget exhausted for lower priority` })
    // 7. Intentional states (info)
    const frozen = profiles.filter(p => p.frozen)
    const disabled = profiles.filter(parkedOff)
    const closing = profiles.filter(closingOut)
    if (frozen.length) out.push({ level: 'info', label: `${frozen.length} frozen`, detail: frozen.map(profLabel).join(', ') })
    if (closing.length) out.push({ level: 'info', label: `${closing.length} closing out`, detail: `${closing.map(profLabel).join(', ')} — disabled, selling what they hold; no new opening orders` })
    if (disabled.length) out.push({ level: 'info', label: `${disabled.length} disabled`, detail: disabled.map(profLabel).join(', ') })
    // 8. Budget over-allocation (informational — priority funding handles it)
    if (settings?.cash_budget) {
      const alloc = profiles.filter(p => p.direction !== 'short').reduce((a, p) => {
        const parked = p.disabled || (p.expiry != null && p.expiry * 1000 <= Date.now() && !p.shares)
        return a + (parked ? ((p.shares && p.avg) ? p.shares * p.avg : 0) : (p.maxCapital || 0))
      }, 0)
      const pct = alloc / settings.cash_budget * 100
      if (pct > 100) out.push({ level: 'info', label: 'Config over budget', detail: `ladders allocate ${pct.toFixed(0)}% of the cash budget (priority-funded)` })
    }
    return out
  }
  const diagnostics = showDiag ? runDiagnostics() : []
  const diagCrit = diagnostics.filter(d => d.level === 'crit').length
  const diagWarn = diagnostics.filter(d => d.level === 'warn').length
  const DIAG_TONE: Record<Diag['level'], string> = {
    crit: 'text-red-600', warn: 'text-amber-600', info: 'text-sky-600', ok: 'text-emerald-600',
  }
  const DIAG_DOT: Record<Diag['level'], string> = {
    crit: 'bg-red-500', warn: 'bg-amber-500', info: 'bg-sky-500', ok: 'bg-emerald-500',
  }

  return (
    <div className="space-y-3">
      <div className="flex items-center justify-end">
        <button type="button"
          onClick={() => {
            if (showDiag) { setShowDiag(false); return }
            if (confirm('Run diagnostics on the latest pushed status?\n\nRead-only — it just checks the current data (halts, ledger reconciliation, token, freshness, budget). No orders or changes.')) setShowDiag(true)
          }}
          className="text-[11px] px-2 py-1 rounded-full bg-muted hover:bg-muted/70 text-muted-foreground flex items-center gap-1">
          🩺 {showDiag ? 'Hide diagnostics' : 'Run diagnostics'}
        </button>
      </div>
      {showDiag && (
        <div className="rounded-2xl border bg-muted/30 p-3 text-sm space-y-2">
          <div className="flex items-center justify-between">
            <span className="font-medium">Diagnostics</span>
            <span className={`text-xs ${diagCrit ? 'text-red-600' : diagWarn ? 'text-amber-600' : 'text-emerald-600'}`}>
              {diagCrit ? `${diagCrit} critical` : diagWarn ? `${diagWarn} warning${diagWarn > 1 ? 's' : ''}` : 'all healthy'}
              {(diagCrit > 0 && diagWarn > 0) ? ` · ${diagWarn} warning${diagWarn > 1 ? 's' : ''}` : ''}
            </span>
          </div>
          <div className="space-y-1.5">
            {[...diagnostics].sort((a, b) => ({ crit: 0, warn: 1, info: 2, ok: 3 })[a.level] - ({ crit: 0, warn: 1, info: 2, ok: 3 })[b.level]).map((d, i) => (
              <div key={i} className="flex items-start gap-2">
                <span className={`mt-1.5 w-2 h-2 rounded-full shrink-0 ${DIAG_DOT[d.level]}`} />
                <span className="flex-1">
                  <span className={DIAG_TONE[d.level]}>{d.label}</span>
                  {d.detail && <span className="text-muted-foreground"> — {d.detail}</span>}
                </span>
              </div>
            ))}
          </div>
          <div className="text-[9px] text-muted-foreground">Live check of the last pushed status ({ingestedAt ? `${Math.round((now - ingestedAt) / 1000)}s old` : 'no data'}). Re-runs on each refresh while open.</div>
        </div>
      )}
      {tokenDaysLeft != null && (() => {
        const expired = tokenDaysLeft <= 0
        const urgent = tokenDaysLeft < 2
        const tone = expired ? 'border-red-500/50 bg-red-500/10 text-red-600'
          : urgent ? 'border-amber-500/50 bg-amber-500/10 text-amber-700'
          : 'border-border bg-muted/30 text-muted-foreground'
        return (
          <div className={`rounded-2xl border px-3 py-2 text-xs flex items-center justify-between gap-2 ${tone}`}
            title="Schwab refresh tokens expire 7 days after the last auth. If it lapses the bot can't re-authenticate and stalls.">
            <span>
              {expired ? '⚠ Schwab token EXPIRED — the bot can\'t re-auth or trade until you refresh'
                : urgent ? `⚠ Schwab token expires in ${tokenDaysLeft.toFixed(1)}d — re-auth soon`
                : `Schwab token: ${tokenDaysLeft.toFixed(1)}d left`}
            </span>
            {(expired || urgent) && <code className="font-mono opacity-80 shrink-0">./reauth-remote.sh</code>}
          </div>
        )
      })()}
      <div className="rounded-2xl border bg-muted/30 p-3 space-y-1 text-sm">
        <div className="flex items-center justify-between text-xs text-muted-foreground">
          <span>
            updated <span className={stale ? 'text-amber-600' : ''}>{ago(ingestedAt, now)}{stale ? ' — stale' : ''}</span>
            <button type="button" onClick={collapseAll}
              title="Collapse every open panel and card, and scroll back to the top (shortcut: c)"
              className="ml-1.5 text-[10px] px-1 rounded border border-border/60 hover:text-foreground hover:border-foreground/40">c</button>
          </span>
          <span>
            {profiles.filter(p => !p.disabled).length} active · {inPlay.length} in play
            {(() => {
              // Only the ones actually hidden are offered — a never-run profile is on screen
              // already, so counting it here would point at nothing.
              const hidden = profiles.filter(p => parkedOff(p) && !neverRan(p))
              if (!hidden.length) return null
              return (
                <button type="button" onClick={() => setShowDisabled(v => !v)}
                  className="ml-1.5 text-amber-700 hover:underline"
                  title={`${hidden.map(p => p.profile).join(', ')} — parked: no new opening orders, and their ladders are already excluded from the allocation total`}>
                  · {hidden.length} disabled ({showDisabled ? 'hide' : 'show'})
                </button>
              )
            })()}
          </span>
        </div>
        <div className="flex items-center justify-between">
          <button type="button" onClick={() => setShowEquity(v => !v)}
            className="text-muted-foreground hover:text-foreground text-left"
            title="Where this money is sitting, by ticker">
            Equity value (open) <span className="text-[10px]">({showEquity ? 'hide' : 'breakdown'})</span>
          </button>
          <button type="button" onClick={() => setShowEquity(v => !v)} className="tabular-nums hover:underline decoration-dotted underline-offset-2">
            ${equityValue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
          </button>
        </div>
        {showEquity && (() => {
          // Equity is reported as one number but lives in a dozen places. Grouped by TICKER,
          // since that is the unit of exposure — four INTC ladders are one bet on INTC — with
          // each one's profiles named so a concentrated position is obvious rather than implied.
          const byTicker = new Map<string, { value: number; unreal: number; shares: number; names: string[] }>()
          for (const p of holding) {
            const v = p.lastPrice != null ? p.shares * p.lastPrice : 0
            const g = byTicker.get(p.symbol) ?? { value: 0, unreal: 0, shares: 0, names: [] }
            g.value += v; g.unreal += p.unrealizedPnl || 0; g.shares += p.shares
            g.names.push(p.profile)
            byTicker.set(p.symbol, g)
          }
          const rows = [...byTicker.entries()].sort((a, b) => b[1].value - a[1].value)
          return (
            <div className="ml-2 pl-2 border-l-2 border-border/40 space-y-0.5 py-1">
              {rows.map(([sym, g]) => (
                <div key={sym} className="flex items-center justify-between text-xs gap-2">
                  <span className="truncate">
                    <button type="button" onClick={() => scrollToProfile(sym)}
                      className="font-medium hover:underline" title={`Jump to ${sym}`}>{sym}</button>
                    {g.names.length > 1 && <span className="text-muted-foreground"> ×{g.names.length}</span>}
                    <span className="text-muted-foreground"> · {g.shares.toLocaleString()} sh</span>
                  </span>
                  <span className="tabular-nums shrink-0 flex items-center gap-2">
                    <span className="text-muted-foreground text-[10px]">{(g.value / equityValue * 100).toFixed(0)}%</span>
                    <span>${Math.round(g.value).toLocaleString()}</span>
                    <span className="w-16 text-right"><Money n={g.unreal} /></span>
                  </span>
                </div>
              ))}
              <div className="flex items-center justify-between text-[10px] text-muted-foreground pt-0.5 border-t border-border/40">
                <span>
                  {rows.length} ticker(s) across {holding.length} long position(s)
                  {profiles.some(p => (p.netSold || 0) > 0) && (
                    <span title="A covered short sells shares the account already owns, turning equity into cash rather than adding to it — so it is not equity value. Its exposure shows in Unrealized and Potential.">
                      {' '}· shorts excluded
                    </span>
                  )}
                </span>
                <span className="tabular-nums">largest {rows.length ? `${rows[0][0]} ${(rows[0][1].value / equityValue * 100).toFixed(0)}%` : '—'}</span>
              </div>
            </div>
          )
        })()}
        <div className="flex items-center justify-between">
          <button type="button" onClick={() => setShowUnreal(v => !v)}
            className="text-muted-foreground hover:text-foreground text-left"
            title="Which positions this is coming from, worst first">
            Unrealized (open) <span className="text-[10px]">({showUnreal ? 'hide' : 'breakdown'})</span>
          </button>
          <button type="button" onClick={() => setShowUnreal(v => !v)} className="tabular-nums hover:underline decoration-dotted underline-offset-2">
            <Money n={totalUnreal} />
          </button>
        </div>
        {showUnreal && (() => {
          // Worst first — a net figure hides its own shape. The same -$3,189 could be one
          // position deeply under or a dozen barely so, and those want different responses.
          // By ticker, like the equity breakdown, since that is the unit of exposure.
          const byTicker = new Map<string, { unreal: number; cost: number; shares: number; names: string[]; maxed: boolean }>()
          for (const p of profiles) {
            const u = p.unrealizedPnl || 0
            // A covered short holds netSold against soldAvg, not shares against avg. Gating on
            // p.shares dropped the whole short book from a figure the header still counted:
            // the breakdown summed -$2,357 under a header reading -$3,175.
            const short = (p.netSold || 0) > 0
            const qty = short ? p.netSold! : p.shares
            const basis = short ? (p.soldAvg || 0) : (p.avg || 0)
            if (!qty || !u) continue
            // Long and short stay separate rows. They are opposite exposures on one ticker, so
            // netting TSLA_TIME against TSLA_R would report a smaller number than either.
            const key = short ? `${p.symbol} short` : p.symbol
            const g = byTicker.get(key) ?? { unreal: 0, cost: 0, shares: 0, names: [], maxed: false }
            g.unreal += u; g.cost += qty * basis; g.shares += qty
            g.names.push(p.profile)
            const filled = short ? (p.soldRungs || 0) : p.filledCount
            if (p.cap && filled >= p.cap) g.maxed = true
            byTicker.set(key, g)
          }
          const rows = [...byTicker.entries()].sort((a, b) => a[1].unreal - b[1].unreal)
          const losers = rows.filter(r => r[1].unreal < 0)
          return (
            <div className="ml-2 pl-2 border-l-2 border-border/40 space-y-0.5 py-1">
              {rows.map(([sym, g]) => (
                <div key={sym} className="flex items-center justify-between text-xs gap-2">
                  <span className="truncate">
                    <button type="button" onClick={() => scrollToProfile(sym)}
                      className="font-medium hover:underline" title={`Jump to ${sym}`}>{sym}</button>
                    {g.names.length > 1 && <span className="text-muted-foreground"> ×{g.names.length}</span>}
                    {g.maxed && <span className="ml-1 text-[9px] text-amber-600" title="Ladder maxed — no rungs left to average down with">maxed</span>}
                    <span className="text-muted-foreground"> · ${Math.round(g.cost).toLocaleString()} in</span>
                  </span>
                  <span className="tabular-nums shrink-0 flex items-center gap-2">
                    <span className="text-muted-foreground text-[10px]">{g.cost ? `${(g.unreal / g.cost * 100).toFixed(1)}%` : '—'}</span>
                    <span className="w-20 text-right"><Money n={g.unreal} /></span>
                  </span>
                </div>
              ))}
              <div className="flex items-center justify-between text-[10px] text-muted-foreground pt-0.5 border-t border-border/40">
                <span>{losers.length} of {rows.length} ticker(s) under water</span>
                <span className="tabular-nums">
                  {losers.length ? `worst ${losers[0][0]} ${(losers[0][1].unreal / totalUnreal * 100).toFixed(0)}% of the loss` : 'none under water'}
                </span>
              </div>
            </div>
          )
        })()}
        <div className="flex items-center justify-between">
          <button type="button" onClick={() => setShowPotential(v => !v)}
            className="text-muted-foreground hover:text-foreground text-left"
            title="Which positions it comes from, and how far each target is">
            Potential (if sells hit) <span className="text-[10px]">({showPotential ? 'hide' : 'breakdown'})</span>
          </button>
          <button type="button" onClick={() => setShowPotential(v => !v)} className="tabular-nums hover:underline decoration-dotted underline-offset-2">
            <Money n={potential} />
          </button>
        </div>
        {showPotential && (() => {
          // Biggest first, with the DISTANCE to each target alongside. The payoff alone is only
          // half of it: $400 that needs a 13% rally is not the same prospect as $400 that needs
          // 1%, and the total quietly mixes the two.
          const byTicker = new Map<string, { gain: number; shares: number; names: string[]; need: number | null; maxed: boolean }>()
          for (const p of profiles) {
            const short = (p.netSold || 0) > 0
            const hasLong = p.shares > 0 && p.sellTarget != null && p.avg != null
            const hasShort = short && p.coverTarget != null && p.soldAvg != null
            if (!hasLong && !hasShort) continue
            const key = short ? `${p.symbol} short` : p.symbol
            const g = byTicker.get(key) ?? { gain: 0, shares: 0, names: [], need: null, maxed: false }
            // A short profits as price FALLS to its buy-back, so the distance to its target is
            // negative where a long's is positive. Signing it this way keeps "how far to go"
            // readable on one axis instead of silently inverting for half the rows.
            g.gain += hasShort ? p.netSold! * (p.soldAvg! - p.coverTarget!) : p.shares * (p.sellTarget! - p.avg!)
            g.shares += hasShort ? p.netSold! : p.shares
            g.names.push(p.profile)
            const tgt = hasShort ? p.coverTarget! : p.sellTarget!
            const need = p.lastPrice ? (tgt / p.lastPrice - 1) * 100 : null
            if (need != null) g.need = g.need == null ? need : (hasShort ? Math.min(g.need, need) : Math.max(g.need, need))
            const filled = short ? (p.soldRungs || 0) : p.filledCount
            if (p.cap && filled >= p.cap) g.maxed = true
            byTicker.set(key, g)
          }
          const rows = [...byTicker.entries()].sort((a, b) => b[1].gain - a[1].gain)
          return (
            <div className="ml-2 pl-2 border-l-2 border-border/40 space-y-0.5 py-1">
              {rows.map(([sym, g]) => (
                <div key={sym} className="flex items-center justify-between text-xs gap-2">
                  <span className="truncate">
                    <button type="button" onClick={() => scrollToProfile(sym)}
                      className="font-medium hover:underline" title={`Jump to ${sym}`}>{sym}</button>
                    {g.names.length > 1 && <span className="text-muted-foreground"> ×{g.names.length}</span>}
                    {g.maxed && <span className="ml-1 text-[9px] text-amber-600" title="Ladder maxed — no rungs left to average down with">maxed</span>}
                    <span className="text-muted-foreground"> · {g.shares.toLocaleString()} sh</span>
                  </span>
                  <span className="tabular-nums shrink-0 flex items-center gap-2">
                    <span className={`text-[10px] ${g.need != null && Math.abs(g.need) > 5 ? 'text-amber-600' : 'text-muted-foreground'}`}
                      title={g.need != null ? `Needs ${g.need >= 0 ? '+' : ''}${g.need.toFixed(1)}% to reach its target` : undefined}>
                      {g.need != null ? `${g.need >= 0 ? '+' : ''}${g.need.toFixed(1)}%` : '—'}
                    </span>
                    <span className="w-20 text-right"><Money n={g.gain} /></span>
                  </span>
                </div>
              ))}
              <div className="flex items-center justify-between text-[10px] text-muted-foreground pt-0.5 border-t border-border/40">
                <span>{rows.length} position(s) with a resting target</span>
                <span className="tabular-nums">
                  {(() => {
                    const near = rows.filter(r => r[1].need != null && Math.abs(r[1].need) <= 2)
                    const g = near.reduce((a, r) => a + r[1].gain, 0)
                    return near.length ? `${near.length} within 2% · ${g >= 0 ? '+' : ''}$${Math.abs(Math.round(g)).toLocaleString()}` : 'none within 2%'
                  })()}
                </span>
              </div>
            </div>
          )
        })()}
        {(() => {
          // full-ladder cash allocation from config = sum of scaled tranches over LONG profiles.
          // Disabled profiles won't place new buys, so they only count what's already committed
          // (shares × avg) — a disabled profile holding nothing contributes 0.
          const alloc = profiles.filter(p => p.direction !== 'short').reduce((a, p) => {
            // A profile that is disabled, or PAST its wind-down date while flat, will never
            // place these rungs — expiry cancels the resting buys and starts no new cycle — so
            // only what it actually holds is committed. The bot already reserves nothing for
            // them; without this the total claimed budget they cannot use.
            const parked = p.disabled || (p.expiry != null && p.expiry * 1000 <= now && !p.shares)
            if (parked) return a + ((p.shares && p.avg) ? p.shares * p.avg : 0)
            return a + (p.maxCapital || 0)
          }, 0)
          const bud = settings?.cash_budget || 0
          if (!alloc) return null
          const pct = bud ? (alloc / bud) * 100 : 0
          return (
            <div className="flex items-center justify-between" title="If every enabled long profile's ladder fully filled: sum of its scaled tranches across all rungs, vs the cash budget. Disabled profiles only count cash already committed (0 if flat). Reverse profiles recycle their own proceeds, so they're excluded.">
              <span className="text-muted-foreground">Allocated (config, full ladders)</span>
              <span className="tabular-nums">
                ${Math.round(alloc).toLocaleString()}
                {bud > 0 && <span className={pct > 100 ? 'text-amber-600' : 'text-muted-foreground'}> · {pct.toFixed(0)}% of ${bud.toLocaleString()} budget</span>}
              </span>
            </div>
          )
        })()}
        {recent && (
          <div className="w-full flex items-center justify-between">
            <button
              type="button"
              onClick={() => setShowHistory(v => !v)}
              className="text-muted-foreground -mx-1 px-1 py-0.5 rounded-md hover:bg-muted/60"
            >
              Last play <span className="text-[10px]">({showHistory ? 'hide' : 'history'})</span>
            </button>
            <span className="tabular-nums">
              <button type="button" onClick={() => scrollToProfile(recent.key)}
                className="font-medium hover:underline" title={`Jump to ${recent.key ?? recent.sym}`}>{recent.key ?? recent.sym}</button>
              {' '}<Money n={recent.sell.pnl} />
              {recent.sell.trim && <span className="text-[10px] text-violet-600"> reduce</span>}
              <span className="text-muted-foreground"> · {ago(recent.sell.ts * 1000, now)}</span>
            </span>
          </div>
        )}
        <div className="flex items-center justify-between text-xs">
          <span className="text-muted-foreground">today / 24h / 7d / 30d / all</span>
          <span className="tabular-nums">
            <Money n={sum(p => p.realizedToday)} /> · <Money n={sum(p => p.realized24h)} /> · <Money n={sum(p => p.realized7d)} /> · <Money n={sum(p => p.realized30d)} /> · <Money n={totalRealized} />
          </span>
        </div>
        {(() => {
          // Ladders with every rung filled: no dry powder left, they can only wait for the
          // target. Sits on the summary card next to the capital it has locked up, because that
          // is the pairing that matters — the money is committed and nothing is holding it back.
          const maxed = profiles.filter(p => p.cap && ((p.netSold || 0) > 0 ? (p.soldRungs || 0) : p.filledCount) >= p.cap && (p.shares || p.netSold))
          if (!maxed.length) return null
          const tied = maxed.reduce((a, p) => a + ((p.netSold || 0) > 0 ? p.netSold! * (p.soldAvg || 0) : (p.shares || 0) * (p.avg || 0)), 0)
          return (
            <div className="flex items-center justify-between text-xs gap-2">
              <span className="text-muted-foreground shrink-0">Maxed ladders</span>
              <span className="tabular-nums text-right text-amber-600">
                {profLinks(maxed, 'text-amber-600')}
                <span className="text-muted-foreground"> · ${Math.round(tied).toLocaleString()} committed, no rungs left</span>
              </span>
            </div>
          )
        })()}
        {(profiles.some(p => p.halted) || profiles.some(p => p.frozen) || profiles.some(p => p.disabled)) && (
          <div className="flex items-center justify-between text-xs gap-2">
            <span className="text-muted-foreground shrink-0">State</span>
            <span className="tabular-nums text-right space-x-2">
              {profiles.some(p => p.halted) && <span className="text-red-600 font-medium">halted: {profLinks(profiles.filter(p => p.halted), 'text-red-600 font-medium')}</span>}
              {profiles.some(p => p.frozen) && <span className="text-sky-600">frozen: {profLinks(profiles.filter(p => p.frozen), 'text-sky-600')}</span>}
              {profiles.some(closingOut) && <span className="text-amber-600">closing out: {profLinks(profiles.filter(closingOut), 'text-amber-600')}</span>}
              {profiles.some(parkedOff) && <span className="text-amber-700">disabled: {profLinks(profiles.filter(parkedOff), 'text-amber-700')}</span>}
            </span>
          </div>
        )}
        {/* Matches the card badges: "not in play" = left out by the cash picker while cash is short
            (whether or not it is asking for cash this poll); "on hold" = in play but still unfunded. */}
        {profiles.some(p => p.notInPlay) && (
          <div className="flex items-center justify-between text-xs gap-2">
            <span className="text-muted-foreground shrink-0" title="Cash is short and these profiles aren't picked in Budgets → Who gets cash when it's short — they get no new buys.">Not in play</span>
            <span className="text-zinc-600 text-right">{profLinks(profiles.filter(p => p.notInPlay), 'text-zinc-600')}</span>
          </div>
        )}
        {profiles.some(p => p.funded === false && !p.notInPlay) && (
          <div className="flex items-center justify-between text-xs gap-2">
            <span className="text-muted-foreground shrink-0">On hold (budget)</span>
            <span className="text-amber-600 text-right">{profLinks(profiles.filter(p => p.funded === false && !p.notInPlay), 'text-amber-600')}</span>
          </div>
        )}
        {profiles.some(p => p.recon) && (
          <div className="flex items-center justify-between text-xs">
            <span className="text-muted-foreground">Shadow ledger</span>
            {profiles.some(p => p.recon && !p.recon.match)
              ? <span className="text-red-600">divergence: {profLinks(profiles.filter(p => p.recon && !p.recon.match), 'text-red-600')}</span>
              : <span className="text-emerald-600">✓ all match broker ({profiles.filter(p => p.recon).length})</span>}
          </div>
        )}
      </div>

      {settings && (
        <div className="rounded-2xl border bg-muted/30 p-3 text-sm">
          {editBudgets ? (
            <div className="space-y-2">
              <div className="text-xs text-muted-foreground">Budgets — apply on the next poll (~15s)</div>
              <label className="flex items-center justify-between gap-2">
                <span className="text-muted-foreground">Cash budget (longs)</span>
                <span className="flex items-center gap-1">$<input
                  type="number" inputMode="decimal" min="0" step="any" value={budgetDraft.cash_budget ?? ''}
                  onChange={e => setBudgetDraft(d => ({ ...d, cash_budget: e.target.value }))}
                  className="w-28 rounded border bg-background px-2 py-1 tabular-nums text-right" /></span>
              </label>
              {Object.keys(settings.reverse_inventory || {}).map(tkr => (
                <label key={tkr} className="flex items-center justify-between gap-2">
                  <span className="text-muted-foreground">{tkr} share pool (shorts)</span>
                  <span className="flex items-center gap-1"><input
                    type="number" inputMode="numeric" min="0" step="1" value={budgetDraft[`rev_${tkr}`] ?? ''}
                    onChange={e => setBudgetDraft(d => ({ ...d, [`rev_${tkr}`]: e.target.value }))}
                    className="w-28 rounded border bg-background px-2 py-1 tabular-nums text-right" /> sh</span>
                </label>
              ))}
              <label className="flex items-center justify-between gap-2">
                <span className="text-muted-foreground" title="Only PLACE a resting order when it's within this % of market; farther rungs are deferred (not ordered) until price approaches. Applies to long buys (below) and short sells (above).">Order within % of mkt</span>
                <span className="flex items-center gap-1"><input
                  type="number" inputMode="decimal" min="0" max="100" step="any" value={budgetDraft.place_within_pct ?? ''}
                  onChange={e => setBudgetDraft(d => ({ ...d, place_within_pct: e.target.value }))}
                  className="w-28 rounded border bg-background px-2 py-1 tabular-nums text-right" /> %</span>
              </label>
              <div className="space-y-1 border-t pt-2">
                <div className="flex items-center justify-between gap-2">
                  <span className="text-muted-foreground"
                    title="When the cash budget can't fund every profile's buys, only the profiles ticked here may take cash for NEW buys — so cash freed by a close goes where you chose. Cash they can't use stays idle. With enough cash for everyone this does nothing; with none ticked, every profile competes as usual. A profile left out whose position then closes is put back in play automatically (shown as 'rejoined').">
                    In play when cash-constrained
                  </span>
                  {playDraft.length > 0 && (
                    <button type="button" className="text-[10px] text-muted-foreground hover:underline" onClick={() => setPlayDraft([])}>clear</button>
                  )}
                </div>
                <div className="flex flex-wrap gap-x-3 gap-y-1 text-xs">
                  {(profiles || []).filter(p => p.direction !== 'short').map(p => (
                    <label key={p.profile} className="flex items-center gap-1">
                      <input type="checkbox" checked={playDraft.includes(p.profile)}
                        onChange={e => setPlayDraft(d => e.target.checked ? [...d, p.profile] : d.filter(x => x !== p.profile))} />
                      <span className={p.disabled ? 'text-muted-foreground line-through' : ''}>{profLabel(p)}</span>
                      {(settings.constrained_rejoined || []).includes(p.profile) && (
                        <span className="text-[10px] text-violet-600" title="Left out, then its position closed — the bot put it back in play.">rejoined</span>
                      )}
                    </label>
                  ))}
                </div>
                {playDraft.length > 0 && (
                  <>
                    <label className="flex items-center gap-1.5 text-xs"
                      title="Off: unpicked profiles keep the buys already resting, they just get nothing new. On: those resting buys are cancelled too, so their cash goes to the picked profiles.">
                      <input type="checkbox" checked={playCancelDraft} onChange={e => setPlayCancelDraft(e.target.checked)} />
                      <span>Cancel unpicked profiles&apos; resting buys</span>
                    </label>
                    <label className="flex items-center justify-between gap-2 text-xs">
                      <span className="text-muted-foreground">Selection expires (optional)</span>
                      <span className="flex items-center gap-1">
                        <input type="datetime-local" value={playUntilDraft} onChange={e => setPlayUntilDraft(e.target.value)}
                          className="rounded border bg-background px-1.5 py-0.5 tabular-nums" />
                        {playUntilDraft && (
                          <button type="button" className="text-[10px] text-muted-foreground hover:underline" onClick={() => setPlayUntilDraft('')}>none</button>
                        )}
                      </span>
                    </label>
                  </>
                )}
              </div>
              <div className="flex gap-2">
                <button type="button" onClick={saveBudgets} className="px-2 py-1 rounded bg-foreground text-background text-[11px]">Save</button>
                <button type="button" onClick={() => setEditBudgets(false)} className="px-2 py-1 rounded bg-muted hover:bg-muted/70 text-[11px]">Cancel</button>
              </div>
            </div>
          ) : (
            <button type="button" onClick={openBudgetEdit}
              className="w-full flex items-center justify-between text-left text-muted-foreground hover:text-foreground">
              <span>Budgets</span>
              <span className="tabular-nums">
                cash ${settings.cash_budget.toLocaleString()}
                {Object.entries(settings.reverse_inventory || {}).map(([k, v]) => ` · ${k} ${v} sh`).join('')}
                {settings.place_within_pct != null && ` · within ${settings.place_within_pct}%`}
                <span className="opacity-60"> ✎</span>
              </span>
            </button>
          )}
          {!editBudgets && (() => {
            // Always visible, so the cash picker can be found without knowing it lives in the
            // Budgets editor. Clicking opens that editor at the picker.
            const rejoined = (settings.constrained_rejoined || []).filter(k => !(settings.constrained_play || []).includes(k))
            const picked = settings.constrained_play || []
            const until = settings.constrained_until ?? null
            const expired = until != null && until * 1000 <= now
            const constrainedNow = (profiles || []).some(p => p.cashConstrained)
            const onHold = (profiles || []).filter(p => p.direction !== 'short' && (p.funded === false || p.notInPlay)).length
            return (
              <button type="button" onClick={openBudgetEdit}
                className="mt-1.5 w-full flex items-start justify-between gap-2 text-left text-xs hover:text-foreground"
                title="When the cash budget can't fund every profile's buys, only the profiles you pick here may take cash for NEW buys. Click to choose.">
                <span className="text-muted-foreground">Who gets cash when it&apos;s short</span>
                {picked.length ? (
                  <span className={`text-right ${expired ? 'text-muted-foreground line-through' : ''}`}>
                    {picked.join(', ')}
                    {rejoined.length > 0 && (
                      <span className="text-violet-600" title="Left out, then their position closed — back in play automatically, equal with your picks.">
                        {' + '}{rejoined.join(', ')} (rejoined after close)
                      </span>
                    )}
                    {settings.constrained_cancel ? ' · cancels others’ buys' : ''}
                    {until != null && (
                      <span className="text-muted-foreground">
                        {expired ? ' · expired' : ` · until ${new Date(until * 1000).toLocaleString(undefined, { month: 'numeric', day: 'numeric', hour: 'numeric', minute: '2-digit' })}`}
                      </span>
                    )}
                    {!expired && (
                      <span className={constrainedNow ? 'text-amber-600' : 'text-muted-foreground'}>
                        {constrainedNow ? ' · active now' : ' · idle (cash covers everyone)'}
                      </span>
                    )}
                    <span className="opacity-60"> ✎</span>
                  </span>
                ) : (
                  <span className="text-right text-muted-foreground">
                    everyone competes{onHold ? ` · ${onHold} on hold now` : ''}
                    {' · '}<span className="text-violet-600">pick profiles</span>
                  </span>
                )}
              </button>
            )
          })()}
        </div>
      )}

      <div className="rounded-2xl border bg-muted/30 p-3 text-sm space-y-2">
        <div className="flex items-center justify-between gap-2">
          <button
            type="button"
            onClick={() => setShowInvestors(v => !v)}
            aria-expanded={showInvestors}
            className="text-muted-foreground -mx-1 px-1 py-0.5 rounded-md hover:bg-muted/60 text-left min-w-0"
          >
            Investors
            {fund && <span className="text-[10px]"> · NAV ${fund.price.toFixed(4)}/unit</span>}
            <span className="text-[10px]"> ({showInvestors ? 'hide' : 'show'})</span>
          </button>
          {/* Depositing while the card is shut would open a form nobody can see. */}
          <button type="button"
            onClick={() => { setShowInvestors(true); setFundForm(f => (f?.mode === 'new' ? null : { mode: 'new' })); setFundName(''); setFundAmt(''); setFundMsg(null) }}
            className="text-[11px] px-2 py-1 rounded bg-foreground text-background shrink-0">+ Deposit</button>
        </div>

        {/* Shut, the card still answers the only question worth asking in passing:
            what the fund is worth and whether today went well. */}
        {!showInvestors && fund && (
          <button type="button" onClick={() => setShowInvestors(true)}
            className="text-[11px] text-muted-foreground text-left -mx-1 px-1 py-0.5 rounded-md hover:bg-muted/60">
            <span className="tabular-nums text-foreground">${fund.totalValue.toLocaleString()}</span>
            {' · '}investors <Money n={fund.investors.reduce((a, i) => a + i.gain, 0)} />
            {fund.todayChange != null && <>{' · '}today <Money n={fund.todayChange} /></>}
            {fund.investors.length > 0 && <span> · {fund.investors.length} investor{fund.investors.length === 1 ? '' : 's'}</span>}
          </button>
        )}
        {!showInvestors && !fund && (
          <div className="text-[11px] text-muted-foreground">No fund yet.</div>
        )}

        {showInvestors && (<>
        {fundForm && (
          <div className="rounded-lg border bg-background/60 p-2 space-y-2">
            <div className="text-[11px] text-muted-foreground">
              {fundForm.mode === 'new' ? 'New deposit — buys units at the current NAV'
                : fundForm.mode === 'deposit' ? `Deposit more for ${fundForm.investor}`
                : fundForm.mode === 'base' ? 'Set base capital — the total the fund\'s return is measured against (keeps everyone\'s current value; changes only future sensitivity)'
                : `Withdraw for ${fundForm.investor} (redeems units at NAV)`}
            </div>
            <div className="flex items-center gap-2 flex-wrap">
              {fundForm.mode === 'new' && (
                <input value={fundName} onChange={e => setFundName(e.target.value)} placeholder="investor name" autoFocus
                  className="rounded border bg-background px-2 py-1 text-xs w-36" />
              )}
              <span className="flex items-center gap-1 text-xs">$<input type="number" inputMode="decimal" min="0" step="any"
                value={fundAmt} onChange={e => setFundAmt(e.target.value)} placeholder="amount" autoFocus={fundForm.mode !== 'new'}
                className="rounded border bg-background px-2 py-1 tabular-nums w-28 text-right" /></span>
              {fundForm.mode !== 'base' && (
                <input value={fundMemo} onChange={e => setFundMemo(e.target.value)} maxLength={200}
                  placeholder="memo (optional)"
                  className="rounded border bg-background px-2 py-1 text-xs w-44" />
              )}
              <button type="button"
                onClick={() => {
                  const amt = Number(fundAmt)
                  if (fundForm.mode === 'new') {
                    const nm = fundName.trim()
                    if (!nm) { setFundMsg('⚠ enter an investor name'); return }
                    if (!isFinite(amt) || amt <= 0) { setFundMsg('⚠ enter a positive amount'); return }
                    sendFund('fund_deposit', { investor: nm, amount: amt, memo: fundMemo.trim() })
                  } else if (fundForm.mode === 'base') {
                    if (!isFinite(amt) || amt <= 0) { setFundMsg('⚠ enter a positive amount'); return }
                    sendFund('fund_set_base', { amount: amt })
                  } else {
                    if (!isFinite(amt) || amt <= 0) { setFundMsg('⚠ enter a positive amount'); return }
                    sendFund(fundForm.mode === 'withdraw' ? 'fund_withdraw' : 'fund_deposit', { investor: fundForm.investor, amount: amt, memo: fundMemo.trim() })
                  }
                  setFundForm(null); setFundName(''); setFundAmt(''); setFundMemo('')
                }}
                className="px-2 py-1 rounded bg-foreground text-background text-[11px]">{fundForm.mode === 'withdraw' ? 'Withdraw' : fundForm.mode === 'base' ? 'Set base' : 'Add'}</button>
              {fundForm.mode === 'withdraw' && (
                <button type="button"
                  onClick={() => { sendFund('fund_withdraw', { investor: fundForm.investor, all: true, memo: fundMemo.trim() }); setFundForm(null); setFundAmt(''); setFundMemo('') }}
                  className="px-2 py-1 rounded bg-muted hover:bg-muted/70 text-[11px]">Withdraw all</button>
              )}
              <button type="button" onClick={() => { setFundForm(null); setFundName(''); setFundAmt(''); setFundMemo('') }}
                className="px-2 py-1 rounded bg-muted hover:bg-muted/70 text-[11px]">Cancel</button>
            </div>
          </div>
        )}
        {fundMsg && <div className={`text-[11px] ${fundMsg.startsWith('⚠') ? 'text-red-600' : fundMsg.startsWith('✓') ? 'text-emerald-600' : 'text-muted-foreground'}`}>{fundMsg}</div>}

        {fund ? (
          <>
            <div className="flex items-center justify-between text-[11px] text-muted-foreground">
              <span>
                Fund value <span className="tabular-nums text-foreground">${fund.totalValue.toLocaleString()}</span>
                {' · '}investors <Money n={fund.investors.reduce((a, i) => a + i.gain, 0)} />
                {fund.annualPct != null && (
                  <span className="tabular-nums" title="Money-weighted annual yield (XIRR) across every investor's deposits and withdrawals, each discounted from its own date. Net of unrealized — NAV is marked to market.">
                    {' · '}{fund.annualPct >= 0 ? '+' : ''}{fund.annualPct.toFixed(1)}%/yr
                  </span>
                )}
                {fund.todayChange != null && (
                  <>
                    {' · '}
                    <span title={`Change since this trading day opened${fund.dayOpenPrice != null ? ` (NAV $${fund.dayOpenPrice.toFixed(4)})` : ''}`}>
                      today <Money n={fund.todayChange} />
                      {fund.todayChangePct != null && fund.todayChangePct !== 0 && (
                        <span className="tabular-nums"> ({fund.todayChangePct >= 0 ? '+' : ''}{fund.todayChangePct.toFixed(2)}%)</span>
                      )}
                    </span>
                  </>
                )}
              </span>
              <button type="button"
                onClick={() => { setFundForm({ mode: 'base' }); setFundAmt(String(Math.round(fund.baseCapital))); setFundMsg(null) }}
                title={`Capital base the return is measured against (your working capital). The whole strategy has moved ${fund.pnlSinceInception >= 0 ? '+' : ''}$${fund.pnlSinceInception.toFixed(2)} since the fund opened; investors hold ~${(fund.totalValue / fund.baseCapital * 100).toFixed(1)}% of the book, so their combined P&L is the smaller figure to the left. Click to re-sync after changing budgets.`}
                className="hover:underline decoration-dotted underline-offset-2">
                base ${fund.baseCapital.toLocaleString()} · NAV ${fund.price.toFixed(4)} <span className="opacity-60">✎</span>
              </button>
            </div>
            {!fund.investors.length && <div className="text-xs text-muted-foreground">No investors yet — add one above.</div>}
            {fund.investors.map(inv => (
              <div key={inv.name}>
                <div className="group/inv flex items-center justify-between gap-2 border-t border-border/40 pt-1.5">
                  <span className="flex flex-col leading-tight" title={`${inv.units.toLocaleString()} units`}>
                    {/* Rename: investors are keyed by name, so this moves the whole record
                        (units, NAV history, memos, note) to the new key. Enter saves, Escape
                        cancels. The bot refuses to rename onto a name already in use. */}
                    {nameEdit === inv.name ? (
                      <span className="flex items-center gap-1">
                        <input value={nameDraft} autoFocus maxLength={40}
                          onChange={e => setNameDraft(e.target.value)}
                          onKeyDown={e => {
                            if (e.key === 'Enter') {
                              const v = nameDraft.trim()
                              if (v && v !== inv.name) sendFund('fund_rename_investor', { investor: inv.name, newName: v })
                              setNameEdit(null)
                            } else if (e.key === 'Escape') setNameEdit(null)
                          }}
                          className="w-36 rounded border bg-background px-1.5 py-0.5 text-xs font-medium" />
                        <button type="button" className="text-[10px] text-violet-600 hover:underline"
                          onClick={() => {
                            const v = nameDraft.trim()
                            if (v && v !== inv.name) sendFund('fund_rename_investor', { investor: inv.name, newName: v })
                            setNameEdit(null)
                          }}>save</button>
                        <button type="button" className="text-[10px] text-muted-foreground hover:underline"
                          onClick={() => setNameEdit(null)}>cancel</button>
                      </span>
                    ) : (
                      <button type="button" title={`Rename ${inv.name}`}
                        onClick={() => { setNameEdit(inv.name); setNameDraft(inv.name) }}
                        className="font-medium text-left hover:underline decoration-dotted underline-offset-2">
                        {inv.name} <span className="opacity-0 group-hover/inv:opacity-60 text-[10px]">✎</span>
                      </button>
                    )}
                    <button type="button"
                      onClick={() => setFundHist(h => (h === inv.name ? null : inv.name))}
                      className="text-[10px] text-muted-foreground tabular-nums text-left hover:text-foreground hover:underline decoration-dotted underline-offset-2">
                      invested ${inv.deposited.toLocaleString()}
                      {inv.withdrawn > 0 && ` · withdrawn $${inv.withdrawn.toLocaleString()}`}
                      <span className="opacity-60"> · {fundHist === inv.name ? 'hide' : 'history'}</span>
                    </button>
                    {/* Free-text note tied to the INVESTOR (not a transaction) — survives
                        deposits and withdrawals. Click to edit, save empty to clear. */}
                    {noteEdit === inv.name ? (
                      <span className="mt-1 flex items-start gap-1">
                        <textarea value={noteDraft} autoFocus rows={2}
                          onChange={e => setNoteDraft(e.target.value)}
                          maxLength={500}
                          placeholder="note about this investor…"
                          className="w-56 rounded border bg-background px-1.5 py-1 text-[11px] leading-snug" />
                        <span className="flex flex-col gap-0.5">
                          <button type="button" className="text-[10px] text-violet-600 hover:underline"
                            onClick={() => { sendFund('fund_set_note', { investor: inv.name, note: noteDraft }); setNoteEdit(null) }}>save</button>
                          <button type="button" className="text-[10px] text-muted-foreground hover:underline"
                            onClick={() => setNoteEdit(null)}>cancel</button>
                        </span>
                      </span>
                    ) : inv.note ? (
                      <button type="button" title="Click to edit this note"
                        onClick={() => { setNoteEdit(inv.name); setNoteDraft(inv.note || '') }}
                        className="mt-0.5 text-[11px] text-left text-muted-foreground italic whitespace-pre-wrap hover:text-foreground max-w-56">
                        {inv.note}
                      </button>
                    ) : (
                      <button type="button"
                        onClick={() => { setNoteEdit(inv.name); setNoteDraft('') }}
                        className="mt-0.5 text-[10px] text-left text-violet-600 hover:underline">+ note</button>
                    )}
                  </span>
                  <span className="flex items-center gap-2 tabular-nums">
                    <span title="Current value">${inv.value.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
                    <span className="flex flex-col items-end leading-tight">
                      <span className="text-xs"><Money n={inv.gain} /> ({inv.gainPct >= 0 ? '+' : ''}{inv.gainPct.toFixed(1)}%)</span>
                      {inv.annualPct != null && (
                        <span className="text-[10px] text-muted-foreground"
                          title={`Money-weighted annual yield (XIRR) over ${inv.heldDays?.toFixed(0) ?? '?'} days, each deposit discounted from its own date. The flat ${inv.gainPct.toFixed(1)}% cannot be compared across investors who paid in at different times; this can.`}>
                          {inv.annualPct >= 0 ? '+' : ''}{inv.annualPct.toFixed(1)}%/yr
                        </span>
                      )}
                      {inv.todayChange != null && (
                        <span className="text-[10px]" title="Change since this trading day opened">
                          today <Money n={inv.todayChange} />
                        </span>
                      )}
                    </span>
                    <button type="button" title={`Deposit more for ${inv.name}`}
                      onClick={() => { setFundForm({ mode: 'deposit', investor: inv.name }); setFundAmt(''); setFundMsg(null) }}
                      className="text-[10px] px-1.5 py-0.5 rounded bg-muted hover:bg-muted/70">+</button>
                    <button type="button" title={`Withdraw for ${inv.name}`}
                      onClick={() => { setFundForm({ mode: 'withdraw', investor: inv.name }); setFundAmt(''); setFundMsg(null) }}
                      className="text-[10px] px-1.5 py-0.5 rounded bg-muted hover:bg-muted/70">−</button>
                    {Math.abs(inv.units) < 0.001 && (
                      <button type="button" title={`Remove ${inv.name}`}
                        onClick={() => sendFund('fund_remove_investor', { investor: inv.name })}
                        className="text-[10px] text-red-600 hover:underline">×</button>
                    )}
                  </span>
                </div>
                {fundHist === inv.name && (
                  <div className="mt-1 mb-1 ml-1 pl-2 border-l-2 border-border/40 space-y-0.5">
                    {(!inv.txns || !inv.txns.length) && <div className="text-[10px] text-muted-foreground">No transactions recorded.</div>}
                    {inv.txns && [...inv.txns].reverse().map((tx, i) => (
                      <div key={i}>
                        <div className="flex items-center justify-between text-[10px] tabular-nums gap-2">
                          <span className={tx.type === 'deposit' ? 'text-emerald-600' : 'text-amber-600'}>
                            {tx.type === 'deposit' ? 'deposit' : 'withdraw'} ${tx.amount.toLocaleString()}
                          </span>
                          <span className="text-muted-foreground">
                            {tx.units.toLocaleString(undefined, { maximumFractionDigits: 2 })}u @ ${tx.price.toFixed(4)} · {new Date(tx.ts * 1000).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })}
                          </span>
                        </div>
                        {/* The memo gets its own line so a long one can't squeeze the figures.
                            Click to edit -- the early deposits predate the memo field, and only
                            the memo is editable: amount/price/units mint and redeem units, so
                            changing them would rewrite the fund's NAV history. */}
                        {txnEdit === `${inv.name}:${tx.ts}` ? (
                          <div className="mt-0.5 flex items-center gap-1">
                            <input value={txnMemo} autoFocus maxLength={200}
                              onChange={e => setTxnMemo(e.target.value)}
                              onKeyDown={e => {
                                if (e.key === 'Enter') {
                                  sendFund('fund_txn_memo', { investor: inv.name, ts_txn: tx.ts, memo: txnMemo.trim() })
                                  setTxnEdit(null)
                                } else if (e.key === 'Escape') setTxnEdit(null)
                              }}
                              placeholder="memo"
                              className="flex-1 rounded border bg-background px-1.5 py-0.5 text-[10px]" />
                            <button type="button" className="text-[10px] text-violet-600 hover:underline"
                              onClick={() => {
                                sendFund('fund_txn_memo', { investor: inv.name, ts_txn: tx.ts, memo: txnMemo.trim() })
                                setTxnEdit(null)
                              }}>save</button>
                            <button type="button" className="text-[10px] text-muted-foreground hover:underline"
                              onClick={() => setTxnEdit(null)}>cancel</button>
                          </div>
                        ) : tx.memo ? (
                          <button type="button" title="Click to edit this memo"
                            onClick={() => { setTxnEdit(`${inv.name}:${tx.ts}`); setTxnMemo(tx.memo || '') }}
                            className="block text-left text-[10px] text-muted-foreground italic break-words hover:text-foreground">
                            {tx.memo}
                          </button>
                        ) : (
                          <button type="button"
                            onClick={() => { setTxnEdit(`${inv.name}:${tx.ts}`); setTxnMemo('') }}
                            className="block text-left text-[10px] text-violet-600/70 hover:text-violet-600 hover:underline">
                            + memo
                          </button>
                        )}
                      </div>
                    ))}
                  </div>
                )}
              </div>
            ))}
            <div className="text-[9px] text-muted-foreground">%/yr is money-weighted (XIRR): each deposit and withdrawal discounted from its own date, so investors who paid in at different times are comparable — they hold the same fund, so the annualised figures converge where the flat percentages do not. · Notional bookkeeping · unit price = full NAV (incl. unrealized) · applies on the next poll (~15s)</div>
          </>
        ) : (
          <div className="text-xs text-muted-foreground">No fund yet — click <span className="font-medium">+ Deposit</span> to add an investor. Deposits buy units at the current NAV; withdrawals redeem at NAV with unrealized gains and losses included.</div>
        )}
        </>)}
      </div>

      {showHistory && (
        <div className="rounded-2xl border bg-muted/30 p-3 space-y-1 text-sm">
          {(() => {
            const q = txFilter.trim().toUpperCase()
            const base = txShowBuys ? plays : plays.filter(pl => pl.pnl != null)
            // Match the ticker OR the action, so "RIVN" and "COVER" both work. The list is one
            // flat stream across every profile, so finding one ticker's run means scrolling past
            // everyone else's.
            const shown = q ? base.filter(pl => (pl.symbol || '').toUpperCase().includes(q)
                                             || (pl.action || 'SELL').toUpperCase().includes(q)) : base
            const visible = shown.slice(0, histLimit); return (
          <>
          <div className="flex items-center justify-between mb-1">
            <span className="text-xs text-muted-foreground shrink-0">
              {txShowBuys ? 'Transaction history' : 'Closes only'} · {Math.min(histLimit, shown.length)} of {shown.length}
              {q && <span className="text-foreground"> matching &ldquo;{txFilter.trim()}&rdquo;</span>}
            </span>
            <span className="flex items-center gap-1.5">
              <span className="relative">
                <input value={txFilter} onChange={e => { setTxFilter(e.target.value); setHistLimit(e.target.value.trim() ? 250 : 15) }}
                  placeholder="filter ticker…" spellCheck={false}
                  className="w-28 rounded-full border bg-background pl-2.5 pr-5 py-0.5 text-[10px] placeholder:text-muted-foreground/70" />
                {txFilter && (
                  <button type="button" onClick={() => { setTxFilter(''); setHistLimit(15) }}
                    title="clear filter"
                    className="absolute right-1.5 top-1/2 -translate-y-1/2 text-[10px] text-muted-foreground hover:text-foreground">×</button>
                )}
              </span>
              <button
                type="button"
                onClick={() => setTxShowBuys(v => !v)}
                className="text-[10px] px-2 py-0.5 rounded-full bg-muted hover:bg-muted/70 text-muted-foreground shrink-0"
              >
                {txShowBuys ? 'closes only' : 'show all'}
              </button>
            </span>
          </div>
          {shown.length === 0 && (
            <div className="text-xs text-muted-foreground">
              {q ? `Nothing matching “${txFilter.trim()}”${!txShowBuys ? ' in closes' : ''}.` : 'No transactions recorded yet.'}
            </div>
          )}
          {visible.map((pl, i) => (
            <div key={i} className="flex items-center justify-between">
              <span>
                <span className={`font-medium ${pl.action === 'BUY' ? 'text-sky-600' : pl.action === 'COVER' ? 'text-violet-600' : pl.action === 'SELL' && pl.pnl == null ? 'text-violet-600' : 'text-emerald-600'}`}>
                  {pl.action ?? 'SELL'}{pl.manual && <ManualChip />}
                </span>
                {' '}
                <button type="button" onClick={() => scrollToProfile(pl.symbol)}
                  className="font-medium hover:underline" title={`Jump to ${pl.symbol}`}>{pl.symbol}</button>
                {pl.qty != null && pl.price != null && (
                  <span className="text-muted-foreground text-xs"> · {pl.qty} @ ${pl.price.toFixed(2)}</span>
                )}
              </span>
              <span className="tabular-nums">
                {pl.pnl != null && <><Money n={pl.pnl} /> · </>}
                <span className="text-muted-foreground">{ago(pl.ts * 1000, now)}</span>
              </span>
            </div>
          ))}
          {shown.length > 15 && (
            <div className="flex gap-3 pt-1">
              {histLimit < shown.length && (
                <button type="button" onClick={() => setHistLimit(n => n + 25)}
                  className="text-[10px] text-muted-foreground hover:text-foreground hover:underline">show more</button>
              )}
              {histLimit > 15 && !q && (
                <button type="button" onClick={() => setHistLimit(15)}
                  className="text-[10px] text-muted-foreground hover:text-foreground hover:underline">show less</button>
              )}
            </div>
          )}
          </>
          ) })()}
        </div>
      )}

      {capital.length > 0 && (() => {
        const confRank = (c: CapitalRow['confidence']) =>
          c === 'tight' ? 3 : c === 'medium' ? 2 : c === 'wide' ? 1 : 0
        const val = (r: CapitalRow, k: string): number | string =>
          k === 'symbol' ? `${r.symbol}${r.direction === 'short' ? ' short' : ''}`
          : k === 'cycles' ? r.cycles
          : k === 'cyclesPerDay' ? (r.cyclesPerDay ?? -1)
          : k === 'conf' ? confRank(r.confidence)
          : k === 'mtm' ? r.mtm
          : k === 'annOnPeak' ? (r.annOnPeak ?? -1e9)
          : k === 'peakLo' ? (r.annOnPeakLo ?? -1e9)
          : k === 'annDeployedAll' ? (r.annDeployedAll ?? -1e9)
          : k === 'avgDeployed' ? r.avgDeployed
          : k === 'liveDays' ? r.tradingDays
          : k === 'dollarDays' ? r.dollarDays + r.dollarDaysOpen
          : k === 'openCapital' ? r.openCapital
          : k === 'medHoldDays' ? r.medHoldDays
          : k === 'resv' ? (r.window && !r.window.insufficient ? (r.window.annReserved ?? -1e9) : -1e9)
          : 0
        const rows = [...capital].sort((a, b) => {
          const x = val(a, capSort), y = val(b, capSort)
          const c = typeof x === 'string' ? x.localeCompare(y as string) : (x as number) - (y as number)
          return capDesc ? -c : c
        })
        // Clicking a header sorts by what that column SHOWS. First click on a new column goes
        // descending, which is the useful direction for every one of them except the ticker.
        const SortTh = ({ k, label, title, cls }: { k: string; label: string; title: string; cls?: string }) => (
          <th className={`${th} ${cls ?? ''} cursor-pointer select-none hover:text-foreground`}
            title={title}
            onClick={() => { if (capSort === k) { setCapDesc(d => !d) } else { setCapSort(k); setCapDesc(k !== 'symbol') } }}>
            {label}
            <span className={`ml-0.5 text-[8px] ${capSort === k ? '' : 'invisible'}`}>{capDesc ? '\u25be' : '\u25b4'}</span>
          </th>
        )
        const stuck = rows.filter(r => r.openDays >= 30)
        const totPnl = rows.reduce((t, r) => t + r.mtm, 0)
        const totDD = rows.reduce((t, r) => t + r.dollarDays + r.dollarDaysOpen, 0)
        const pct = (v: number | null | undefined) => v == null ? '—' : `${v.toFixed(0)}%`
        const days = (d: number) => d < 1 ? `${Math.round(d * 24)}h` : `${d.toFixed(1)}d`
        const kmoney = (v: number) => {
          const a = Math.abs(v)
          const t = a >= 1000 ? `$${(a / 1000).toFixed(1)}k` : `$${a.toFixed(0)}`
          return (v < 0 ? '-' : '+') + t
        }
        const kcap = (v: number) => v >= 1000 ? `$${(v / 1000).toFixed(0)}k` : `$${v.toFixed(0)}`
        // Dollar-days run to seven figures, so show a magnitude rather than the full number —
        // it is the denominator behind every rate AND the size of the sample they rest on.
        const kdd = (v: number) => v >= 1e6 ? `${(v / 1e6).toFixed(1)}M`
          : v >= 1000 ? `${(v / 1000).toFixed(0)}k` : `${v.toFixed(0)}`
        // A rate is only as good as the sample under it, so fade it as the evidence thins.
        // The eye then skips the numbers that deserve skipping, without hiding anything.
        const fade = (r: CapitalRow) =>
          r.confidence === 'tight' ? '' :
          r.confidence === 'medium' ? 'opacity-60' :
          r.confidence === 'wide' ? 'opacity-35' : 'opacity-30 italic'
        // A dot alongside, because opacity alone is easy to miss and disappears entirely for
        // anyone reading this in bright light or with low contrast sensitivity.
        const dot = (r: CapitalRow) =>
          r.confidence === 'tight' ? '●●●' :
          r.confidence === 'medium' ? '●●○' :
          r.confidence === 'wide' ? '●○○' : '○○○'
        const ciText = (r: CapitalRow) => r.ciLo != null && r.ciHi != null
          ? `${r.cycles} cycles · 80% of the time this lands between ${r.ciLo.toFixed(0)}% and ${r.ciHi.toFixed(0)}%`
          : `only ${r.cycles} cycle(s) — too few to put a range on; treat this rate as a hint, not a measurement`
        const th = 'font-normal pb-1.5 px-1.5 whitespace-nowrap'
        return (
        <div className="rounded-2xl border bg-muted/30 p-3 space-y-3 text-sm">
          <button type="button" onClick={() => setShowCapital(v => !v)}
            className="w-full flex items-center justify-between gap-2" aria-expanded={showCapital}>
            <span className="font-medium">Capital efficiency <span className="text-[10px] text-muted-foreground">({showCapital ? 'hide' : 'show'})</span></span>
            <span className="text-xs text-muted-foreground tabular-nums">
              {totDD > 0 && `${(totPnl / totDD * 365 * 100).toFixed(0)}% ann.`}
              {stuck.length > 0 && <span className="text-amber-600"> · {stuck.length} stuck 30d+</span>}
            </span>
          </button>

          {showCapital && (
            <div className="space-y-3 pt-1">
              <p className="text-[11px] text-muted-foreground leading-relaxed">
                Profit against <strong>dollar-days</strong> — capital times the time it was held, so a
                dollar earned twice as fast counts twice. Everything is marked to market and includes
                open positions.
              </p>
              <details className="text-[11px] text-muted-foreground">
                <summary className="cursor-pointer hover:text-foreground select-none">how these are calculated</summary>
                <div className="pt-2 space-y-1.5 leading-relaxed border-l-2 border-border/40 pl-2 mt-1">
                  <p><strong className="text-foreground">depl</strong> — return while the capital is actually out there. Hover for the closed-cycles-only figure.</p>
                  <p><strong className="text-foreground">peak</strong> — return on the largest sum ever committed at once, read from the ledger, so it never drifts when a ladder is resized. This is the default sort.</p>
                  <p><strong className="text-foreground">resv</strong> — return on the configured ladder, counting only cycles opened since that ladder last changed. 18 of 24 profiles have been resized, so charging a full history against today&apos;s ladder would flatter exactly the ones that were cut. Blank until 3 cycles and 7 days have run.</p>
                  <p><strong className="text-foreground">weekends</strong> — not charged. Dollar-days
                  accrue on trading days only, because the cost of tying capital up is what else it could
                  have been doing, and over a weekend that is nothing. This is not a flat rescale: a
                  ticker that only trades intraday keeps all its dollar-days, one that carries positions
                  over a weekend keeps about two thirds, so the discount goes exactly to the positions
                  the market shut the door on. <strong>open</strong> and <strong>hold</strong> stay in
                  calendar days — those are wall-clock facts you judge by eye, not rate denominators.</p>
                  <p><strong className="text-foreground">grouping</strong> — one row per ticker and
                  direction, pooling every profile on it. Four INTC ladders are four bets on one stock:
                  they rise and fall together and compete for the same budget, so judging them apart
                  says little about whether INTC is worth holding. Long and short stay separate even on
                  a shared ticker, since a covered short ties up stock while a long spends cash. Hover
                  a ticker to see its profiles.</p>
                  <p><strong className="text-foreground">ranking</strong> — sorted on the conservative
                  end of each interval, not the headline number, so a thin sample cannot top the board
                  on a lucky run. Tickers with too few cycles to bootstrap sort last: a single cycle is
                  not a ranking.</p>
                  <p><strong className="text-foreground">fading</strong> — the rate columns fade as the
                  sample thins. Each profile&apos;s cycles are resampled to get an 80% interval, so both
                  cycles, dollar-days and cycle-to-cycle variation are all priced in — volume as well
                  as spread, since twelve tidy cycles produce a tight-looking interval only because a
                  bootstrap cannot price the bad cycle that has not happened yet. The conf dots say it
                  outright; the rates fade to match. Hover any rate for its range. Under 5 cycles no
                  range is shown at all — with one cycle every resample is that same cycle,
                  which would report perfect confidence for the weakest possible evidence.</p>
                  <p><strong className="text-foreground">open</strong> — capital still tied up, and for how long. Counting only closed cycles would measure the winners and hide the losers, which for an averaging-down ladder is backwards: the positions that never close tie up the most capital for the longest.</p>
                </div>
              </details>
              <div className="flex items-center gap-2 text-[10px] text-muted-foreground">
                <span>click any column to sort</span>
                {capSort !== 'peakLo' && (
                  <button type="button" onClick={() => { setCapSort('peakLo'); setCapDesc(true) }}
                    className="px-2 py-0.5 rounded-full bg-muted hover:bg-muted/70">
                    reset to conservative ranking
                  </button>
                )}
              </div>
              <div className="overflow-x-auto rounded-lg bg-background">
                <table className="w-full text-[11px] tabular-nums">
                  <thead>
                    <tr className="text-[10px] uppercase tracking-wide text-muted-foreground text-right">
                      <SortTh k="symbol" label="ticker" cls="text-left pl-2 sticky left-0 bg-background" title="Ticker and direction. Every profile on it is pooled; hover a row's ticker to see which." />
                      <SortTh k="mtm" label="P&amp;L" title="Realized on closed cycles plus unrealized on anything still open" />
                      <SortTh k="cycles" label="cyc" title="Completed cycles pooled across every profile on this ticker — a cycle is one full round trip, opened and closed" />
                      <SortTh k="cyclesPerDay" label="cyc/d" title="Cycles per TRADING day. A cycle cannot close on a Saturday, so this counts weekday sessions — counting calendar days would understate turnover by about 30%." />
                      <SortTh k="conf" label="conf" title="How much evidence stands behind this row's rates: cycles, dollar-days and how much the cycles vary. Three dots = the sample supports the number; one = treat it as a hint." />
                      <SortTh k="annOnPeak" label="peak" title="Annualised return on the largest sum ever committed at once — never drifts when a ladder is resized" />
                      <SortTh k="annDeployedAll" label="depl" title="Annualised return while the capital is actually deployed" />
                      <SortTh k="avgDeployed" label="avg cap" title="Average capital this ticker has had deployed over its life" />
                      <SortTh k="liveDays" label="sess" title="Trading sessions since this ticker's first transaction. Multiply by avg cap and you get the $-days to its right. Hover a row for the calendar age." />
                      <SortTh k="dollarDays" label="$-days" title="Dollar-days: capital multiplied by the TRADING days it was held. Weekends are not charged — the market is shut, so no other ladder could have used that money either. This is the denominator under every rate here, and the size of the evidence they rest on." />
                      <SortTh k="openCapital" label="open" title="Capital still tied up in an open position, and how long it has been held" />
                      <SortTh k="medHoldDays" label="hold" title="Median hold, and the 90th-percentile tail the median hides" />
                      <SortTh k="resv" label="resv" cls="pr-0" title="Annualised return on the configured ladder, since that ladder last changed" />
                    </tr>
                  </thead>
                  <tbody>
                    {rows.map(r => (
                      <tr key={`${r.symbol}-${r.direction}`} className="text-right border-t border-border/40 hover:bg-muted/40">
                        <td className="text-left py-1.5 pr-1.5 pl-2 whitespace-nowrap sticky left-0 bg-background">
                          <span title={r.members.length > 1
                            ? `${r.profiles} profiles: ${r.members.map(m => `${m.profile} (${m.cycles}cyc, ${m.mtm >= 0 ? '+' : ''}$${m.mtm.toFixed(0)})`).join(', ')}`
                            : `${r.members[0]?.profile ?? r.symbol} · ${r.cycles} completed cycles`}>
                            {r.symbol}
                            {r.profiles > 1 && <span className="ml-0.5 text-[9px] text-muted-foreground">×{r.profiles}</span>}
                          </span>
                          {r.direction === 'short' && (
                            <span className="ml-1 text-[9px] text-violet-600" title="Covered short: ties up SHARE INVENTORY, not cash — its dollar-days are the notional value of the stock committed, so it is kept separate from the same ticker's long profiles.">short</span>
                          )}
                        </td>
                        <td className={`px-1.5 ${r.mtm < 0 ? 'text-red-600' : 'text-emerald-600'}`}
                          title={`${r.realized.toFixed(2)} realized${r.openUnrealized ? ` + ${r.openUnrealized.toFixed(2)} unrealized` : ''} over ${r.cycles} cycles`}>
                          {kmoney(r.mtm)}
                        </td>
                        <td className="px-1.5 text-muted-foreground"
                          title={r.members.length > 1
                            ? `${r.cycles} cycles: ${r.members.map(m => `${m.profile} ${m.cycles}`).join(', ')}`
                            : `${r.cycles} completed cycles`}>{r.cycles}</td>
                        <td className="px-1.5 text-muted-foreground"
                          title={r.cyclesPerDay
                            ? `${r.cycles} cycles over ${r.tradingDays} trading sessions (${r.liveDays.toFixed(0)} calendar days) — ${(r.cyclesPerDay * 5).toFixed(1)} a week`
                            : 'no completed cycles yet'}>
                          {r.cyclesPerDay ? r.cyclesPerDay.toFixed(2) : '—'}
                        </td>
                        <td className={`px-1.5 text-[8px] tracking-tighter align-middle ${fade(r)}`}
                          title={ciText(r)}>{dot(r)}</td>
                        <td className={`px-1.5 font-medium ${fade(r)} ${r.annOnPeak != null && r.annOnPeak < 0 ? 'text-red-600' : (r.annOnPeak != null && r.annOnPeak < 15 ? 'text-amber-600' : '')}`}
                          title={`Peak committed $${Math.round(r.peakCapital).toLocaleString()}${r.annOnPeakLo != null ? ` · ranked on its conservative end, ${r.annOnPeakLo.toFixed(0)}%` : ''} · ${ciText(r)}`}>
                          {pct(r.annOnPeak)}
                        </td>
                        <td className={`px-1.5 ${fade(r)} ${r.annDeployedAll != null && r.annDeployedAll < 0 ? 'text-red-600' : ''}`}
                          title={`${ciText(r)}${r.annDeployedClosed != null ? ` · ${r.annDeployedClosed.toFixed(0)}% counting closed cycles only` : ''}`}>
                          {pct(r.annDeployedAll)}
                        </td>
                        <td className="px-1.5 text-muted-foreground"
                          title={`$${Math.round(r.avgDeployed).toLocaleString()} held on average across ${r.liveDays.toFixed(0)} days`}>
                          {kcap(r.avgDeployed)}
                        </td>
                        <td className="px-1.5 text-muted-foreground"
                          title={`${r.tradingDays} trading sessions · ${r.liveDays.toFixed(0)} calendar days since its first transaction`}>
                          {r.tradingDays >= 1 ? `${r.tradingDays}` : '<1'}
                        </td>
                        <td className="px-1.5 text-muted-foreground"
                          title={`${Math.round(r.dollarDays + r.dollarDaysOpen).toLocaleString()} dollar-days = $${Math.round(r.avgDeployed).toLocaleString()} × ${r.liveDays.toFixed(0)} days${r.dollarDaysOpen > 0 ? ` (${Math.round(r.dollarDaysOpen).toLocaleString()} of it still open)` : ''}`}>
                          {kdd(r.dollarDays + r.dollarDaysOpen)}
                        </td>
                        <td className={`px-1.5 whitespace-nowrap ${r.openDays >= 30 ? 'text-amber-600' : 'text-muted-foreground'}`}
                          title={r.openCapital ? `$${Math.round(r.openCapital).toLocaleString()} tied up for ${r.openDays.toFixed(0)} days` : 'flat'}>
                          {r.openCapital ? `$${(r.openCapital / 1000).toFixed(1)}k · ${r.openDays.toFixed(0)}d` : '—'}
                        </td>
                        <td className="px-1.5 whitespace-nowrap">
                          {days(r.medHoldDays)}
                          <span className="text-muted-foreground"> / {days(r.p90HoldDays)}</span>
                        </td>
                        <td className="px-1.5 pr-0 text-muted-foreground">
                          {r.window && !r.window.insufficient && r.window.annReserved != null ? (
                            <span title={`${r.window.cycles} cycles over ${r.window.cfgDays.toFixed(0)}d on the current ladder · util ${r.window.utilPct?.toFixed(0)}%`}>
                              {pct(r.window.annReserved)}
                            </span>
                          ) : (
                            <span title={r.window
                              ? `Only ${r.window.cycles} cycle(s) in ${r.window.cfgDays.toFixed(0)}d since the ladder last changed — too little to annualise`
                              : 'No ladder history recorded yet'}>—</span>
                          )}
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
              <p className="text-[10px] text-muted-foreground leading-relaxed">
                Time is counted in <strong>trading sessions</strong>: a weekend costs nothing, since the
                market is shut and no other ladder could have used that money either. Rates annualise on
                252 sessions to match, and <strong>avg cap × sess = $-days</strong>, the denominator
                under every rate — capital times days held —
                and so also the size of the sample behind it. Faded rates have thin samples; hover for
                the range. <strong>avg cap</strong> is the
                dollar-days denominator: the capital this ticker has
                had working on average across its whole life. Hover it for the raw figure. Amber: under
                15% on peak capital, or a position open 30+ days. Red: negative once marked to market.
              </p>
            </div>
          )}
        </div>
        )
      })()}

      {analytics && (analytics.perProfile.length > 0 || analytics.byHour.length > 0) && (
        <div className="rounded-2xl border bg-muted/30 p-3 space-y-3 text-sm">
          <button
            type="button"
            onClick={() => setShowAnalytics(v => !v)}
            className="w-full flex items-center justify-between"
          >
            <span className="font-medium">Analytics <span className="text-[10px] text-muted-foreground">({showAnalytics ? 'hide' : 'show'})</span></span>
            <span className="text-xs text-muted-foreground tabular-nums">
              {analytics.totals.plays} plays · {analytics.totals.buys} buys
            </span>
          </button>

          {showAnalytics && (
            <div className="space-y-4 pt-1">
              {/* Profit distribution by profile (donut) — windowed; absolute $ or return on capital */}
              {analytics.perProfile.some(s => s.realized > 0) && (() => {
                const raw = analytics.windows?.[analyticsWindow] ?? analytics.perProfile
                // group per-profile entries by TICKER (symbol); keep the breakdown for drill-down
                type Grp = { symbol: string; realized: number; plays: number; maxCapital: number; profiles: ProfStat[] }
                const gmap = new Map<string, Grp>()
                for (const s of raw) {
                  const g = gmap.get(s.symbol) ?? { symbol: s.symbol, realized: 0, plays: 0, maxCapital: 0, profiles: [] }
                  g.realized += s.realized; g.plays += s.plays; g.maxCapital += s.maxCapital
                  g.profiles.push(s)
                  gmap.set(s.symbol, g)
                }
                const pp = [...gmap.values()].sort((a, b) => b.realized - a.realized)
                const roc = (s: { realized: number; maxCapital: number }) => (s.maxCapital ? (s.realized / s.maxCapital) * 100 : 0)
                const val = (s: { realized: number; maxCapital: number }) => Math.max(0, normalize ? roc(s) : s.realized)
                const total = pp.reduce((a, s) => a + val(s), 0) || 1
                const R = 42, C = 2 * Math.PI * R
                let acc = 0
                // A donut can only draw positive slices, so `total` above stays winners-only for
                // the geometry. The CENTRE must not: it read "$88 TOTAL" on a day that closed
                // -$1,769, because the two losing symbols had no slice and so were not counted.
                const netReal = pp.reduce((a, s) => a + s.realized, 0)
                const netCap = pp.reduce((a, s) => a + s.maxCapital, 0)
                const losers = pp.filter(s => s.realized < 0)
                const center = normalize
                  ? (netCap ? `${(netReal / netCap * 100).toFixed(1)}%` : '—')
                  : `${netReal < 0 ? '-' : ''}$${Math.round(Math.abs(netReal)).toLocaleString()}`
                const hasData = pp.some(s => s.realized !== 0)
                return (
                  <div className="space-y-2">
                    <div className="flex items-center justify-between">
                      <div className="text-[10px] uppercase tracking-wide text-muted-foreground">
                        {normalize ? 'Return on capital by symbol' : 'Profit distribution by symbol'}
                        {losers.length > 0 && (
                          <span className="normal-case tracking-normal text-red-500/80"
                            title={`${losers.map(l => `${l.symbol} ${l.realized.toFixed(2)}`).join(', ')} — a donut cannot draw a negative slice, so losers are listed without one. The centre figure is the net.`}>
                            {' '}· ring shows gains only, centre is net
                          </span>
                        )}
                      </div>
                      <button
                        type="button"
                        onClick={() => setNormalize(v => !v)}
                        className="text-[10px] px-2 py-0.5 rounded-full bg-muted hover:bg-muted/70 text-muted-foreground"
                      >
                        {normalize ? '% return on capital' : '$ absolute'}
                      </button>
                    </div>
                    <div className="flex gap-1">
                      {ANALYTICS_WINDOWS.map(w => (
                        <button
                          key={w.key}
                          type="button"
                          onClick={() => setAnalyticsWindow(w.key)}
                          className={`text-[10px] px-2 py-0.5 rounded-full ${analyticsWindow === w.key ? 'bg-foreground text-background' : 'bg-muted hover:bg-muted/70 text-muted-foreground'}`}
                        >
                          {w.label}
                        </button>
                      ))}
                    </div>
                    {netReal !== 0 && (() => {
                      const days = analytics.tradingDaysByWindow?.[analyticsWindow] || 1
                      const perYear = analytics.tradingDaysPerYear || 252
                      const ann = (r: number) => (r / days) * perYear
                      // classify each per-profile entry by direction (a ticker like TSLA can hold
                      // both a long and covered shorts, so we must split at the PROFILE level).
                      const dir = new Map((profiles || []).map(p => [p.profile, p.direction]))
                      const priceFor = (sym: string) => (profiles || []).find(p => p.symbol === sym)?.lastPrice || 0
                      // NET, not winners-only. Clamping each entry at zero made a losing window
                      // report a positive rate: 2026-10-05 closed -$1,769 and read +10%/yr.
                      const longReal = raw.filter(e => dir.get(e.profile ?? '') !== 'short').reduce((a, e) => a + e.realized, 0)
                      const shortReal = raw.filter(e => dir.get(e.profile ?? '') === 'short').reduce((a, e) => a + e.realized, 0)
                      // Denominators mirror the CAPS you set: longs vs the cash budget; shorts vs the
                      // share-pool VALUE (pool shares × price) — the pre-existing shares the covered
                      // grid monetizes. Blended = total realized ÷ (cash budget + pool value).
                      const cashBudget = settings?.cash_budget || 0
                      const rev = settings?.reverse_inventory || {}
                      const shortBase = Object.entries(rev).reduce((a, [sym, sh]) => a + (Number(sh) || 0) * priceFor(sym), 0)
                      const totalBase = cashBudget + shortBase
                      const pct = (annPnl: number, base: number) => base ? (annPnl / base) * 100 : null
                      const longPct = pct(ann(longReal), cashBudget)
                      const shortPct = pct(ann(shortReal), shortBase)
                      const blendPct = pct(ann(longReal + shortReal), totalBase)
                      const hasShort = shortReal > 0 || shortBase > 0
                      const pctSpan = (v: number | null) => (
                        <span className={`font-medium ${v != null && v >= 0 ? 'text-emerald-600' : 'text-red-500'}`}>{v != null ? `${v.toFixed(0)}%/yr` : '—'}</span>
                      )
                      if (!hasShort) {
                        return (
                          <div className="text-[11px] text-muted-foreground"
                            title={`Annualized: $${longReal.toFixed(0)} realized over ${days} trading day(s) → ÷${days} ×${perYear}/yr ÷ $${cashBudget.toLocaleString()} cash budget. Assumes this window's pace holds.`}>
                            ≈ {pctSpan(longPct)} annualized
                            <span className="opacity-70"> · ${Math.round(ann(longReal)).toLocaleString()}/yr on ${cashBudget.toLocaleString()} budget · {days} trading-day pace</span>
                          </div>
                        )
                      }
                      return (
                        <div className="text-[11px] text-muted-foreground space-y-0.5"
                          title={`Split annualized return, extrapolated from ${days} trading day(s) at ${perYear}/yr.\n• Long: realized from long profiles ÷ $${cashBudget.toLocaleString()} cash budget.\n• Short: realized from covered-short profiles ÷ share-pool value (pool shares × current price = $${Math.round(shortBase).toLocaleString()}) — the pre-existing shares the grid monetizes.\n• Blended: total realized ÷ (cash budget + pool value).`}>
                          <div>
                            ≈ {pctSpan(blendPct)} blended
                            <span className="opacity-70"> · ${Math.round(ann(longReal + shortReal)).toLocaleString()}/yr on ${Math.round(totalBase).toLocaleString()} total capital · {days} trading-day pace</span>
                          </div>
                          <div className="pl-3">
                            <span className="opacity-70">long </span>{pctSpan(longPct)}
                            <span className="opacity-70"> · ${Math.round(ann(longReal)).toLocaleString()}/yr on ${cashBudget.toLocaleString()} cash budget</span>
                          </div>
                          <div className="pl-3">
                            <span className="opacity-70">short </span>{pctSpan(shortPct)}
                            <span className="opacity-70"> · ${Math.round(ann(shortReal)).toLocaleString()}/yr on ${Math.round(shortBase).toLocaleString()} share pool</span>
                          </div>
                        </div>
                      )
                    })()}
                    {!hasData ? (
                      <div className="text-xs text-muted-foreground py-2">No completed plays in this window.</div>
                    ) : (
                    <div className="flex items-center gap-3">
                      <div className="relative w-24 h-24 shrink-0">
                        <svg viewBox="0 0 100 100" className="w-24 h-24 -rotate-90">
                          {pp.map((s, i) => {
                            if (val(s) <= 0) return null
                            const dash = (val(s) / total) * C
                            const seg = (
                              <circle
                                key={s.symbol} cx="50" cy="50" r={R} fill="none"
                                stroke={PIE_COLORS[i % PIE_COLORS.length]} strokeWidth="16"
                                strokeDasharray={`${dash} ${C - dash}`} strokeDashoffset={-acc}
                              />
                            )
                            acc += dash
                            return seg
                          })}
                        </svg>
                        <div className="absolute inset-0 flex flex-col items-center justify-center">
                          <span className="text-[10px] font-medium tabular-nums">{center}</span>
                          <span className="text-[8px] text-muted-foreground uppercase tracking-wide">{normalize ? 'blended' : 'total'}</span>
                        </div>
                      </div>
                      <div className="flex-1 space-y-1">
                        {pp.filter(s => val(s) > 0 || s.realized < 0).map(s => {
                          const i = pp.indexOf(s)
                          const multi = s.profiles.length > 1
                          const open = expandedSym === s.symbol
                          return (
                            <div key={s.symbol}>
                              <div
                                className={`flex items-center gap-2 text-xs ${multi ? 'cursor-pointer hover:opacity-70' : ''}`}
                                onClick={multi ? () => setExpandedSym(v => (v === s.symbol ? null : s.symbol)) : undefined}
                                title={multi ? `${s.profiles.length} profiles on ${s.symbol} — click to expand` : undefined}
                              >
                                <span className="w-2.5 h-2.5 rounded-sm shrink-0"
                      title={s.realized < 0 ? 'A loss has no slice — the ring can only draw positive share' : undefined}
                      style={s.realized < 0
                        ? { border: '1.5px solid rgb(239 68 68 / 0.6)' }
                        : { background: PIE_COLORS[i % PIE_COLORS.length] }} />
                                <span className="font-medium shrink-0">{s.symbol}{multi && <span className="text-muted-foreground text-[9px]">{open ? ' ▾' : ` ▸${s.profiles.length}`}</span>}</span>
                                <span className="flex-1 text-right tabular-nums">
                                  {normalize
                                    ? <span className={roc(s) < 0 ? 'text-red-500' : 'text-emerald-600'}>{roc(s).toFixed(2)}%</span>
                                    : <Money n={s.realized} />}
                                  <span className="text-muted-foreground">
                        {s.realized < 0 ? ' · loss' : ` · ${((val(s) / total) * 100).toFixed(0)}%`} · {s.plays}×
                      </span>
                                </span>
                              </div>
                              {multi && open && [...s.profiles].sort((a, b) => b.realized - a.realized).map(pr => (
                                <div key={pr.profile} className="flex items-center gap-2 text-[11px] pl-[18px] text-muted-foreground">
                                  <span className="shrink-0">{pr.profile}</span>
                                  <span className="flex-1 text-right tabular-nums">
                                    {normalize ? `${roc(pr).toFixed(2)}%` : money(pr.realized)}
                                    <span className="opacity-70"> · {pr.plays}×</span>
                                  </span>
                                </div>
                              ))}
                            </div>
                          )
                        })}
                      </div>
                    </div>
                    )}
                    {normalize && (
                      <div className="text-[9px] text-muted-foreground">return on capital = realized ÷ max committed cash per symbol (sum of its profiles)</div>
                    )}
                  </div>
                )
              })()}

              {/* Realized P&L by day */}
              {analytics.byDay.length > 0 && (() => {
                // Newest first, then cut — slicing before reversing would show the OLDEST 14.
                const allDays = [...analytics.byDay].reverse()
                const days = allDays.slice(0, dayLimit)
                // Scale the bars to what is on screen, so expanding re-scales to the longer
                // window rather than leaving every recent day dwarfed by one outlier months back.
                const max = Math.max(...days.map(d => Math.abs(d.realized)), 1)
                const shownTotal = days.reduce((a, d) => a + d.realized, 0)
                return (
                  <div className="space-y-1">
                    <div className="flex items-baseline justify-between gap-2">
                      <span className="text-[10px] uppercase tracking-wide text-muted-foreground">
                        Realized by day (PT, last {days.length}{allDays.length > days.length ? ` of ${allDays.length}` : ''})
                      </span>
                      <span className="text-[10px] tabular-nums text-muted-foreground">
                        <Money n={shownTotal} /> over {days.length}d
                      </span>
                    </div>
                    {days.map(d => (
                      <div key={d.date} className="flex items-center gap-2 text-xs">
                        <span className="w-12 shrink-0 text-muted-foreground tabular-nums">{d.date.slice(5)}</span>
                        <div className="flex-1 h-3 rounded bg-muted overflow-hidden">
                          <div
                            className={d.realized >= 0 ? 'h-full bg-emerald-500/60' : 'h-full bg-red-500/60'}
                            style={{ width: `${Math.max(2, (Math.abs(d.realized) / max) * 100)}%` }}
                          />
                        </div>
                        <span className="w-[110px] text-right tabular-nums shrink-0">
                          <Money n={d.realized} /><span className="text-muted-foreground"> · {d.plays}×</span>
                        </span>
                      </div>
                    ))}
                    {allDays.length > 14 && (
                      <div className="flex gap-3 pt-1">
                        {dayLimit < allDays.length && (
                          <>
                            <button type="button" onClick={() => setDayLimit(n => n + 30)}
                              className="text-[10px] text-muted-foreground hover:text-foreground hover:underline">show 30 more</button>
                            <button type="button" onClick={() => setDayLimit(allDays.length)}
                              className="text-[10px] text-muted-foreground hover:text-foreground hover:underline">all {allDays.length}</button>
                          </>
                        )}
                        {dayLimit > 14 && (
                          <button type="button" onClick={() => setDayLimit(14)}
                            className="text-[10px] text-muted-foreground hover:text-foreground hover:underline">show less</button>
                        )}
                      </div>
                    )}
                  </div>
                )
              })()}

              {/* Open/close timing by hour */}
              {analytics.byHour.length > 0 && (() => {
                const hrs = analytics.byHourWindows?.[hourWindow] ?? analytics.byHour
                const max = Math.max(...hrs.flatMap(h => [h.buys, h.sells]), 1)
                return (
                  <div className="space-y-1.5">
                    <div className="flex items-center justify-between">
                      <div className="text-[10px] uppercase tracking-wide text-muted-foreground">Activity by hour (PT) · opens vs closes</div>
                    </div>
                    <div className="flex gap-1">
                      {ANALYTICS_WINDOWS.map(w => (
                        <button
                          key={w.key}
                          type="button"
                          onClick={() => setHourWindow(w.key)}
                          className={`text-[10px] px-2 py-0.5 rounded-full ${hourWindow === w.key ? 'bg-foreground text-background' : 'bg-muted hover:bg-muted/70 text-muted-foreground'}`}
                        >
                          {w.label}
                        </button>
                      ))}
                    </div>
                    {hrs.length === 0 && <div className="text-xs text-muted-foreground py-1">No activity in this window.</div>}
                    {hrs.map(h => (
                      <div key={h.hour} className="flex items-center gap-2 text-[11px]">
                        <span className="w-7 text-right text-muted-foreground shrink-0">{fmtHour(h.hour)}</span>
                        <div className="flex-1 space-y-0.5">
                          <div className="flex items-center gap-1">
                            <div className="h-2 rounded-sm bg-sky-500/60" style={{ width: `${(h.buys / max) * 100}%` }} />
                            <span className="text-[9px] text-muted-foreground shrink-0">{h.buys}</span>
                          </div>
                          <div className="flex items-center gap-1">
                            <div className="h-2 rounded-sm bg-emerald-500/60" style={{ width: `${(h.sells / max) * 100}%` }} />
                            <span className="text-[9px] text-muted-foreground shrink-0">{h.sells}</span>
                          </div>
                        </div>
                      </div>
                    ))}
                    <div className="flex gap-3 text-[9px] text-muted-foreground pt-0.5">
                      <span className="flex items-center gap-1"><span className="inline-block w-2 h-2 rounded-sm bg-sky-500/60" /> buys (opens)</span>
                      <span className="flex items-center gap-1"><span className="inline-block w-2 h-2 rounded-sm bg-emerald-500/60" /> sells (closes)</span>
                    </div>
                  </div>
                )
              })()}
            </div>
          )}
        </div>
      )}

      <div className="flex items-center justify-between pt-1">
        <span className="text-xs text-muted-foreground">Profiles</span>
        <button type="button"
          title="Create a brand-new long profile from the default template"
          onClick={() => {
            const sym = (window.prompt('New profile — traded ticker symbol:', 'INTC') || '')
              .trim().toUpperCase().replace(/[^A-Z0-9.]/g, '').slice(0, 12)
            if (!sym) return
            const nk = cleanKey(window.prompt(
              `Profile key for ${sym}:\n\nStarts DISABLED with default params ($1,000 tranche · +25%/step · -1.7%/step · +1.0% profit · cap 6 · no max price). Tune it, then Enable.`,
              keyTaken(sym) ? nextFreeKey(sym) : sym) || '')
            if (!nk) return
            if (keyTaken(nk)) { alert(`A profile named "${nk}" already exists. Pick a different key.`); return }
            sendCreate(nk, sym)
          }}
          className="px-2 py-1 rounded bg-foreground text-background text-[11px]">+ New profile</button>
      </div>

      {(() => {
        // Group the cards by TICKER, longs before shorts within each. Eight TSLA profiles were
        // scattered down the page in roster order, so the two sides of the same stock — five
        // ladders buying it and three covered shorts selling it — never appeared together.
        const vis = profiles.filter(p => showDisabled || !parkedOff(p) || neverRan(p))
        // Symbols keep their first-appearance order so the list does not reshuffle itself
        // between polls; only the grouping is imposed.
        const symOrder = new Map<string, number>()
        vis.forEach(p => { if (!symOrder.has(p.symbol)) symOrder.set(p.symbol, symOrder.size) })
        // Within a ticker: longs before shorts, then by STAGGER TIER. The tiers are a deliberate
        // ladder — tier 1 sits nearest market and each one below it is offset further — so reading
        // them 1, 2, 3 follows the shape they describe. Untiered profiles (tier 0 = excluded from
        // staggering) sort after the tiered ones rather than ahead of them on a numeric sort.
        // effTier, not staggerTier, so a tier you just changed re-sorts immediately instead of
        // waiting for the push to come back.
        //
        // ONLY when the ticker actually staggers. Staggering is opt-in per ticker (group gap > 0),
        // but compute_stagger() ranks the siblings and writes staggerTier 1..n REGARDLESS — so a
        // ticker with the gap off still carries tier numbers, ranked by max_price, with no band
        // behind them. Sorting on those put RIVN / RIVN-SMALL / RIVN-TINY in an order the card
        // itself calls "Stagger: off". Same gate the card uses to render a tier at all.
        const tiered = (p: Profile) => !!p.staggerGap && (p.staggerSiblings ?? 0) > 1
        const tierKey = (p: Profile) => {
          if (!tiered(p)) return Number.MAX_SAFE_INTEGER
          const t = effTier(p); return t > 0 ? t : Number.MAX_SAFE_INTEGER
        }
        const ordered = [...vis].sort((a, b) => {
          const sa = symOrder.get(a.symbol) ?? 0, sb = symOrder.get(b.symbol) ?? 0
          if (sa !== sb) return sa - sb
          const da = a.direction === 'short' ? 1 : 0, db = b.direction === 'short' ? 1 : 0
          if (da !== db) return da - db
          const ta = tierKey(a), tb = tierKey(b)
          if (ta !== tb) return ta - tb
          return a.profile.localeCompare(b.profile)
        })
        const countBySym = new Map<string, Profile[]>()
        for (const p of ordered) countBySym.set(p.symbol, [...(countBySym.get(p.symbol) ?? []), p])
        return ordered.map((p, idx) => {
        const firstOfSym = idx === 0 || ordered[idx - 1].symbol !== p.symbol
        const sibs = countBySym.get(p.symbol) ?? []
        const holding = p.phase === 'HOLDING' && p.shares > 0
        const short = p.direction === 'short'
        const active = short ? (p.netSold ?? 0) > 0 : holding  // green badge when position is "on"
        // whole-card tint so a stopped profile is obvious at a glance (frozen wins over disabled)
        const tone = p.frozen ? 'border-sky-500/40 bg-sky-500/[0.06]'
          : closingOut(p) ? 'border-amber-500/40 bg-amber-500/[0.06]'
          : p.disabled ? 'border-amber-500/50 bg-amber-500/[0.12]'
          : 'bg-muted/30'
        return (
          <Fragment key={p.profile}>
          {!firstOfSym && short && ordered[idx - 1]?.symbol === p.symbol
            && ordered[idx - 1]?.direction !== 'short' && (
            <div className="flex items-center gap-2 px-1 pt-1" aria-hidden>
              <span className="text-[9px] uppercase tracking-wider text-violet-600">↺ covered shorts</span>
              <span className="flex-1 h-px bg-violet-500/25" />
            </div>
          )}
          {firstOfSym && sibs.length > 1 && (() => {
            const longs = sibs.filter(x => x.direction !== 'short')
            const shorts = sibs.filter(x => x.direction === 'short')
            const equity = longs.reduce((a, x) => a + (x.lastPrice != null ? x.shares * x.lastPrice : 0), 0)
            const unreal = sibs.reduce((a, x) => a + (x.unrealizedPnl || 0), 0)
            return (
              <div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5 px-1 pt-2 pb-0.5">
                <span className="text-xs font-semibold tracking-tight">
                  {p.symbol}
                  <span className="ml-1.5 font-normal text-[10px] text-muted-foreground">
                    {longs.length > 0 && `${longs.length} long`}
                    {longs.length > 0 && shorts.length > 0 && ' · '}
                    {shorts.length > 0 && <span className="text-violet-600">{shorts.length} short</span>}
                  </span>
                </span>
                <span className="text-[10px] tabular-nums text-muted-foreground">
                  {equity > 0 && <>${Math.round(equity).toLocaleString()} equity</>}
                  {equity > 0 && unreal !== 0 && ' · '}
                  {unreal !== 0 && <Money n={unreal} />}
                </span>
              </div>
            )
          })()}
          <div id={`profile-${p.profile}`}
            className={`rounded-2xl border border-l-4 p-4 space-y-3 scroll-mt-4 transition-shadow ${tone} ${
              short ? 'border-l-violet-500' : 'border-l-emerald-600/40'
            } ${flash === p.profile ? 'ring-2 ring-sky-400 ring-offset-2' : ''}`}>
            <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
              <span className="font-semibold tracking-tight shrink-0">{p.profile}</span>
              {p.profile !== p.symbol && (
                <span className="text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground" title="Traded ticker">{p.symbol}</span>
              )}
              {p.lastPrice != null && (
                <span className="text-sm tabular-nums text-muted-foreground">${p.lastPrice.toFixed(2)}</span>
              )}
              <span
                className={`text-[10px] px-2 py-0.5 rounded-full ${
                  active ? 'bg-emerald-500/15 text-emerald-600' : 'bg-muted text-muted-foreground'
                }`}
              >
                {short ? (p.phase || 'ARMED') : (holding ? 'HOLDING' : 'WAITING')}
              </span>
              {short && (
                <span className="text-[10px] px-2 py-0.5 rounded-full bg-violet-500/15 text-violet-600" title="Covered-short / reverse grid (sells owned shares high, buys back lower)">↺ short</span>
              )}
              {short && p.shadow && (
                <span className="text-[10px] px-2 py-0.5 rounded-full bg-amber-500/15 text-amber-600" title="Shadow: computes + logs would-be orders but places nothing">shadow</span>
              )}
              {(() => {
                const act = pending[p.profile]
                if (act) return (
                  <span className="text-[10px] px-2 py-0.5 rounded-full bg-muted text-muted-foreground animate-pulse">
                    {act === 'resume' || act === 'cancel_await' ? 'resuming…'
                      : act === 'set_config' || act === 'clear_config' ? 'saving…'
                      : 'arming…'}
                  </span>
                )
                const paused = (!!p.pausedUntil && p.pausedUntil * 1000 > now) || !!p.pauseHold
                if (!paused) return null
                if (p.awaitPrice != null) return (
                  <span className="text-[10px] px-2 py-0.5 rounded-full bg-violet-500/15 text-violet-600" title="Awaiting your manual order on this rung — bot is hands-off; it reclaims the rung at pre-market open">
                    await ${p.awaitPrice.toFixed(2)}
                  </span>
                )
                if (p.pauseHold) return (
                  <span className="text-[10px] px-2 py-0.5 rounded-full bg-violet-500/15 text-violet-600" title="Pause expired but a manual order is still live — holding resume to avoid a duplicate rung; resumes once it fills/expires">
                    paused · manual order live
                  </span>
                )
                return (
                  <span className="text-[10px] px-2 py-0.5 rounded-full bg-violet-500/15 text-violet-600" title="Manual control — bot not managing this profile; reclaims at pre-market open">
                    paused · {Math.max(1, Math.round((p.pausedUntil! * 1000 - now) / 3600000))}h
                  </span>
                )
              })()}
              {p.frozen && (
                <span className="text-[10px] px-2 py-0.5 rounded-full bg-sky-500/15 text-sky-600 font-medium" title="FROZEN — all resting orders cancelled, position held untouched (no buying or selling) until unfrozen. From the edit panel.">
                  frozen
                </span>
              )}
              {closingOut(p) && (
                <span className="text-[10px] px-2 py-0.5 rounded-full bg-amber-500/15 text-amber-600 font-medium"
                  title={`CLOSING OUT — disabled, but it still holds a position, so the ${short ? 'buy-back' : 'sell'} stays resting and no new ${short ? 'sells' : 'buys'} go out. Once it is flat it becomes disabled and hides with the rest. Enable it to resume the ladder.`}>
                  closing out
                </span>
              )}
              {parkedOff(p) && (
                <span className="text-[10px] px-2 py-0.5 rounded-full bg-amber-500/20 text-amber-700 font-medium" title={p.disabledReason === 'expiry'
                    ? 'RETIRED — its wind-down date passed and its last cycle closed, so the bot disabled it. Places no new opening orders. Enabling it also clears the elapsed wind-down date.'
                    : 'DISABLED — places no new opening orders; keeps the closing order (sell/cover) until the position is flat, then idles. Re-enable from the edit panel.'}>
                  {p.disabledReason === 'expiry' ? 'retired (expired)'
                    : p.disabledReason === 'rename' ? 'disabled for rename' : 'disabled'}
                </span>
              )}
              {p.pendingRename && (
                <span className="text-[10px] px-2 py-0.5 rounded-full bg-violet-500/15 text-violet-700 font-medium"
                  title={`Rename to ${p.pendingRename} is queued. The bot only moves an idle profile's state and ledger files, so it waits until this one has no resting orders and no position — disable it and let the orders clear, and it applies on the next cycle.`}>
                  → {p.pendingRename} pending
                </span>
              )}
              {neverRan(p) && (
                <span className="text-[10px] px-2 py-0.5 rounded-full bg-sky-500/15 text-sky-700 font-medium"
                  title="Has never traded, so it stays on screen while you set it up. Once it takes its first fill it behaves like any other profile, and hides with the rest if you disable it.">
                  never run
                </span>
              )}
              {p.closeHandoff && (
                <span className="text-[10px] px-2 py-0.5 rounded-full bg-violet-500/20 text-violet-700 font-medium" title="HANDED OFF for a manual overnight close — bot management is suspended so you can edit its close order to an overnight session in the app. Reclaim (from the edit panel) once it fills.">
                  manual close
                </span>
              )}
              {p.expiry != null && (
                <span className={`text-[10px] px-2 py-0.5 rounded-full ${p.expiry * 1000 <= now ? 'bg-zinc-500/20 text-zinc-600' : 'bg-amber-500/15 text-amber-600'}`}
                  title={p.expiry * 1000 <= now ? 'EXPIRED — finished its cycle and will not re-arm. Clear the expiry (edit panel) to resume.' : `Winds down on ${new Date(p.expiry * 1000).toLocaleDateString()} — finishes the current cycle, then stops re-arming.`}>
                  {p.expiry * 1000 <= now ? 'expired' : `exp ${new Date(p.expiry * 1000).toLocaleDateString(undefined, { month: 'numeric', day: 'numeric' })}`}
                </span>
              )}
              {p.notInPlay ? (
                <span className="text-[10px] px-2 py-0.5 rounded-full bg-zinc-500/15 text-zinc-600"
                  title="Cash is constrained and this profile isn't picked in Budgets → In play — it gets no new buys until cash frees up for everyone or you change the selection.">
                  not in play
                </span>
              ) : p.funded === false && (
                <span className="text-[10px] px-2 py-0.5 rounded-full bg-amber-500/15 text-amber-600" title="Budget reserved for higher-priority profiles — holds position, places no new buys">
                  on hold
                </span>
              )}
              {p.halted && (
                <span className="text-[10px] px-2 py-0.5 rounded-full bg-red-600 text-white font-medium" title="Profile HALTED — the internal ledger diverged from the broker (oversell risk). No trading until investigated + restarted.">
                  HALTED
                </span>
              )}
              {p.recon && (
                <span
                  className={`text-[10px] px-2 py-0.5 rounded-full ${p.recon.match ? 'bg-emerald-500/15 text-emerald-600' : 'bg-red-500/15 text-red-600'}`}
                  title={`Ledger (position of record) vs broker guardrail — this profile: internal ${p.recon.iShares} sh @ $${p.recon.iAvg.toFixed(2)} | broker ${p.recon.bShares} sh @ $${p.recon.bAvg.toFixed(2)}`
                    + (p.recon.symSigma != null && p.recon.symBroker != null ? ` | ticker Σ ${p.recon.symSigma} vs broker ${p.recon.symBroker} sh` : '')
                    + (p.recon.untracked ? ` | +${p.recon.untracked} sh held outside the bot (benign)` : '')
                    + (p.recon.match ? '' : ` | Δsh ${p.recon.dShares}, Δavg $${p.recon.dAvg.toFixed(3)}`)}
                >
                  {p.recon.match ? 'ledger ✓' : `ledger ✗ Δ${p.recon.dShares}sh`}
                </span>
              )}
              <span className="ml-auto shrink-0 text-[10px] text-muted-foreground">{ago(p.updatedMs, now)}</span>
            </div>

            {p.tranche != null && (
              editSym === p.profile ? (
                <div className="rounded-lg border bg-muted/40 p-2 space-y-2 text-xs -mt-1">
                  <div className="grid grid-cols-2 gap-2">
                    {configFields(short).map(({ key, label, nullable }) => (
                      <label key={key} className="flex flex-col gap-0.5">
                        <span className="text-[9px] uppercase tracking-wide text-muted-foreground">{label}</span>
                        <input
                          type="number" inputMode="decimal" step="any" min="0"
                          value={draft[key] ?? ''}
                          placeholder={nullable ? 'none' : ''}
                          onChange={e => setDraft(d => ({ ...d, [key]: e.target.value }))}
                          className="rounded border bg-background px-2 py-1 tabular-nums"
                        />
                      </label>
                    ))}
                  </div>
                  {(() => {
                    const t = Number(draft.tranche)
                    const sc = Number(draft.tranche_scale_pct) || 0
                    const cap = Math.floor(Number(draft.max_rungs))
                    if (!isFinite(t) || t <= 0 || !isFinite(cap) || cap < 1) return null
                    let sum = 0
                    for (let k = 0; k < cap; k++) sum += t * Math.pow(1 + sc / 100, k)
                    return (
                      <div className="flex items-center justify-between rounded border border-dashed bg-background/60 px-2 py-1">
                        <span className="text-[10px] uppercase tracking-wide text-muted-foreground">{short ? 'Full-ladder sells' : 'Full-ladder allocation'}</span>
                        <span className="tabular-nums font-semibold">${sum.toLocaleString(undefined, { maximumFractionDigits: 0 })}<span className="ml-1 font-normal text-muted-foreground">· {cap} rungs</span></span>
                      </div>
                    )
                  })()}
                  <div className="flex items-center gap-2 flex-wrap">
                    <button type="button" onClick={() => saveEdit(p.profile)}
                      className="px-2 py-1 rounded bg-foreground text-background text-[11px]">Save</button>
                    <button type="button" onClick={() => setEditSym(null)}
                      className="px-2 py-1 rounded bg-muted hover:bg-muted/70 text-[11px]">Cancel</button>
                    <button type="button"
                      onClick={() => {
                        const nk = cleanKey(window.prompt(`Duplicate ${profLabel(p)} as a new profile key.\n\nCopies all params and starts DISABLED — tune it (e.g. tranche), then Enable.${pendingNote(short)}`, nextFreeKey(p.profile)) || '')
                        if (!nk) return
                        if (keyTaken(nk)) { alert(`A profile named "${nk}" already exists. Pick a different key.`); return }
                        // commit first so the copy inherits the values you can see, not the stale ones
                        runPanelAction(p, `duplicated to ${nk}`, () => sendDuplicate(p.profile, nk))
                      }}
                      className="px-2 py-1 rounded bg-muted hover:bg-muted/70 text-[11px]">Duplicate</button>
                    {p.extra && (
                      <button type="button"
                        onClick={() => {
                          const nk = cleanKey(window.prompt(`Rename ${profLabel(p)} to a new profile key.${pendingNote(short)}`, p.profile) || '')
                          if (!nk || nk === p.profile) return
                          if (keyTaken(nk)) { alert(`A profile named "${nk}" already exists. Pick a different key.`); return }
                          // The bot moves state and ledger files, so it only renames an IDLE
                          // profile. One sitting armed with resting rungs never gets there on its
                          // own — it keeps re-placing them — so offer to disable it, let the
                          // rename land, and switch it back on.
                          const busy = !p.disabled && (p.shares > 0 || (p.netSold || 0) > 0
                            || (p.restingBuys?.length || 0) > 0 || (p.sellPrices?.length || 0) > 0)
                          let auto = false
                          if (busy) {
                            const holds = p.shares > 0 || (p.netSold || 0) > 0
                            if (holds) {
                              alert(`${profLabel(p)} holds a position, so it cannot be renamed yet.\n\n`
                                + `The rename moves its state and ledger files, which is only safe once it is flat. `
                                + `Let the position close first, then rename.`)
                              return
                            }
                            auto = confirm(`${profLabel(p)} has resting orders, and a rename can only be applied while it is idle.\n\n`
                              + `Disable it now (cancelling those orders), rename to "${nk}" once they clear, then switch it back on?\n\n`
                              + `It takes a cycle or two. Cancel to leave it running and rename later.`)
                            if (!auto) return
                          }
                          runPanelAction(p, auto ? `disabling, then renaming to ${nk}` : `renamed to ${nk}`,
                            () => sendRename(p.profile, nk, auto))
                        }}
                        className="px-2 py-1 rounded bg-muted hover:bg-muted/70 text-[11px]">Rename</button>
                    )}
                    <button type="button"
                      onClick={() => {
                        if (p.disabled) { runPanelAction(p, 'enabled', () => sendCommand(p.profile, 'enable')); return }
                        if (confirm(`Disable ${profLabel(p)}?\n\nStops placing new ${short ? 'sells' : 'buys'}. If it holds a position it keeps the ${short ? 'buy-back' : 'sell'} until flat, then idles. Re-enable anytime.${pendingNote(short)}`)) { runPanelAction(p, 'disabled', () => sendCommand(p.profile, 'disable')) }
                      }}
                      className={`px-2 py-1 rounded text-[11px] ${p.disabled ? 'bg-emerald-600 text-white' : 'bg-muted hover:bg-muted/70'}`}>{p.disabled ? 'Enable' : 'Disable'}</button>
                    <button type="button"
                      onClick={() => {
                        if (p.frozen) { runPanelAction(p, 'unfrozen', () => sendCommand(p.profile, 'unfreeze')); return }
                        if (confirm(`Freeze ${profLabel(p)}?\n\nCancels ALL its resting orders (buys + sell/cover) and holds the position untouched — no buying or selling — until you unfreeze.${pendingNote(short)}`)) { runPanelAction(p, 'frozen', () => sendCommand(p.profile, 'freeze')) }
                      }}
                      className={`px-2 py-1 rounded text-[11px] ${p.frozen ? 'bg-sky-600 text-white' : 'bg-muted hover:bg-muted/70'}`}>{p.frozen ? 'Unfreeze' : 'Freeze'}</button>
                    {p.closeHandoff ? (
                      <>
                        <button type="button"
                          onClick={() => { if (confirm(`Reclaim ${profLabel(p)} — confirm the manual close has FILLED?\n\nThe bot will book the close at its ${short ? 'buy-back' : 'sell'} target price and re-arm. Only click this AFTER your overnight order has actually filled.${pendingNote(short)}`)) { runPanelAction(p, 'reclaimed', () => sendCommand(p.profile, 'reclaim_close')) } }}
                          className="px-2 py-1 rounded bg-violet-600 text-white text-[11px]">Reclaim (closed)</button>
                        <button type="button"
                          onClick={() => { runPanelAction(p, 'hand-off cancelled', () => sendCommand(p.profile, 'cancel_handoff')) }}
                          className="px-2 py-1 rounded bg-muted hover:bg-muted/70 text-[11px]">Cancel hand-off</button>
                      </>
                    ) : (p.reduceReserved || 0) > 0 ? (
                      <span className="px-2 py-1 text-[11px] text-muted-foreground"
                        title="A manual reduce is holding shares out of the close, so the close isn't the whole position. Cancel the reduce (or let it fill) to hand off the close.">
                        Hand off — after the reduce
                      </span>
                    ) : (
                      <button type="button"
                        onClick={() => { if (confirm(`Hand off ${profLabel(p)} for a manual overnight close?\n\nThe bot suspends ALL management (won't cancel or re-price its orders) so you can edit its resting ${short ? 'buy-back' : 'sell'} order to an overnight session in the Schwab app. When it fills, come back and click Reclaim.${pendingNote(short)}`)) { runPanelAction(p, 'handed off', () => sendCommand(p.profile, 'handoff_close')) } }}
                        className="px-2 py-1 rounded bg-muted hover:bg-muted/70 text-[11px]" title="Suspend the bot so you can edit its close order to overnight in the app">Hand off (overnight)</button>
                    )}
                    <button type="button"
                      onClick={() => {
                        // Deliberately does NOT commit pending edits — this action exists to throw
                        // the overrides away, so saving them first then deleting them is theatre.
                        const c = changedFields(short)
                        const warn = c.length ? `\n\nYour unsaved edits (${c.join(', ')}) will be DISCARDED, not saved.` : ''
                        if (confirm(`Reset ${profLabel(p)} to its config.py defaults?${warn}`)) { sendCommand(p.profile, 'clear_config'); setEditSym(null) }
                      }}
                      className="ml-auto px-2 py-1 rounded text-red-600 text-[11px] hover:underline">Reset to default</button>
                  </div>
                  <div className="flex items-center gap-2 text-[11px] text-muted-foreground">
                    <span title="After this date, the profile finishes its current cycle (including further tranches) but does NOT start a new one — it idles.">Expiry (wind-down)</span>
                    <input type="date"
                      defaultValue={p.expiry ? new Date(p.expiry * 1000).toISOString().slice(0, 10) : ''}
                      onChange={e => {
                        const v = e.target.value
                        if (!v) { sendExpiry(p.profile, null); return }
                        sendExpiry(p.profile, Math.floor(new Date(v + 'T23:59:59').getTime() / 1000))
                      }}
                      className="rounded border bg-background px-2 py-1 text-xs" />
                    {p.expiry && (
                      <button type="button" onClick={() => sendExpiry(p.profile, null)}
                        className="text-[10px] text-red-600 hover:underline">clear</button>
                    )}
                  </div>
                  <div className="text-[9px] text-muted-foreground">Applies on the next poll cycle (~15s). Max price blank = no cap.</div>
                </div>
              ) : (
                <button type="button" onClick={() => openEdit(p)}
                  title="Edit profile configuration"
                  className="block w-full text-left text-[10px] text-muted-foreground hover:text-foreground -mt-1 hover:underline decoration-dotted underline-offset-2">
                  ${p.tranche.toLocaleString()} tranche{p.scalePct ? ` (+${p.scalePct}%/step)` : ''} · {short ? '+' : '-'}{p.firstStepPct}% / {short ? '+' : '-'}{p.stepPct}% · {short ? '-' : '+'}{p.profitPct}% · cap {p.cap ?? '—'}{short ? (p.minPrice ? ` · min $${p.minPrice}` : '') : (p.maxPrice ? ` · max $${p.maxPrice}` : '')}{p.maxCapital ? ` · ${short ? 'sells' : 'alloc'} ~$${(p.maxCapital / 1000).toFixed(1)}k${p.maxShares ? ` / ${p.maxShares} sh` : ''}` : ''} <span className="opacity-60">✎</span>
                </button>
              )
            )}

            {(p.staggerSiblings ?? 0) > 1 && (
              <div className="text-[10px] text-muted-foreground -mt-0.5 flex items-center gap-1 flex-wrap"
                title={short
                  ? "Optional: same-ticker shorts stagger their sell floors so a recovered short doesn't overlap its siblings. Each tier sells its gap % above the flat sibling below it (per-tier, overridable). Live shorts keep their frozen ladder."
                  : "Optional: same-ticker profiles stagger their entries so a recovered profile doesn't overlap its siblings. Each tier's ladder sits its gap % below the flat sibling above it (per-tier, overridable). Held positions keep their frozen ladder."}>
                <span className="opacity-60">⤷</span>
                {!p.staggerGap ? (
                  <span>Stagger: <span className="opacity-70">off</span>
                    <button type="button" onClick={() => submitStagger(p.profile, p.symbol, '10')}
                      className="ml-1 text-sky-600 hover:underline">enable</button>
                  </span>
                ) : effTier(p) === 0 ? (
                  <span>Stagger: <span className="opacity-70">excluded</span>
                    <button type="button" onClick={() => setStaggerMembership(p.profile, true)}
                      className="ml-1 text-sky-600 hover:underline">include</button>
                  </span>
                ) : (<>
                  <span className="inline-flex items-center gap-0.5">
                    Stagger: tier
                    {tierEditKey === p.profile ? (
                      <input autoFocus type="number" min={1} max={p.staggerCount ?? 9} step={1}
                        defaultValue={effTier(p) || 1}
                        onKeyDown={(e) => { if (e.key === 'Enter') submitTier(p.profile, (e.target as HTMLInputElement).value); if (e.key === 'Escape') setTierEditKey(null) }}
                        onBlur={(e) => submitTier(p.profile, e.target.value)}
                        className="w-9 rounded border bg-background px-1 py-0.5 text-[10px] tabular-nums" />
                    ) : (
                      <button type="button" onClick={() => setTierEditKey(p.profile)}
                        title={short ? "Click to set this short's tier order (lower = sells nearer market). Reorders the whole ticker group." : "Click to set this profile's tier order (lower = enters higher). Reorders the whole ticker group."}
                        className="hover:text-foreground hover:underline decoration-dotted underline-offset-2 tabular-nums">
                        {effTier(p)}<span className="opacity-60">✎</span>
                      </button>
                    )}
                    /{p.staggerCount}
                  </span>
                  <span className="opacity-50">·</span>
                  {effTier(p) === 1 ? (() => {
                    // tier 1 = band edge: edits this group's default gap (0 disables the group).
                    // Keyed by SYMBOL:direction so a ticker's long + short groups don't collide.
                    const groupKey = `${p.symbol}:${p.direction ?? 'long'}`
                    return staggerEditSym === groupKey ? (
                      <span className="inline-flex items-center gap-1">
                        gap
                        <input autoFocus type="number" min={0} max={100} step="any" defaultValue={p.staggerGap ?? 10}
                          onKeyDown={(e) => { if (e.key === 'Enter') submitStagger(p.profile, groupKey, (e.target as HTMLInputElement).value); if (e.key === 'Escape') setStaggerEditSym(null) }}
                          onBlur={(e) => submitStagger(p.profile, groupKey, e.target.value)}
                          className="w-11 rounded border bg-background px-1 py-0.5 text-[10px] tabular-nums" />%
                      </span>
                    ) : (
                      <button type="button" onClick={() => setStaggerEditSym(groupKey)}
                        title="The gap between tiers for this group (long/short kept separate) — used by any tier that hasn't set its own. Set 0 to turn staggering off for this group.">
                        <span className="hover:text-foreground hover:underline decoration-dotted underline-offset-2">group gap {p.staggerGap}% <span className="opacity-60">✎</span></span>
                      </button>
                    )
                  })() : (() => {
                    // tier 2+: estimated ceiling/floor from the bot (cumulative), plus this tier's
                    // editable STEP gap (its distance from the tier above/below).
                    const est = p.staggerCeilingEst ?? null
                    const bound = short
                      ? (est != null && p.minPrice != null && est <= p.minPrice)
                      : (est != null && p.maxPrice != null && est >= p.maxPrice)
                    const label = short ? 'floor' : 'ceiling'
                    // live ladder reference while flat — differs from the band estimate when
                    // deployed siblings compress the stagger (they don't compete for entry)
                    const flat = short ? (p.phase === 'ARMED' && (p.netSold ?? 0) === 0) : p.phase === 'WAITING'
                    const live = flat && p.ladderRef != null && est != null && Math.abs(p.ladderRef - est) > est * 0.002
                      ? p.ladderRef : null
                    return (<span className="inline-flex items-center gap-1">
                      <span title={short
                        ? `Estimated sell floor: +${p.staggerOffsetPct}% above the bottom tier's reference${p.minPrice != null ? `, floored at your min $${p.minPrice}` : ''}. Only applies while armed/flat.`
                        : `Estimated ceiling: −${p.staggerOffsetPct}% below the top tier's reference${p.maxPrice != null ? `, capped at your max $${p.maxPrice}` : ''}. Only applies while flat.`}>
                        {est != null ? <>{label} ≈ <span className="tabular-nums font-medium text-foreground/80">${est.toFixed(2)}</span></> : 'staggered'}
                        {bound && <span className="opacity-60"> ({short ? 'floored' : 'capped'})</span>}
                      </span>
                      {live != null && (
                        <span title={`The ladder's ACTUAL ${label} right now. Differs from the band when the profile's own ${short ? 'low-water or min price sits above' : 'high-water sits below'} the band ${label}.`}>
                          <span className="opacity-50">·</span> now <span className="tabular-nums font-medium text-foreground/80">${live.toFixed(2)}</span>
                        </span>
                      )}
                      <span className="opacity-50">·</span>
                      {gapEditKey === p.profile ? (
                        <span className="inline-flex items-center gap-1">gap
                          <input autoFocus type="number" min={0} max={100} step="any" defaultValue={p.staggerStepGap ?? p.staggerGap ?? 10}
                            onKeyDown={(e) => { if (e.key === 'Enter') submitStepGap(p.profile, (e.target as HTMLInputElement).value); if (e.key === 'Escape') setGapEditKey(null) }}
                            onBlur={(e) => submitStepGap(p.profile, e.target.value)}
                            className="w-11 rounded border bg-background px-1 py-0.5 text-[10px] tabular-nums" />%
                        </span>
                      ) : (
                        <span className="inline-flex items-center gap-1">
                          <button type="button" onClick={() => setGapEditKey(p.profile)}
                            title={`This tier's gap from the tier ${short ? 'below' : 'above'} it${p.staggerGapSet ? ` — custom, overrides the group gap ${p.staggerGap}%` : ' — inherits the group gap'}.`}>
                            <span className={`hover:underline decoration-dotted underline-offset-2 ${p.staggerGapSet ? 'text-violet-600' : 'hover:text-foreground'}`}>gap {short ? '+' : '−'}{p.staggerStepGap ?? p.staggerGap}% <span className="opacity-60">✎</span></span>
                          </button>
                          {p.staggerGapSet && (
                            <button type="button" onClick={() => clearStepGap(p.profile)}
                              title={`Clear this override — follow the group gap (${p.staggerGap}%) again.`}
                              className="opacity-70 hover:opacity-100 hover:text-red-600 hover:underline">reset</button>
                          )}
                        </span>
                      )}
                    </span>)
                  })()}
                  <span className="opacity-50">·</span>
                  <button type="button" onClick={() => setStaggerMembership(p.profile, false)}
                    title="Remove this profile from the stagger group — it trades independently and stops counting as a sibling."
                    className="opacity-70 hover:opacity-100 hover:text-red-600 hover:underline">exclude</button>
                </>)}
              </div>
            )}

            {/* Action feedback for the WHOLE card. This used to live inside the stagger row, which
                only renders on a multi-profile ticker — so on a solo profile every "queued"
                confirmation was silently invisible. */}
            {(() => { const m = savedMsg[p.profile]; return m ? (
              <div className={`text-[11px] font-medium ${m.startsWith('✓') ? 'text-emerald-600' : 'text-red-600'}`}>{m}</div>
            ) : null })()}

            <div className="grid grid-cols-2 gap-y-1.5 text-sm">
              {short ? (
                <>
                  <span className="text-muted-foreground">Distributed</span>
                  <span className="text-right tabular-nums">
                    {p.netSold ? `${p.netSold} sh @ $${p.soldAvg?.toFixed(2) ?? '—'}` : '0 (armed)'}
                    {/* `p.netSold && ...` rendered a literal 0 when flat: && returns the number,
                        and React prints it — hence "0 (armed)0". Compare explicitly. */}
                    {(p.netSold ?? 0) > 0 && p.soldAvg != null && (
                      <span className="text-muted-foreground"> · ${(p.netSold! * p.soldAvg).toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
                    )}
                  </span>
                  {(p.netSold ?? 0) > 0 && (
                    <>
                      <span className="text-muted-foreground">Buy-back target</span>
                      <span className="text-right tabular-nums">
                        {p.coverTarget != null ? `$${p.coverTarget.toFixed(2)}` : '—'}
                        {closeGain(p, true) != null && (
                          <span className="text-muted-foreground font-normal"
                            title={`If this buy-back fills: ${p.netSold} sh × $${(p.soldAvg! - p.coverTarget!).toFixed(2)} per share. Not booked until it does.`}>
                            {' · '}{closeGain(p, true)! >= 0 ? '+' : '−'}${Math.abs(closeGain(p, true)!).toFixed(2)}
                          </span>
                        )}
                        {closeControl(p)}
                      </span>
                      <span className="text-muted-foreground">Unrealized</span>
                      <span className="text-right tabular-nums font-medium"><Money n={p.unrealizedPnl} /></span>
                    </>
                  )}
                  <span className="text-muted-foreground">Low-water</span>
                  <span className="text-right tabular-nums">
                    ${p.lowWater?.toFixed(2) ?? '—'}
                    {pending[p.profile] === 'reset_anchor'
                      ? <span className="ml-2 text-[10px] text-muted-foreground">resetting…</span>
                      : (p.netSold ?? 0) === 0 && p.lowWater != null && p.lastPrice != null && (
                        <button type="button"
                          onClick={() => { if (confirm(`Re-anchor ${profLabel(p)} low-water to the current price ($${p.lastPrice!.toFixed(2)})?\n\nResets the sell ladder to market. Only applies while no rung has sold.`)) sendCommand(p.profile, 'reset_anchor') }}
                          className="ml-2 text-[10px] text-violet-600 hover:underline">reset</button>
                      )}
                  </span>
                  <span className="text-muted-foreground">Resting sells</span>
                  <span className="text-right tabular-nums">
                    {p.sellPrices && p.sellPrices.length ? p.sellPrices.map((x, i) => (
                      <span key={i}>
                        {i > 0 && ', '}
                        <button
                          type="button"
                          title="Click to hand off for a manual overnight order at this level (EXTO). The bot suspends this profile so you can edit the resting sell to an overnight session in the app, then reclaims at the open."
                          onClick={() => {
                            if (confirm(`Hand off ${profLabel(p)} to place/edit a manual SELL (distribute) at $${x.toFixed(2)} overnight?\n\nThe bot suspends this profile so you can edit its resting sell to an overnight session in the Schwab app. It reclaims the instant a matching order fills, or at the next session open. Wait for the "await" badge before editing in the brokerage.`))
                              sendCommand(p.profile, 'await_manual', x)
                          }}
                          className="hover:underline decoration-dotted underline-offset-2"
                        >
                          ${x.toFixed(2)}
                        </button>
                        {p.sellPricesSh?.[i] ? <span className="text-muted-foreground"> ({p.sellPricesSh[i]})</span> : null}
                      </span>
                    )) : (p.awaitPrice == null ? '—' : null)}
                    {p.awaitPrice != null && !(p.sellPrices || []).some(x => Math.abs(x - p.awaitPrice!) < 0.005) && (
                      <AwaitedRung price={p.awaitPrice} any={!!(p.sellPrices && p.sellPrices.length)} />
                    )}
                    {p.deferredSells && p.deferredSells.length > 0 && (
                      <span className="text-muted-foreground/40" title="Tracked but not ordered — too far above market; no inventory reserved until price approaches">
                        {(p.sellPrices && p.sellPrices.length) || p.awaitPrice != null ? ', ' : ''}{p.deferredSells.map(x => `$${x.toFixed(2)}`).join(', ')}
                      </span>
                    )}
                    {((p.pausedUntil && p.pausedUntil * 1000 > now) || p.pauseHold) && !pending[p.profile] && (
                      <button type="button" onClick={() => sendCommand(p.profile, 'resume')}
                        className="ml-2 text-[10px] text-violet-600 hover:underline">resume</button>
                    )}
                  </span>
                  <span className="text-muted-foreground">Rungs sold</span>
                  <span className="text-right tabular-nums flex items-center justify-end gap-2">
                    {p.cap ? (
                      <span className="inline-flex h-1.5 w-16 rounded-full bg-muted overflow-hidden" title={`${p.soldRungs ?? 0} of ${p.cap} sold`}>
                        <span className="bg-violet-500/60" style={{ width: `${Math.min(100, ((p.soldRungs ?? 0) / p.cap) * 100)}%` }} />
                      </span>
                    ) : null}
                    <span>{p.soldRungs ?? 0}{p.cap ? ` / ${p.cap}` : ''} sold</span>
                  </span>
                  <span className="text-muted-foreground"
                    title="Shares committed of the ticker's pool: those already distributed PLUS those reserved by resting sell rungs. So it can read above zero while Distributed is 0 — the resting sells are holding inventory against a fill.">Share pool</span>
                  <span className="text-right tabular-nums">{p.poolUsed != null ? `${p.poolUsed} / ${p.poolCap} sh` : '—'}</span>
                </>
              ) : (<>
              {holding ? (
                <>
                  <span className="text-muted-foreground">Position</span>
                  <span className="text-right tabular-nums">
                    {p.shares} sh @ ${p.avg?.toFixed(2)}
                    {p.avg != null && (
                      <span className="text-muted-foreground"> · ${(p.shares * p.avg).toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
                    )}
                  </span>
                  <span className="text-muted-foreground">Sell target</span>
                  <span className="text-right tabular-nums">
                    {p.sellTarget != null ? `$${p.sellTarget.toFixed(2)}` : 'suspended (pre-mkt)'}
                    {/* Muted, not green: this is what the resting order WOULD pay, not money
                        booked. Rendering it in the same colour as realized P&L made a projection
                        look like a result. */}
                    {closeGain(p, false) != null && (
                      <span className="text-muted-foreground font-normal"
                        title={`If this sell fills: ${p.shares} sh × $${(p.sellTarget! - p.avg!).toFixed(2)} per share. Not booked until it does.`}>
                        {' · '}{closeGain(p, false)! >= 0 ? '+' : '−'}${Math.abs(closeGain(p, false)!).toFixed(2)}
                      </span>
                    )}
                    {closeControl(p)}
                  </span>
                  <span className="text-muted-foreground">Unrealized</span>
                  <span className="text-right tabular-nums font-medium"><Money n={p.unrealizedPnl} /></span>
                </>
              ) : (
                <>
                  <span className="text-muted-foreground">High-water</span>
                  <span className="text-right tabular-nums">
                    ${p.highWater?.toFixed(2) ?? '—'}
                    {pending[p.profile] === 'reset_anchor'
                      ? <span className="ml-2 text-[10px] text-muted-foreground">resetting…</span>
                      : p.highWater != null && p.lastPrice != null && (
                        <button type="button"
                          onClick={() => { if (confirm(`Re-anchor ${profLabel(p)} high-water to the current price ($${p.lastPrice!.toFixed(2)})?\n\nResets the ladder to market. Only applies while flat (no tranche filled).`)) sendCommand(p.profile, 'reset_anchor') }}
                          className="ml-2 text-[10px] text-violet-600 hover:underline">reset</button>
                      )}
                  </span>
                </>
              )}
              <span className="text-muted-foreground">Resting buys</span>
              <span className="text-right tabular-nums">
                {p.restingBuys.length ? p.restingBuys.map((x, i) => (
                  <span key={i}>
                    {i > 0 && ', '}
                    <button
                      type="button"
                      title="Click to await a manual order at this level (overnight EXTO). The bot hands off this profile and reclaims at pre-market open."
                      onClick={() => {
                        if (confirm(`Await a manual order for ${profLabel(p)} at $${x.toFixed(2)}?\n\nThe bot will hand off this profile so you can place/edit an EXTO order overnight, then reclaim it at pre-market open. Wait for the "await" badge before editing in the brokerage.`))
                          sendCommand(p.profile, 'await_manual', x)
                      }}
                      className="hover:underline decoration-dotted underline-offset-2"
                    >
                      ${x.toFixed(2)}
                    </button>
                    {p.restingBuysSh?.[i] ? <span className="text-muted-foreground"> ({p.restingBuysSh[i]})</span> : null}
                  </span>
                )) : (p.awaitPrice == null ? '—' : null)}
                {p.awaitPrice != null && !p.restingBuys.some(x => Math.abs(x - p.awaitPrice!) < 0.005) && (
                  <AwaitedRung price={p.awaitPrice} any={p.restingBuys.length > 0} />
                )}
                {p.deferredBuys && p.deferredBuys.length > 0 && (
                  <span className="text-muted-foreground/40" title="Tracked but not ordered — too far below market, so no buying power is reserved until price approaches">
                    {(p.restingBuys.length || p.awaitPrice != null ? ', ' : '')}{p.deferredBuys.map(x => `$${x.toFixed(2)}`).join(', ')}
                  </span>
                )}
                {((p.pausedUntil && p.pausedUntil * 1000 > now) || p.pauseHold) && !pending[p.profile] && (
                  <button
                    type="button"
                    onClick={() => sendCommand(p.profile, 'resume')}
                    className="ml-2 text-[10px] text-violet-600 hover:underline"
                  >
                    resume
                  </button>
                )}
              </span>
              <span className="text-muted-foreground">Tranches</span>
              {(() => {
                // Every rung filled = no dry powder. The ladder cannot average down again; it
                // can only wait for the target. Worth calling out, and worth saying how far
                // that target is — the gap is what has to close before the capital comes back.
                const done = !!p.cap && p.filledCount >= p.cap
                const need = (p.sellTarget && p.lastPrice) ? (p.sellTarget / p.lastPrice - 1) * 100 : null
                return (
                <span className="text-right tabular-nums flex items-center justify-end gap-2">
                  {p.cap ? (
                    <span className="inline-flex h-1.5 w-16 rounded-full bg-muted overflow-hidden" title={`${p.filledCount} of ${p.cap} filled`}>
                      <span className={done ? 'bg-amber-500/70' : 'bg-sky-500/60'} style={{ width: `${Math.min(100, (p.filledCount / p.cap) * 100)}%` }} />
                    </span>
                  ) : null}
                  <span className={done ? 'text-amber-600' : undefined}
                    title={done
                      ? `Ladder exhausted — all ${p.cap} rungs filled, nothing left to average down with.`
                        + (need != null ? ` Needs ${need >= 0 ? '+' : ''}${need.toFixed(1)}% to reach its target.` : '')
                      : `${p.filledCount} of ${p.cap} rungs filled`}>
                    {p.filledCount}{p.cap ? ` / ${p.cap}` : ''} filled
                    {done && <span className="ml-1 text-[10px]">· maxed</span>}
                  </span>
                </span>
                )
              })()}
              </>)}
              {manualRow(p)}
              <div className="col-span-2 border-t border-border/50 mt-1 pt-1.5" />
              <span className="text-muted-foreground">Realized (today)</span>
              <span className="text-right tabular-nums font-medium">
                <Money n={p.realizedToday} />
                {p.lastPnl != null && <span className="text-muted-foreground font-normal"> (last {money(p.lastPnl)})</span>}
              </span>
              <span className="text-muted-foreground text-xs self-center">24h / 7d / 30d{p.realizedAll != null ? ' / all' : ''}</span>
              <span className="text-right tabular-nums text-xs self-center">
                <Money n={p.realized24h} /> · <Money n={p.realized7d} /> · <Money n={p.realized30d} />
                {p.realizedAll != null && <> · <Money n={p.realizedAll} /></>}
              </span>
              {(p.lastSell || p.txnCount) ? (
                <>
                  <button
                    type="button"
                    onClick={() => { setTxnAll(false); setTxnSym(v => (v === p.profile ? null : p.profile)) }}
                    className="text-left text-muted-foreground hover:text-foreground"
                  >
                    Last close <span className="text-[10px]">({txnSym === p.profile ? 'hide' : 'history'})</span>
                  </button>
                  <button
                    type="button"
                    onClick={() => { setTxnAll(false); setTxnSym(v => (v === p.profile ? null : p.profile)) }}
                    className="text-right tabular-nums hover:opacity-70"
                  >
                    {p.lastSell ? (
                      <>
                        {p.lastSell.qty} @ ${p.lastSell.price.toFixed(2)} <Money n={p.lastSell.pnl} />
                        {p.lastSell.trim && <span className="text-[10px] text-violet-600"> reduce</span>}
                        <span className="text-muted-foreground"> · {ago(p.lastSell.ts * 1000, now)}</span>
                      </>
                    ) : (
                      <span className="text-muted-foreground">none yet · {p.txnCount ?? 0} open</span>
                    )}
                  </button>
                </>
              ) : null}
            </div>

            {txnSym === p.profile && (() => {
              const txns = txnData[p.profile]
              return (
              <div className="rounded-lg border bg-muted/40 p-2 space-y-0.5 text-xs">
                <div className="text-[10px] text-muted-foreground mb-1">
                  {p.symbol} transactions · {txns?.length ?? p.txnCount ?? 0}
                </div>
                {/* Only a genuine empty ledger says "none" — while the first fetch is in flight
                    we say so, rather than flashing "No transactions recorded yet." at someone
                    who has hundreds. */}
                {!txns && txnBusy && (
                  <div className="text-muted-foreground">Loading…</div>
                )}
                {txns && !txns.length && (
                  <div className="text-muted-foreground">No transactions recorded yet.</div>
                )}
                {(txnAll ? txns : txns?.slice(0, 12))?.map((tx, i) => (
                  <div key={i} className="flex items-center justify-between tabular-nums">
                    <span>
                      <span className={`font-medium ${tx.action === 'BUY' ? 'text-sky-600' : tx.action === 'COVER' ? 'text-violet-600' : tx.action === 'SELL' && tx.pnl == null ? 'text-violet-600' : 'text-emerald-600'}`}>
                        {tx.action}{tx.manual && <ManualChip />}
                      </span>
                      <span className="text-muted-foreground"> {tx.qty} @ ${tx.price.toFixed(2)}</span>
                    </span>
                    <span className="text-muted-foreground">
                      {tx.pnl != null && <><Money n={tx.pnl} /> · </>}
                      {ago(tx.ts * 1000, now)}
                    </span>
                  </div>
                ))}
                {(txns?.length ?? 0) > 12 && (
                  <button type="button" onClick={() => setTxnAll(v => !v)}
                    className="mt-1 text-[10px] text-muted-foreground hover:text-foreground hover:underline">
                    {txnAll ? 'show less' : `show all ${txns!.length}`}
                  </button>
                )}
              </div>
              )
            })()}

          </div>
          </Fragment>
        )
        })
      })()}
    </div>
  )
}
