// Verdict — live multi-leg strategy builder.
// Combines BTC perp legs + HIP-4 outcome (YES/NO) legs into a single position with a live
// payoff diagram, computed metrics, and one-click sequential submission to /exchange.

(function () {
  const { useState, useEffect, useMemo, useRef } = React;
  const FlashNum  = window.VDFlash.FlashNum;
  const TickArrow = window.VDFlash.TickArrow;

  const BTC_PERP_ASSET_ID = 0; // BTC is asset 0 in the perp universe

  function fmtUsd0(v) { return v == null ? '—' : (v < 0 ? '-$' : '$') + Math.abs(Math.round(v)).toLocaleString(); }
  function fmtUsd2(v) { return v == null ? '—' : (v < 0 ? '-$' : '$') + Math.abs(v).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }
  function fmtCents(p) { return p == null ? '—' : (p * 100).toFixed(1) + '¢'; }
  function uid() { return Math.random().toString(36).slice(2, 9); }

  // Build an outcome leg that carries everything we need: HL asset id (for /exchange),
  // outcome id (for tracking), payoff rule (for the chart math), and a display label.
  function makeOutcomeLeg(outcome, side, size, price, label) {
    const isBucket = outcome && outcome.bucketIdx != null;
    const target = outcome && outcome.parsed && outcome.parsed.targetPriceNum;
    const rule = isBucket
      ? { type: 'range', lo: outcome.bucketLo, hi: outcome.bucketHi }
      : { type: 'binary', target };
    return {
      id: uid(), kind: 'outcome', side, size, price,
      assetId: side === 'YES' ? outcome.yesAssetId : outcome.noAssetId,
      outcomeId: outcome.outcome,
      rule,
      label: label || (isBucket ? outcome.bucketLabel : (side === 'YES' ? 'YES' : 'NO')),
    };
  }

  // ───────────────────────── Smart presets ─────────────────────────
  // HIP-4 outcomes are pure binaries: they pay $1 or $0 at settle, and the price IS the
  // implied probability. The outcome-native strategy categories are:
  //
  //   1. Pure probability bets — buy a side you think the market is mispricing
  //   2. Delta-hedged probability bets — neutralize the perp-equivalent direction so
  //      you're isolating "did it settle on this side?" from "did it move on the way?"
  //   3. Box arbitrage — buy both sides when YES_ask + NO_ask < 1 (free $$$)
  //
  // Hedge sizes for (2) come from the logistic delta dp/d_spot at current state, which
  // we compute live in `build` so the hedge is correctly sized for the moment.
  function deltaYesAtSpot(spot, target, currentSpot, currentMid) {
    if (currentMid == null || currentMid <= 0.001 || currentMid >= 0.999) return 0;
    if (target <= 0) return 0;
    const distRel = (currentSpot - target) / target;
    if (Math.abs(distRel) < 1e-7) return 200 * 0.25 / target; // steep, near target
    const k = -Math.log(1 / currentMid - 1) / distRel;
    const p = 1 / (1 + Math.exp(-k * (spot - target) / target));
    return k * p * (1 - p) / target; // dp/d_spot
  }
  // Hedge BTC quantity to neutralize N YES contracts at current spot:
  //   delta exposure = N * d (USD per $ of spot move)
  //   perp BTC qty   = delta exposure / 1   (1 BTC perp = 1 USD per $ of spot move per BTC held)
  //   But we want BTC qty s.t. perp PnL slope ≈ -binary PnL slope. Slope of binary PnL per
  //   $ of spot: N * d (USD/USD). Slope of perp PnL per $: qtyBTC. So qtyBTC = N * d.
  function hedgeBtcQtyForYes(yesContracts, spot, target, currentMid) {
    return yesContracts * deltaYesAtSpot(spot, target, spot, currentMid);
  }
  function hedgeBtcQtyForNo(noContracts, spot, target, currentMid) {
    // d(NO)/d_spot = -d(YES)/d_spot, so perp side flips from short → long
    return noContracts * deltaYesAtSpot(spot, target, spot, currentMid);
  }
  // Find the bucket whose [lo, hi) range contains the given spot
  function bucketContainingSpot(buckets, spot) {
    if (spot == null) return null;
    for (const b of buckets) {
      const lo = b.bucketLo, hi = b.bucketHi;
      if ((lo == null || spot >= lo) && (hi == null || spot < hi)) return b;
    }
    return null;
  }

  // Each preset's `needs` field gates availability based on what HL has live for the asset:
  //   binary       — at least one priceBinary outcome
  //   bucket       — a priceBucket question with named outcomes
  //   perp         — perp price is streaming
  //   multi-expiry — two or more expiries on the same underlying (not yet on HL)
  // Multiple needs are combined with '+' (e.g. "binary+perp" requires both).
  const PRESETS = [
    // ────── Single binary bets ──────
    {
      id: 'prob-yes', name: 'Bet YES', tag: '+Y', group: 'directional',
      needs: 'binary',
      simple: 'Buy YES contracts on the binary. Each pays $1 if YES wins at settle, $0 if NO wins.',
      howItWorks: 'Single leg. Pay YES ask × contracts upfront. At settle: collect $1 per contract if YES, lose ask per contract if NO.',
      bestWhen: 'You think YES is more likely than the market is pricing. Edge = (your probability estimate) − (YES ask).',
      risk: '100% of premium at risk. Lose everything if NO wins.',
      build: ({ binary, yesAsk }) => ([
        makeOutcomeLeg(binary, 'YES', 100, yesAsk || 0.5),
      ]),
    },
    {
      id: 'prob-no', name: 'Bet NO', tag: '+N', group: 'directional',
      needs: 'binary',
      simple: 'Buy NO contracts on the binary. Each pays $1 if NO wins at settle, $0 if YES wins.',
      howItWorks: 'Single leg. Pay NO ask × contracts upfront. At settle: collect $1 per contract if NO, lose ask per contract if YES.',
      bestWhen: 'You think NO is more likely than the market is pricing. Edge = (your probability estimate) − (NO ask).',
      risk: '100% of premium at risk. Lose everything if YES wins.',
      build: ({ binary, noAsk }) => ([
        makeOutcomeLeg(binary, 'NO', 100, noAsk || 0.5),
      ]),
    },

    // ────── Hedged binary bets ──────
    {
      id: 'delta-yes', name: 'Bet YES, hedged', tag: '+Y / -P', group: 'hedged',
      needs: 'binary+perp',
      simple: 'Buy YES + short the perp at the binary\'s current delta. For short holds, your P&L tracks the YES probability rather than the price.',
      howItWorks: 'Long N YES + short Δ·N BTC perp, where Δ = dp/d_spot at current state from a logistic calibrated to the live YES mid. Cancels linear spot sensitivity at entry.',
      bestWhen: 'Short holds or near-the-money positions where you want pure probability exposure. The closer you are to target, the higher the gamma decay — recompute hedge often.',
      risk: 'Static hedge ratio degrades as spot moves (gamma). Perp funding accrues over time. Perp keeps unbounded directional exposure at settle.',
      build: ({ binary, perpMid, target, yesAsk, currentMid }) => {
        const N = 100;
        const qty = +hedgeBtcQtyForYes(N, perpMid, target, currentMid).toFixed(4);
        return [
          makeOutcomeLeg(binary, 'YES', N, yesAsk || 0.5),
          { id: uid(), kind: 'perp', side: 'short', size: Math.max(qty, 0.001), price: perpMid },
        ];
      },
    },
    {
      id: 'delta-no', name: 'Bet NO, hedged', tag: '+N / +P', group: 'hedged',
      needs: 'binary+perp',
      simple: 'Buy NO + long the perp. Mirror of the YES hedge — pure NO probability exposure, neutralized at entry.',
      howItWorks: 'Long N NO + long Δ·N BTC perp. d(NO)/d_spot is negative so the matching perp leg is long.',
      bestWhen: 'Same as YES-hedged, opposite side. Short holds, near-the-money positions.',
      risk: 'Same gamma decay as YES-hedged. Funding accrues. Static hedge ratio drifts as spot moves.',
      build: ({ binary, perpMid, target, noAsk, currentMid }) => {
        const N = 100;
        const qty = +hedgeBtcQtyForNo(N, perpMid, target, currentMid).toFixed(4);
        return [
          makeOutcomeLeg(binary, 'NO', N, noAsk || 0.5),
          { id: uid(), kind: 'perp', side: 'long', size: Math.max(qty, 0.001), price: perpMid },
        ];
      },
    },
    {
      id: 'insured-long', name: 'Insured Long', tag: '+P / +N', group: 'hedged',
      needs: 'binary+perp',
      simple: 'Long the perp + buy NO on the binary as a downside hedge. NO pays $1 if BTC settles below target, partially offsetting perp losses.',
      howItWorks: 'Long N BTC perp + long M NO contracts. If BTC drops below target at settle, NO pays $M while perp loses (settle − entry)·N — combined loss is bounded. If BTC rallies above target, perp gains while NO premium is sunk cost.',
      bestWhen: 'Bullish on the underlying but want a defined-loss tail hedge for the case where BTC settles below the binary\'s target.',
      risk: 'NO premium is a sunk cost if BTC stays above target. The hedge pays a fixed $1 regardless of how far below target BTC drops, so for large drops the perp loss exceeds NO\'s payout.',
      build: ({ binary, perpMid, noAsk }) => {
        // Size NO so its payout if BTC settles at target ≈ perp loss at the same level.
        // For a $5,000 perp notional (0.05 BTC × ~$100k), 200 NO contracts = $200 cushion;
        // we tune this based on perpMid so the hedge feels meaningful.
        const perpQty = 0.05;
        const M = Math.max(50, Math.round(perpQty * perpMid * 0.04));
        return [
          { id: uid(), kind: 'perp', side: 'long', size: perpQty, price: perpMid },
          makeOutcomeLeg(binary, 'NO', M, noAsk || 0.5, 'Downside hedge'),
        ];
      },
    },
    {
      id: 'box-arb', name: 'Box arbitrage', tag: '+Y / +N', group: 'arb',
      needs: 'binary',
      simple: 'Buy BOTH sides of the binary. Exactly one pays $1 at settle. If combined ask < $1, you profit by the difference (minus fees).',
      howItWorks: 'Long N YES + long N NO. At settle one wins ($N), the other pays $0. Payoff per pair = $1 − (YES_ask + NO_ask).',
      bestWhen: 'Only when YES_ask + NO_ask < $1.00. Spreads usually swallow this, but new markets and thin order books occasionally let it through.',
      risk: 'HL trading fees reduce or eliminate the profit. If combined cost ≥ $1, guaranteed loss equal to the spread.',
      build: ({ binary, yesAsk, noAsk }) => ([
        makeOutcomeLeg(binary, 'YES', 100, yesAsk || 0.5),
        makeOutcomeLeg(binary, 'NO',  100, noAsk  || 0.5),
      ]),
    },

    // ────── Multi-bucket strategies ──────
    {
      id: 'range-bet', name: 'Range Bet', tag: '+B', group: 'directional',
      needs: 'bucket',
      simple: 'Buy YES on the bucket containing current spot. Pays $1 per contract if BTC settles inside that range at expiry.',
      howItWorks: 'Single leg on the multi-bucket question. The build picks the bucket whose price range contains current spot. At settle: $1 if BTC ends in that range, $0 otherwise.',
      bestWhen: 'You think BTC stays range-bound near current spot through expiry. Pricing edge = (your range probability) − (bucket ask).',
      risk: '100% premium at risk if BTC breaks the range in either direction. Bucket markets typically have wider spreads than the binary.',
      build: ({ question, currentSpot }) => {
        const buckets = (question && question.namedOutcomes) || [];
        if (!buckets.length) return [];
        const target = bucketContainingSpot(buckets, currentSpot) || buckets[Math.floor(buckets.length/2)];
        return [makeOutcomeLeg(target, 'YES', 100, (target.books.yes && target.books.yes.ask) || 0.5, 'Spot range')];
      },
    },
    {
      id: 'tail-bet', name: 'Tail Bet (volatility)', tag: '+B / +B', group: 'volatility',
      needs: 'bucket',
      simple: 'Buy YES on the lowest AND highest bucket. Pays $1 per pair if BTC moves sharply in either direction.',
      howItWorks: 'Two legs on the multi-bucket question. Long lowest-bucket YES + long highest-bucket YES. At settle exactly one tail wins ($1) if BTC ends outside the middle range; both lose if it stays in the middle.',
      bestWhen: 'Expecting a sharp move in either direction (volatility play) — useful before catalysts (CPI, FOMC, earnings) without taking direction.',
      risk: 'You pay both tail premiums upfront. If BTC stays range-bound through settle, the full combined premium is lost. Cheaper than buying the middle bucket\'s NO when tails are underpriced.',
      build: ({ question }) => {
        const buckets = (question && question.namedOutcomes) || [];
        if (buckets.length < 2) return [];
        const lo = buckets[0];
        const hi = buckets[buckets.length - 1];
        return [
          makeOutcomeLeg(lo, 'YES', 100, (lo.books.yes && lo.books.yes.ask) || 0.5, 'Lower tail'),
          makeOutcomeLeg(hi, 'YES', 100, (hi.books.yes && hi.books.yes.ask) || 0.5, 'Upper tail'),
        ];
      },
    },
    {
      id: 'bucket-box', name: 'Bucket arbitrage', tag: '+B…', group: 'arb',
      needs: 'bucket',
      simple: 'Buy YES on EVERY bucket (including the fallback). Always pays $1 — pure arb if combined cost < $1.',
      howItWorks: 'N legs (one per bucket). Combined cost = sum of all bucket asks. Exactly one bucket wins at settle, paying $1 per contract; the rest pay $0.',
      bestWhen: 'When the bucket asks sum to less than $1.00 (check the "Combined cost" metric). Most common right after new questions list while order books are thin.',
      risk: 'Trading fees reduce profit on the winning leg. If combined cost ≥ $1.00, guaranteed loss. The fallback bucket can also win if BTC settles between defined ranges in odd ways — make sure it\'s included.',
      build: ({ question }) => {
        const buckets = (question && question.namedOutcomes) || [];
        const fb = question && question.fallbackOutcome;
        const all = fb ? buckets.concat([fb]) : buckets;
        return all.map(b => makeOutcomeLeg(b, 'YES', 100, (b.books.yes && b.books.yes.ask) || 0.5, b.bucketLabel || 'Fallback'));
      },
    },

    // ────── Multi-expiry (placeholder, not tradeable on HL today) ──────
    {
      id: 'calendar', name: 'Calendar Spread', tag: '+Y(near) / -Y(far)', group: 'multi-expiry',
      needs: 'multi-expiry',
      simple: 'Buy YES on the near-term binary + sell YES on the longer-term binary at the same target. Bets on resolution timing rather than direction.',
      howItWorks: 'Two legs on the same target across two expiries: long YES (near) + short YES (far). If YES wins by the near expiry, pocket near-term YES − initial credit; if it doesn\'t resolve YES until later, near-term loses but far-term short profits.',
      bestWhen: 'You expect YES to resolve sooner rather than later (or vice versa for the reverse spread). Useful around catalyst dates clustered inside the near-term window.',
      risk: 'Selling outcome contracts requires margin/collateral on HL. If YES never wins, both legs settle to opposite values — net P&L depends on credit paid.',
      build: () => [],   // placeholder
    },
  ];

  // ───────────────────────── Asset universe ─────────────────────────
  // BTC is the only underlying with a live HIP-4 outcome on mainnet today; equity perps
  // exist via HL's xyz/cash/km builder DEXs but no equity outcomes yet — patched in
  // when they ship.
  const ASSETS = [
    { sym: 'BTC',   name: 'Bitcoin',   perpAssetId: 0,        hasOutcome: true  },
    { sym: 'NVDA',  name: 'NVIDIA',    perpCoin: 'xyz:NVDA',  hasOutcome: false },
    { sym: 'AAPL',  name: 'Apple',     perpCoin: 'xyz:AAPL',  hasOutcome: false },
    { sym: 'MSFT',  name: 'Microsoft', perpCoin: 'xyz:MSFT',  hasOutcome: false },
    { sym: 'TSLA',  name: 'Tesla',     perpCoin: 'xyz:TSLA',  hasOutcome: false },
    { sym: 'GOOGL', name: 'Alphabet',  perpCoin: 'xyz:GOOGL', hasOutcome: false },
    { sym: 'AMZN',  name: 'Amazon',    perpCoin: 'xyz:AMZN',  hasOutcome: false },
  ];

  // ───────────────────────── Payoff math ─────────────────────────
  // Mode = 'settle': binary outcome resolves (YES pays $1 if spot > target, else $0).
  // Mode = 'now':    outcome leg is marked at the current implied probability given the
  //   hypothetical spot — calibrated logistic centered at target, anchored to the
  //   real currentOutcomeMid at the actual currentSpot. Captures pre-settle MTM.
  function impliedProbAtSpot(spot, target, currentSpot, currentMid) {
    if (currentMid == null || currentMid <= 0.001 || currentMid >= 0.999) {
      // Fallback when outcome is bid at extremes — use a steep step at target.
      return spot > target ? 0.999 : 0.001;
    }
    if (target <= 0) return 0.5;
    const distRel = (currentSpot - target) / target;
    if (Math.abs(distRel) < 1e-7) {
      return 1 / (1 + Math.exp(-200 * (spot - target) / target));
    }
    // Solve k from logistic: currentMid = 1 / (1 + exp(-k * distRel))
    // → k = -ln(1/currentMid - 1) / distRel
    const k = -Math.log(1 / currentMid - 1) / distRel;
    const z = (spot - target) / target;
    return 1 / (1 + Math.exp(-k * z));
  }
  // Returns true if the outcome leg's win condition is met at this hypothetical spot.
  function outcomeWins(leg, spot, ctxTarget) {
    const r = leg.rule;
    if (r && r.type === 'range') {
      const inRange = (r.lo == null || spot >= r.lo) && (r.hi == null || spot < r.hi);
      return leg.side === 'YES' ? inRange : !inRange;
    }
    if (r && r.type === 'binary') {
      const above = spot > r.target;
      return leg.side === 'YES' ? above : !above;
    }
    // Legacy fallback: use ctx.target with side
    const above = spot > ctxTarget;
    return leg.side === 'YES' ? above : !above;
  }
  function legPnl(leg, spot, ctx) {
    const sz = +leg.size || 0;
    const px = +leg.price || 0;
    if (leg.kind === 'perp') {
      const dir = leg.side === 'long' ? 1 : -1;
      return dir * (spot - px) * sz;
    }
    if (leg.kind === 'outcome') {
      if (ctx.mode === 'settle') {
        return outcomeWins(leg, spot, ctx.target) ? sz * (1 - px) : -sz * px;
      }
      // mode === 'now' — MTM model: cleanly defined for binary legs (calibrated logistic).
      // For range/bucket legs we don't have a calibrated model with one mid value, so
      // we treat them as if the price stays at entry — closing now nets ~0.
      if (leg.rule && leg.rule.type === 'range') {
        return 0;
      }
      const probYes = impliedProbAtSpot(spot, ctx.target, ctx.currentSpot, ctx.currentMid);
      const sideValue = leg.side === 'YES' ? probYes : (1 - probYes);
      return sz * (sideValue - px);
    }
    return 0;
  }
  function totalPnl(legs, spot, ctx) {
    let s = 0;
    for (const l of legs) s += legPnl(l, spot, ctx);
    return s;
  }
  function legCost(leg) {
    const sz = +leg.size || 0;
    const px = +leg.price || 0;
    if (leg.kind === 'perp') return 0;        // perp is margin, not premium — show 0 cost
    if (leg.kind === 'outcome') return sz * px;
    return 0;
  }

  // ───────────────────────── Payoff chart ─────────────────────────
  // Two overlaid curves:
  //   solid (acid)      — payoff IF held to settle (binary cliff at target)
  //   dashed (info)     — payoff IF marked-to-market right now at the hypothetical spot
  // The gap between them shows "how much value time-decay gives back at settle vs. now".
  function PayoffChart({ legs, target, currentSpot, currentMid, sym, height = 240 }) {
    const wrapRef = useRef(null);
    const [size, setSize] = useState({ w: 600, h: height });
    const [hoverX, setHoverX] = useState(null);

    useEffect(() => {
      if (!wrapRef.current) return;
      const ro = new ResizeObserver((es) => {
        const cr = es[0].contentRect;
        setSize({ w: Math.max(320, Math.floor(cr.width)), h: height });
      });
      ro.observe(wrapRef.current);
      return () => ro.disconnect();
    }, [height]);

    if (currentSpot == null || target == null) return null;

    const xRange = useMemo(() => {
      const center = currentSpot;
      const span = Math.max(currentSpot * 0.07, Math.abs(target - currentSpot) * 1.6);
      return { xMin: center - span, xMax: center + span };
    }, [currentSpot, target]);

    const SAMPLES = 240;
    const ctxSettle = { mode: 'settle', target, currentSpot, currentMid };
    const ctxNow    = { mode: 'now',    target, currentSpot, currentMid };

    const series = useMemo(() => {
      const { xMin, xMax } = xRange;
      const settle = [], now = [];
      for (let i = 0; i < SAMPLES; i++) {
        const x = xMin + (i / (SAMPLES - 1)) * (xMax - xMin);
        settle.push({ x, y: totalPnl(legs, x, ctxSettle) });
        now.push   ({ x, y: totalPnl(legs, x, ctxNow) });
      }
      return { settle, now };
    }, [legs, target, xRange, currentSpot, currentMid]);

    const M = { l: 12, r: 56, t: 28, b: 28 };
    const W = size.w, H = size.h;
    const innerW = Math.max(10, W - M.l - M.r);
    const innerH = Math.max(10, H - M.t - M.b);

    // Y-range covers both curves so they share a scale
    const allY = series.settle.map(p => p.y).concat(series.now.map(p => p.y)).concat([0]);
    const yMinR = Math.min(...allY);
    const yMaxR = Math.max(...allY);
    const yPad = Math.max((yMaxR - yMinR) * 0.10, 1);
    const yMin = yMinR - yPad;
    const yMax = yMaxR + yPad;

    const xScale = (x) => M.l + ((x - xRange.xMin) / (xRange.xMax - xRange.xMin)) * innerW;
    const yScale = (y) => M.t + (1 - (y - yMin) / (yMax - yMin || 1)) * innerH;

    const zeroY = yScale(0);
    const targetX = xScale(target);
    const spotX   = xScale(currentSpot);

    function pathFor(seq) {
      return seq.map((p, i) => `${i === 0 ? 'M' : 'L'} ${xScale(p.x).toFixed(2)} ${yScale(p.y).toFixed(2)}`).join(' ');
    }
    function areaFor(seq, predicate) {
      let d = '', inside = false;
      for (let i = 0; i < seq.length; i++) {
        const p = seq[i];
        if (predicate(p.y)) {
          if (!inside) { d += `M ${xScale(p.x).toFixed(2)} ${zeroY} L ${xScale(p.x).toFixed(2)} ${yScale(p.y).toFixed(2)}`; inside = true; }
          else d += ` L ${xScale(p.x).toFixed(2)} ${yScale(p.y).toFixed(2)}`;
        } else if (inside) {
          d += ` L ${xScale(p.x).toFixed(2)} ${zeroY} Z`; inside = false;
        }
      }
      if (inside) {
        const lp = seq[seq.length - 1];
        d += ` L ${xScale(lp.x).toFixed(2)} ${zeroY} Z`;
      }
      return d;
    }

    const settlePath = pathFor(series.settle);
    const nowPath    = pathFor(series.now);
    const greenAreaD = areaFor(series.settle, y => y >= 0);
    const redAreaD   = areaFor(series.settle, y => y < 0);

    function onMove(e) { const r = e.currentTarget.getBoundingClientRect(); setHoverX(e.clientX - r.left); }
    function onLeave() { setHoverX(null); }

    let hover = null;
    if (hoverX != null) {
      let best = 0, bestDist = Infinity;
      for (let i = 0; i < series.settle.length; i++) {
        const sx = xScale(series.settle[i].x);
        const d = Math.abs(sx - hoverX);
        if (d < bestDist) { best = i; bestDist = d; }
      }
      const ps = series.settle[best];
      const pn = series.now[best];
      hover = {
        x: xScale(ps.x),
        ySettle: yScale(ps.y), yNow: yScale(pn.y),
        spot: ps.x, settlePnl: ps.y, nowPnl: pn.y,
      };
    }

    return (
      <div ref={wrapRef} style={{ width: '100%' }}>
        {/* Legend */}
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
          <div style={{ display: 'flex', gap: 14, alignItems: 'center' }}>
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 11, color: 'var(--text-2)' }}>
              <svg width="22" height="6"><line x1="0" y1="3" x2="22" y2="3" stroke="var(--acid)" strokeWidth="2" /></svg>
              At settle (binary)
            </span>
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 11, color: 'var(--text-2)' }}>
              <svg width="22" height="6"><line x1="0" y1="3" x2="22" y2="3" stroke="var(--info)" strokeWidth="2" strokeDasharray="3 2" /></svg>
              If priced now (MTM)
            </span>
          </div>
        </div>

        <div style={{ position: 'relative', width: '100%', height: H, overflow: 'hidden' }}>
          <svg width={W} height={H} style={{ display: 'block' }}>
            {/* Areas — driven by the SETTLE curve so the cliff is unmistakable */}
            <path d={greenAreaD} fill="rgba(107,229,193,0.14)" />
            <path d={redAreaD}   fill="rgba(255,92,110,0.14)" />

            {/* Zero line */}
            <line x1={M.l} y1={zeroY} x2={M.l + innerW} y2={zeroY}
                  stroke="var(--text-3)" strokeOpacity="0.5" strokeDasharray="3 3" />

            {/* Target marker */}
            <line x1={targetX} y1={M.t} x2={targetX} y2={M.t + innerH}
                  stroke="var(--acid)" strokeOpacity="0.5" strokeDasharray="4 4" />
            <text x={targetX} y={M.t - 4} textAnchor="middle"
                  style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 9, fill: 'var(--acid)', letterSpacing: '0.06em' }}>
              TARGET {fmtUsd0(target)}
            </text>

            {/* Spot marker */}
            <line x1={spotX} y1={M.t} x2={spotX} y2={M.t + innerH}
                  stroke="var(--mint)" strokeOpacity="0.6" />
            <text x={spotX} y={H - 6} textAnchor="middle"
                  style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 9, fill: 'var(--mint)' }}>
              SPOT {fmtUsd0(currentSpot)}
            </text>

            {/* Curves */}
            <path d={nowPath}    fill="none" stroke="var(--info)" strokeWidth="1.4" strokeLinejoin="round" strokeDasharray="4 3" opacity="0.95" />
            <path d={settlePath} fill="none" stroke="var(--acid)" strokeWidth="1.8" strokeLinejoin="round" />

            {/* Y labels */}
            <text x={M.l + innerW + 6} y={yScale(yMaxR)} dy="0.32em" style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, fill: 'var(--mint)' }}>{fmtUsd0(yMaxR)}</text>
            <text x={M.l + innerW + 6} y={yScale(yMinR)} dy="0.32em" style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, fill: 'var(--coral)' }}>{fmtUsd0(yMinR)}</text>
            <text x={M.l + innerW + 6} y={zeroY}        dy="0.32em" style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, fill: 'var(--text-3)' }}>$0</text>

            {/* Hover crosshair: dot on each curve */}
            {hover && (
              <g pointerEvents="none">
                <line x1={hover.x} y1={M.t} x2={hover.x} y2={M.t + innerH}
                      stroke="var(--text-2)" strokeOpacity="0.4" strokeDasharray="3 3" />
                <circle cx={hover.x} cy={hover.ySettle} r="4" fill="var(--bg-base)" stroke="var(--acid)" strokeWidth="1.5" />
                <circle cx={hover.x} cy={hover.yNow}    r="3" fill="var(--bg-base)" stroke="var(--info)" strokeWidth="1.4" />
              </g>
            )}

            {/* Mouse capture */}
            <rect x={M.l} y={M.t} width={innerW} height={innerH} fill="transparent"
                  onMouseMove={onMove} onMouseLeave={onLeave} style={{ cursor: 'crosshair' }} />
          </svg>

          {hover && (
            <div style={{
              position: 'absolute',
              left: Math.max(8, Math.min(W - 200, hover.x + 12)),
              top:  Math.max(8, Math.min(H - 88, Math.min(hover.ySettle, hover.yNow) - 56)),
              padding: '10px 12px',
              background: 'var(--bg-elev-3)',
              border: '1px solid var(--line)',
              borderRadius: 'var(--r)',
              pointerEvents: 'none',
              minWidth: 180,
            }}>
              <div className="cap-sm">if {sym} at</div>
              <div className="num" style={{ fontSize: 14, fontWeight: 600, marginBottom: 6 }}>{fmtUsd0(hover.spot)}</div>
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
                <div>
                  <div className="cap-sm" style={{ color: 'var(--acid)' }}>at settle</div>
                  <div className="num" style={{ fontSize: 13, fontWeight: 700, color: hover.settlePnl >= 0 ? 'var(--mint)' : 'var(--coral)' }}>
                    {hover.settlePnl >= 0 ? '+' : ''}{fmtUsd2(hover.settlePnl)}
                  </div>
                </div>
                <div>
                  <div className="cap-sm" style={{ color: 'var(--info)' }}>now MTM</div>
                  <div className="num" style={{ fontSize: 13, fontWeight: 700, color: hover.nowPnl >= 0 ? 'var(--mint)' : 'var(--coral)' }}>
                    {hover.nowPnl >= 0 ? '+' : ''}{fmtUsd2(hover.nowPnl)}
                  </div>
                </div>
              </div>
            </div>
          )}
        </div>
      </div>
    );
  }

  // ───────────────────────── Leg editor ─────────────────────────
  function LegRow({ leg, idx, btcMid, outcome, onChange, onRemove }) {
    const yesMid = outcome.mid;
    const noMid  = outcome.mid != null ? 1 - outcome.mid : null;
    const yesTop = outcome.books.yes || {};
    const noTop  = outcome.books.no || {};

    function setField(k, v) { onChange({ ...leg, [k]: v }); }
    const isPerp = leg.kind === 'perp';
    const accent = isPerp
      ? (leg.side === 'long' ? 'var(--mint)' : 'var(--coral)')
      : (leg.side === 'YES' ? 'var(--mint)' : 'var(--coral)');

    const cost = legCost(leg);
    const sideLabel = isPerp ? leg.side.toUpperCase() : leg.side;
    const sizeLabel = isPerp ? 'BTC' : 'contracts';

    return (
      <div style={{
        display: 'grid', gridTemplateColumns: '40px 110px 100px 1fr 1fr 80px 32px',
        gap: 10, alignItems: 'center', padding: '10px 12px',
        borderBottom: '1px solid var(--line-subtle)',
      }}>
        <span className="cap-sm" style={{ color: 'var(--text-3)' }}>{String(idx + 1).padStart(2, '0')}</span>
        <span className="cap-sm" style={{
          color: accent, fontWeight: 700, padding: '4px 8px',
          border: '1px solid currentColor', textAlign: 'center', letterSpacing: '0.06em',
        }}>
          {isPerp ? 'PERP' : 'OUTCOME'}
        </span>
        {/* Side toggle */}
        <div style={{ display: 'flex', gap: 0, border: '1px solid var(--line-subtle)' }}>
          {(isPerp ? ['long','short'] : ['YES','NO']).map(s => {
            const active = s === leg.side;
            const c = (s === 'long' || s === 'YES') ? 'var(--mint)' : 'var(--coral)';
            return (
              <button key={s} onClick={() => setField('side', s)} className="cap-sm" style={{
                flex: 1, padding: '4px 0',
                color: active ? c : 'var(--text-3)',
                background: active ? (s === 'long' || s === 'YES' ? 'var(--mint-bg10)' : 'var(--coral-bg10)') : 'transparent',
              }}>{s.toUpperCase().slice(0, 1)}</button>
            );
          })}
        </div>
        {/* Size input */}
        <div>
          <input type="number" min="0" step={isPerp ? '0.001' : '1'} value={leg.size}
                 onChange={(e) => setField('size', e.target.value)}
                 className="num"
                 style={{
                   width: '100%', padding: '6px 8px', fontSize: 13,
                   background: 'var(--bg-elev-2)', border: '1px solid var(--line-subtle)',
                   color: 'var(--text)',
                 }} />
          <div className="cap-sm" style={{ fontSize: 9, color: 'var(--text-4)', marginTop: 2 }}>{sizeLabel}</div>
        </div>
        {/* Price input */}
        <div>
          <input type="number" min="0" step={isPerp ? '1' : '0.001'} value={leg.price}
                 onChange={(e) => setField('price', e.target.value)}
                 className="num"
                 style={{
                   width: '100%', padding: '6px 8px', fontSize: 13,
                   background: 'var(--bg-elev-2)', border: '1px solid var(--line-subtle)',
                   color: 'var(--text)',
                 }} />
          <div className="cap-sm" style={{ fontSize: 9, color: 'var(--text-4)', marginTop: 2 }}>
            {isPerp ? `mid ${fmtUsd0(btcMid)}` : `mid ${fmtCents(leg.side === 'YES' ? yesMid : noMid)}`}
          </div>
        </div>
        {/* Cost */}
        <div className="num" style={{ textAlign: 'right', fontSize: 12 }}>
          {isPerp ? <span style={{ color: 'var(--text-3)' }}>margin</span> : fmtUsd0(cost)}
        </div>
        {/* Remove */}
        <button onClick={onRemove} title="Remove leg" style={{
          width: 24, height: 24, color: 'var(--text-3)', fontSize: 14,
          border: '1px solid var(--line-subtle)',
        }}>×</button>
      </div>
    );
  }

  // ───────────────────────── Submit-all panel ─────────────────────────
  function SubmitAll({ legs, outcome, btcMid, wallet, onConnect, perpAssetId = BTC_PERP_ASSET_ID }) {
    const [phase, setPhase] = useState('idle'); // idle | working | done | error
    const [progress, setProgress] = useState([]); // per-leg results: [{leg, status, msg, oid?}]
    const totalCost = legs.reduce((s, l) => s + legCost(l), 0);

    async function placeAll() {
      if (!wallet || !wallet.address) { onConnect(); return; }
      if (legs.length === 0) return;
      setPhase('working');
      const results = legs.map(l => ({ leg: l, status: 'pending' }));
      setProgress(results.slice());
      let anyError = false;
      for (let i = 0; i < legs.length; i++) {
        const l = legs[i];
        const isPerp = l.kind === 'perp';
        const assetId = isPerp
          ? perpAssetId
          : (l.assetId != null
              ? l.assetId
              : (l.side === 'YES' ? outcome.yesAssetId : outcome.noAssetId));
        const isBuy = isPerp ? (l.side === 'long') : true;     // outcome legs are always "buy YES" or "buy NO"
        results[i] = { ...results[i], status: 'signing' };
        setProgress(results.slice());
        try {
          const resp = await window.VDLive.submitOrder({
            assetId, isBuy,
            priceStr: String(l.price),
            sizeStr:  String(l.size),
            tif: 'Gtc',
          });
          if (resp.status !== 'ok') {
            const msg = typeof resp.response === 'string' ? resp.response : 'Rejected';
            results[i] = { ...results[i], status: 'error', msg };
            anyError = true;
          } else {
            const st = resp.response && resp.response.data && resp.response.data.statuses && resp.response.data.statuses[0];
            if (st && st.error) { results[i] = { ...results[i], status: 'error', msg: st.error }; anyError = true; }
            else if (st && st.resting) results[i] = { ...results[i], status: 'resting', oid: st.resting.oid };
            else if (st && st.filled)  results[i] = { ...results[i], status: 'filled',  oid: st.filled.oid, fill: `${st.filled.totalSz} @ ${st.filled.avgPx}` };
            else                        results[i] = { ...results[i], status: 'ok' };
          }
        } catch (e) {
          if (e && e.code === 4001) { results[i] = { ...results[i], status: 'cancelled' }; anyError = true; break; }
          results[i] = { ...results[i], status: 'error', msg: e.message || String(e) };
          anyError = true;
        }
        setProgress(results.slice());
      }
      setPhase(anyError ? 'error' : 'done');
    }

    const canSubmit = legs.length > 0 && legs.every(l => +l.size > 0 && +l.price > 0);

    return (
      <div>
        <button onClick={placeAll} disabled={!canSubmit || phase === 'working'} style={{
          width: '100%', padding: '14px 0',
          background: phase === 'working' ? 'var(--bg-elev-3)' : 'var(--acid)',
          color: phase === 'working' ? 'var(--text-2)' : 'var(--bg-base)',
          fontWeight: 700, fontSize: 13, letterSpacing: '0.08em', textTransform: 'uppercase',
          opacity: !canSubmit ? 0.4 : 1, cursor: !canSubmit ? 'not-allowed' : 'pointer',
        }}>
          {phase === 'working' ? 'Submitting…' :
           !wallet || !wallet.address ? 'Connect wallet to submit' :
           `Place all ${legs.length} legs · cost ${fmtUsd0(totalCost)}`}
        </button>

        {progress.length > 0 && (
          <div style={{ marginTop: 12, border: '1px solid var(--line-subtle)' }}>
            {progress.map((r, i) => {
              const colorMap = { pending: 'var(--text-3)', signing: 'var(--amber)', resting: 'var(--mint)',
                                 filled: 'var(--mint)', ok: 'var(--mint)', error: 'var(--coral)', cancelled: 'var(--text-3)' };
              const iconMap = { pending: '·', signing: '⟳', resting: '✓', filled: '✓', ok: '✓', error: '✗', cancelled: '–' };
              return (
                <div key={i} style={{
                  display: 'grid', gridTemplateColumns: '24px 1fr 1fr',
                  padding: '8px 12px', fontSize: 11,
                  borderBottom: i === progress.length - 1 ? 'none' : '1px solid var(--line-subtle)',
                  color: colorMap[r.status],
                }}>
                  <span style={{ fontWeight: 700 }}>{iconMap[r.status] || '·'}</span>
                  <span>
                    {r.leg.kind === 'perp' ? `Perp ${r.leg.side.toUpperCase()} ${r.leg.size} BTC @ ${fmtUsd0(+r.leg.price)}` :
                     `${r.leg.side} ${r.leg.size} @ ${fmtCents(+r.leg.price)}`}
                  </span>
                  <span style={{ textAlign: 'right' }}>
                    {r.fill ? `Filled ${r.fill}` :
                     r.oid ? `oid ${r.oid}` :
                     r.msg ? r.msg :
                     r.status}
                  </span>
                </div>
              );
            })}
          </div>
        )}
      </div>
    );
  }

  // ───────────────────────── Expiry tabs (RH date strip) ─────────────────────────
  function ExpiryTabs({ expiries, activeIdx, onPick }) {
    return (
      <div>
        <div className="cap-sm" style={{ marginBottom: 8 }}>↳ Expiry</div>
        <div style={{ display: 'flex', border: '1px solid var(--line-subtle)' }}>
          {expiries.map((e, i) => {
            const active = i === activeIdx;
            return (
              <button key={i} onClick={() => onPick(i)} style={{
                padding: '10px 16px',
                background: active ? 'var(--acid-bg10)' : 'transparent',
                color: active ? 'var(--acid)' : 'var(--text-2)',
                borderRight: '1px solid var(--line-subtle)',
                display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 3,
                minWidth: 140, transition: 'background var(--d-fast) var(--ease)',
              }}>
                <span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 11.5, fontWeight: 700, letterSpacing: '0.04em' }}>{e.label}</span>
                <span className="cap-sm" style={{ color: active ? 'var(--acid-dim)' : 'var(--text-3)', fontSize: 9 }}>{e.subLabel}</span>
              </button>
            );
          })}
          <button disabled style={{
            padding: '10px 16px', cursor: 'not-allowed',
            background: 'transparent', color: 'var(--text-4)',
            borderLeft: '1px dashed var(--line-subtle)',
            display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 3,
            minWidth: 140,
          }}>
            <span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 11.5, letterSpacing: '0.04em' }}>+ more</span>
            <span className="cap-sm" style={{ fontSize: 9 }}>weekly · monthly soon</span>
          </button>
        </div>
      </div>
    );
  }

  // ───────────────────────── Outcome chain (RH options-chain analog) ─────────────────────────
  // One row per (expiry × target). Click a price cell to add that leg at that price/side.
  function OutcomeChain({ rows, onAddLeg, sym }) {
    return (
      <div>
        <div className="cap-sm" style={{ marginBottom: 8 }}>↳ Outcome chain · click a price to add the leg</div>
        <div style={{ border: '1px solid var(--line-subtle)' }}>
          <div className="cap" style={{
            display: 'grid', gridTemplateColumns: '60px 110px 2fr 2fr',
            padding: '8px 12px', borderBottom: '1px solid var(--line-subtle)',
            color: 'var(--text-3)', gap: 12,
          }}>
            <div>Δ Spot</div>
            <div>Target</div>
            <div style={{ textAlign: 'center', color: 'var(--coral)' }}>NO · bid / ask</div>
            <div style={{ textAlign: 'center', color: 'var(--mint)' }}>YES · bid / ask</div>
          </div>
          {rows.map((r, i) => (
            <div key={i} style={{
              display: 'grid', gridTemplateColumns: '60px 110px 2fr 2fr',
              padding: '12px', alignItems: 'center', gap: 12,
              borderBottom: i === rows.length - 1 ? 'none' : '1px solid var(--line-subtle)',
            }}>
              <div className="num" style={{
                fontSize: 11, fontWeight: 600,
                color: r.distance >= 0 ? 'var(--mint)' : 'var(--coral)',
              }}>
                {r.distance == null ? '—' : (r.distance >= 0 ? '+' : '') + r.distance.toFixed(2) + '%'}
              </div>
              <div className="num" style={{ fontSize: 15, fontWeight: 600 }}>${r.target.toLocaleString()}</div>
              {/* NO bid/ask */}
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }}>
                <ChainCell value={r.noBid} side="NO" type="bid" onClick={() => r.noBid != null && onAddLeg('NO', r.noBid)} />
                <ChainCell value={r.noAsk} side="NO" type="ask" onClick={() => r.noAsk != null && onAddLeg('NO', r.noAsk)} />
              </div>
              {/* YES bid/ask */}
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }}>
                <ChainCell value={r.yesBid} side="YES" type="bid" onClick={() => r.yesBid != null && onAddLeg('YES', r.yesBid)} />
                <ChainCell value={r.yesAsk} side="YES" type="ask" onClick={() => r.yesAsk != null && onAddLeg('YES', r.yesAsk)} />
              </div>
            </div>
          ))}
        </div>
      </div>
    );
  }

  function ChainCell({ value, side, type, onClick }) {
    const tone = side === 'YES' ? 'mint' : 'coral';
    const isAsk = type === 'ask';
    const bg = isAsk
      ? (tone === 'mint' ? 'var(--mint-bg20)' : 'var(--coral-bg20)')
      : 'transparent';
    const border = isAsk
      ? (tone === 'mint' ? 'var(--mint)' : 'var(--coral)')
      : 'var(--line-subtle)';
    const color = tone === 'mint' ? 'var(--mint)' : 'var(--coral)';
    return (
      <button onClick={onClick} disabled={value == null}
        title={value == null ? 'No quote' : `Click to add ${side} leg @ ${(value*100).toFixed(1)}¢`}
        className="num" style={{
        padding: '10px 0', textAlign: 'center', fontSize: 13, fontWeight: isAsk ? 700 : 500,
        background: bg, color, border: `1px solid ${border}`,
        cursor: value == null ? 'not-allowed' : 'pointer',
        opacity: value == null ? 0.4 : 1,
        transition: 'background var(--d-fast) var(--ease)',
      }}
      onMouseEnter={(e) => { if (value != null) e.currentTarget.style.background = (tone === 'mint' ? 'var(--mint-bg20)' : 'var(--coral-bg20)'); }}
      onMouseLeave={(e) => { if (value != null) e.currentTarget.style.background = bg; }}>
        {value == null ? '—' : (value * 100).toFixed(1) + '¢'}
      </button>
    );
  }

  // ───────────────────────── Underlying perp row ─────────────────────────
  // Mirrors the chain grid so the SHORT/LONG buttons line up under NO/YES columns —
  // visually reinforces "shorting ≈ betting NO direction" / "longing ≈ betting YES direction".
  function PerpRow({ sym, mid, onAdd }) {
    return (
      <div>
        <div className="cap-sm" style={{ marginBottom: 8 }}>↳ Underlying perp · {sym}</div>
        <div style={{ border: '1px solid var(--line-subtle)' }}>
          <div style={{
            display: 'grid', gridTemplateColumns: '60px 110px 2fr 2fr',
            padding: '12px', alignItems: 'center', gap: 12,
          }}>
            <div className="cap-sm" style={{ color: 'var(--text-3)' }}>SPOT</div>
            <div className="num" style={{ fontSize: 15, fontWeight: 600 }}>
              <FlashNum value={mid}>{fmtUsd0(mid)}</FlashNum>
            </div>
            <button onClick={() => onAdd('short', mid)} disabled={!mid} className="num" style={{
              padding: '12px 0', textAlign: 'center', fontSize: 13, fontWeight: 700,
              background: 'var(--coral-bg20)', color: 'var(--coral)',
              border: '1px solid var(--coral)',
              transition: 'background var(--d-fast) var(--ease)',
            }}
            onMouseEnter={(e) => { if (mid) e.currentTarget.style.background = 'var(--coral-bg20)'; }}>
              + SHORT 0.01 BTC
            </button>
            <button onClick={() => onAdd('long', mid)} disabled={!mid} className="num" style={{
              padding: '12px 0', textAlign: 'center', fontSize: 13, fontWeight: 700,
              background: 'var(--mint-bg20)', color: 'var(--mint)',
              border: '1px solid var(--mint)',
              transition: 'background var(--d-fast) var(--ease)',
            }}>
              + LONG 0.01 BTC
            </button>
          </div>
        </div>
      </div>
    );
  }

  // ───────────────────────── Asset picker ─────────────────────────
  function AssetPicker({ outcomes, selectedSym, onPick }) {
    const outcomeBySym = {};
    for (const o of outcomes) {
      const u = o.parsed && o.parsed.underlying;
      if (u) outcomeBySym[u] = o;
    }
    return (
      <div>
        <div className="cap-sm" style={{ marginBottom: 10 }}>↳ Pick an underlying to build a strategy on</div>
        <div style={{ display: 'grid', gridTemplateColumns: `repeat(auto-fit, minmax(120px, 1fr))`, gap: 8 }}>
          {ASSETS.map(a => {
            const live = !!outcomeBySym[a.sym];
            const active = a.sym === selectedSym;
            return (
              <button key={a.sym} onClick={() => onPick(a.sym)} style={{
                textAlign: 'left', padding: 14,
                background: active ? 'var(--acid-bg10)' : 'var(--bg-elev)',
                border: '1px solid ' + (active ? 'var(--acid)' : 'var(--line-subtle)'),
                transition: 'border-color var(--d-fast) var(--ease), background var(--d-fast) var(--ease)',
              }}
              onMouseEnter={(e) => { if (!active) e.currentTarget.style.borderColor = 'var(--text-3)'; }}
              onMouseLeave={(e) => { if (!active) e.currentTarget.style.borderColor = 'var(--line-subtle)'; }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                    {window.VDIcons && window.VDIcons.AssetGlyph && <window.VDIcons.AssetGlyph sym={a.sym} size={28} />}
                    <span className="num" style={{ fontSize: 16, fontWeight: 700, color: active ? 'var(--acid)' : 'var(--text)' }}>{a.sym}</span>
                  </div>
                  {live && (
                    <span className="cap-sm" style={{ fontSize: 9, color: 'var(--mint)', letterSpacing: '0.08em' }}>● OUTCOME</span>
                  )}
                </div>
                <div className="cap-sm" style={{ fontSize: 10, color: 'var(--text-3)', marginTop: 8 }}>{a.name}</div>
                <div className="cap-sm" style={{ fontSize: 9, color: live ? 'var(--text-2)' : 'var(--text-4)', marginTop: 6 }}>
                  {live ? 'Perp + HIP-4 binary' : 'Perp only · outcome soon'}
                </div>
              </button>
            );
          })}
        </div>
      </div>
    );
  }

  // ───────────────────────── Builder shell ─────────────────────────
  function LiveStrategyBuilder({ initialSym = null }) {
    const [snap, setSnap] = useState(window.VDLive ? window.VDLive.state : null);
    useEffect(() => {
      if (!window.VDLive) return;
      return window.VDLive.subscribe((s) => setSnap({
        ...s,
        outcomes: s.outcomes.map(o => ({ ...o, books: { ...o.books }, fullBook: { ...o.fullBook } })),
        wallet: { ...s.wallet },
        btc: { ...s.btc },
      }));
    }, []);

    const [selectedSym, setSelectedSym] = useState(initialSym);
    // Re-preselect if the parent passes a new initialSym (e.g. clicking through different watchlist rows)
    useEffect(() => { if (initialSym) setSelectedSym(initialSym); }, [initialSym]);
    const [legs, setLegs] = useState([]);
    const [activePreset, setActivePreset] = useState(null);
    const [notional, setNotional] = useState(100); // total $ premium to spend on the strategy

    // Mobile detection — used to switch grid layouts
    const [isMobile, setIsMobile] = useState(typeof window !== 'undefined' ? window.innerWidth < 720 : false);
    useEffect(() => {
      const onResize = () => setIsMobile(window.innerWidth < 720);
      window.addEventListener('resize', onResize);
      return () => window.removeEventListener('resize', onResize);
    }, []);

    if (!snap) return null;

    const outcomes = snap.outcomes || [];
    const standalone = (snap.standalone && snap.standalone.length) ? snap.standalone : outcomes;
    const questions = snap.questions || [];
    const assetMeta = ASSETS.find(a => a.sym === selectedSym) || null;

    // The "primary" binary on this asset (any standalone priceBinary outcome)
    const binary = standalone.find(o => o.parsed && o.parsed.class === 'priceBinary' && o.parsed.underlying === selectedSym) || null;
    // The first multi-bucket question on this asset
    const question = questions.find(q => q.parsed && q.parsed.class === 'priceBucket' && q.parsed.underlying === selectedSym) || null;
    // Used for the "is anything live for this asset?" gate — covers either market type
    const outcome = binary || (question && question.namedOutcomes[0]) || null;

    // Live perp mid — currently we only stream BTC perp; for other syms this will be null
    // and the builder shows a coming-soon state below.
    const perpMid = (selectedSym === 'BTC' && snap.btc && snap.btc.mid) || null;
    const target  = binary && binary.parsed && binary.parsed.targetPriceNum;
    const yesAsk  = binary && binary.books && binary.books.yes && binary.books.yes.ask;
    const noAsk   = binary && binary.books && binary.books.no  && binary.books.no.ask;
    const currentMid = binary && binary.mid;

    // Availability map driving which presets show / are enabled
    const available = {
      binary: !!binary,
      bucket: !!question,
      perp: !!perpMid,
      'multi-expiry': false,
    };
    function presetIsAvailable(p) {
      const needs = (p.needs || '').split('+').filter(Boolean);
      return needs.every(n => available[n]);
    }

    // Build the preset's legs scaled to a target USD notional (total premium spent).
    // Perp legs scale at the same factor so structural ratios (e.g. delta hedge) are preserved.
    function buildPresetLegs(p, notionalDollars) {
      const ctx = { binary, question, perpMid, target, yesAsk, noAsk, currentMid, currentSpot: perpMid };
      const baseLegs = p.build(ctx);
      const basePremium = baseLegs
        .filter(l => l.kind === 'outcome')
        .reduce((s, l) => s + (+l.size) * (+l.price), 0);
      if (basePremium <= 0) return baseLegs;
      const factor = notionalDollars / basePremium;
      return baseLegs.map(l => ({
        ...l,
        size: l.kind === 'perp' ? +(l.size * factor).toFixed(4) : Math.max(1, Math.round(l.size * factor)),
      }));
    }
    function applyPreset(p) {
      if (!presetIsAvailable(p)) return;
      setActivePreset(p.id);
      setLegs(buildPresetLegs(p, notional));
    }
    // Re-scale legs whenever the notional changes (only if a preset is active)
    useEffect(() => {
      if (!activePreset || !outcome || !perpMid) return;
      const p = PRESETS.find(x => x.id === activePreset);
      if (p) setLegs(buildPresetLegs(p, notional));
    }, [notional]);
    function addLeg(kind) {
      const newLeg = kind === 'perp'
        ? { id: uid(), kind: 'perp',    side: 'long', size: 0.01, price: perpMid || 0 }
        : { id: uid(), kind: 'outcome', side: 'YES',  size: 50,   price: yesAsk || currentMid || 0.5 };
      setLegs(legs.concat([newLeg]));
      setActivePreset(null);
    }
    function updateLeg(idx, l) { const out = legs.slice(); out[idx] = l; setLegs(out); setActivePreset(null); }
    function removeLeg(idx) { setLegs(legs.filter((_, i) => i !== idx)); setActivePreset(null); }
    function clearAll() { setLegs([]); setActivePreset(null); }

    function pickAsset(sym) {
      setSelectedSym(sym);
      // Reset legs when switching underlying — old legs would price wrong against new spot/target
      setLegs([]); setActivePreset(null);
    }

    const totalCost = legs.reduce((s, l) => s + legCost(l), 0);
    const ctxNow = { mode: 'now', target, currentSpot: perpMid, currentMid };
    const ctxSettle = { mode: 'settle', target, currentSpot: perpMid, currentMid };
    const settlePnlNow = perpMid != null && target != null ? totalPnl(legs, perpMid, ctxSettle) : 0;
    const nowPnlNow    = perpMid != null && target != null ? totalPnl(legs, perpMid, ctxNow)    : 0;

    const metrics = useMemo(() => {
      if (!perpMid || !target || legs.length === 0) return null;
      const span = Math.max(perpMid * 0.07, Math.abs(target - perpMid) * 1.6);
      const xMin = perpMid - span, xMax = perpMid + span;
      let maxG = -Infinity, maxL = Infinity, maxGAt = null, maxLAt = null;
      const xs = [], ys = [];
      const ctx = { mode: 'settle', target, currentSpot: perpMid, currentMid };
      for (let i = 0; i < 240; i++) {
        const x = xMin + (i / 239) * (xMax - xMin);
        const y = totalPnl(legs, x, ctx);
        xs.push(x); ys.push(y);
        if (y > maxG) { maxG = y; maxGAt = x; }
        if (y < maxL) { maxL = y; maxLAt = x; }
      }
      const bes = [];
      for (let i = 1; i < ys.length; i++) {
        if ((ys[i - 1] < 0 && ys[i] >= 0) || (ys[i - 1] > 0 && ys[i] <= 0)) {
          const t = ys[i - 1] / (ys[i - 1] - ys[i]);
          bes.push(xs[i - 1] + t * (xs[i] - xs[i - 1]));
        }
      }
      return { maxG, maxL, maxGAt, maxLAt, breakevens: bes };
    }, [legs, perpMid, target, currentMid]);

    return (
      <div className="card" style={{
        padding: 24, border: '1px solid var(--line)',
        background: 'linear-gradient(180deg, var(--bg-elev-2), var(--bg-elev))',
      }}>
        <div style={{ marginBottom: 28 }}>
          <div className="cap-sm" style={{ marginBottom: 8, color: 'var(--text-3)' }}>↳ Strategy builder · Perp + HIP-4 outcomes</div>
          <h1 className="serif" style={{ fontSize: 56, fontWeight: 400, letterSpacing: '-0.035em', lineHeight: 1, margin: 0 }}>
            What do you <em style={{ color: 'var(--acid)' }}>think</em> happens?
          </h1>
        </div>

        {/* Step 1: pick an asset */}
        <div style={{ marginBottom: selectedSym ? 18 : 0 }}>
          <AssetPicker outcomes={outcomes} selectedSym={selectedSym} onPick={pickAsset} />
        </div>

        {!selectedSym && (
          <div style={{ marginTop: 18, padding: 16, border: '1px dashed var(--line-subtle)', textAlign: 'center', color: 'var(--text-3)', fontSize: 12 }}>
            Pick an underlying above to start building.
          </div>
        )}

        {selectedSym && !outcome && (
          <div style={{ marginTop: 12, padding: 18, border: '1px solid var(--line-subtle)', background: 'var(--bg-elev)' }}>
            <div className="cap-sm" style={{ marginBottom: 6, color: 'var(--amber)' }}>↳ Outcome market not live yet</div>
            <div style={{ fontSize: 13, color: 'var(--text-2)', marginBottom: 4 }}>
              Hyperliquid hasn't launched a HIP-4 binary on <strong>{selectedSym}</strong> yet — only the BTC daily binary is live as of {new Date().toISOString().slice(0,10)}.
            </div>
            <div style={{ fontSize: 12, color: 'var(--text-3)' }}>
              The builder needs at least an outcome market for the multi-leg flows. Pick BTC for now, or check back when more markets ship.
            </div>
          </div>
        )}

        {selectedSym && outcome && (
          <>
            {/* Spot + expiry strip — minimal context */}
            <div style={{ display: 'flex', alignItems: isMobile ? 'flex-start' : 'baseline', gap: isMobile ? 14 : 28, padding: '14px 0', borderTop: '1px solid var(--line-subtle)', borderBottom: '1px solid var(--line-subtle)', marginBottom: 24, flexWrap: 'wrap' }}>
              <div>
                <div className="cap-sm">{selectedSym} spot</div>
                <div className="num" style={{ fontSize: 22, fontWeight: 600 }}>
                  <FlashNum value={perpMid}>{fmtUsd0(perpMid)}</FlashNum>
                </div>
              </div>
              <div>
                <div className="cap-sm">Question</div>
                <div className="num" style={{ fontSize: 14, fontWeight: 600 }}>
                  {selectedSym} &gt; {fmtUsd0(target)}
                </div>
              </div>
              <div>
                <div className="cap-sm">Settles</div>
                <div className="num" style={{ fontSize: 14, fontWeight: 600 }}>
                  {outcome.parsed && outcome.parsed.expiryMs
                    ? new Date(outcome.parsed.expiryMs).toLocaleString(undefined, { weekday: 'short', hour: '2-digit', minute: '2-digit', hour12: false }) + ' UTC'
                    : '—'}
                </div>
              </div>
              <div style={{ marginLeft: 'auto', textAlign: 'right' }}>
                <div className="cap-sm">YES <span style={{ color: 'var(--text-4)' }}>·</span> NO</div>
                <div className="num" style={{ fontSize: 14, fontWeight: 600 }}>
                  <span style={{ color: 'var(--mint)' }}><FlashNum value={currentMid}>{fmtCents(currentMid)}</FlashNum></span>
                  <span style={{ color: 'var(--text-4)' }}> · </span>
                  <span style={{ color: 'var(--coral)' }}><FlashNum value={currentMid}>{fmtCents(currentMid != null ? 1 - currentMid : null)}</FlashNum></span>
                </div>
              </div>
            </div>

            {/* Step 2: pick a strategy — big cards, the only build entry point */}
            <div style={{ marginBottom: 24 }}>
              <div className="cap-sm" style={{ marginBottom: 10 }}>② Pick a strategy</div>
              {/* Group presets by category — easier to scan than a flat row */}
              {[
                { id: 'directional', label: 'Single bets · directional' },
                { id: 'hedged',      label: 'Hedged · neutralize price moves' },
                { id: 'volatility',  label: 'Volatility · path-shape bets' },
                { id: 'arb',         label: 'Arbitrage · risk-free if pricing allows' },
                { id: 'multi-expiry', label: 'Multi-expiry · time-spread (coming soon)' },
              ].map(group => {
                const groupPresets = PRESETS.filter(p => p.group === group.id);
                if (!groupPresets.length) return null;
                return (
                  <div key={group.id} style={{ marginBottom: 14 }}>
                    <div className="cap-sm" style={{ marginBottom: 8, color: 'var(--text-3)' }}>{group.label}</div>
                    <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(auto-fit, minmax(220px, 1fr))', gap: 10 }}>
                      {groupPresets.map(p => {
                        const active = p.id === activePreset;
                        const enabled = presetIsAvailable(p);
                        const reason = !enabled ? (
                          (p.needs || '').split('+').filter(n => !available[n]).map(n => ({
                            'binary': 'binary market not live',
                            'bucket': 'no multi-bucket question on this asset',
                            'perp':   'perp price not streaming',
                            'multi-expiry': 'HL only has daily binaries — multi-expiry not yet shipped',
                          })[n] || n).join(' · ')
                        ) : null;
                        return (
                          <button key={p.id} onClick={() => enabled && applyPreset(p)}
                            disabled={!enabled}
                            title={reason || ''}
                            style={{
                              textAlign: 'left', padding: 14,
                              background: active ? 'var(--acid-bg10)' : 'var(--bg-elev)',
                              border: '1px solid ' + (active ? 'var(--acid)' : 'var(--line-subtle)'),
                              transition: 'border-color var(--d-fast) var(--ease), background var(--d-fast) var(--ease)',
                              minHeight: isMobile ? 'auto' : 120,
                              opacity: enabled ? 1 : 0.5,
                              cursor: enabled ? 'pointer' : 'not-allowed',
                            }}
                            onMouseEnter={(e) => { if (enabled && !active) e.currentTarget.style.borderColor = 'var(--text-3)'; }}
                            onMouseLeave={(e) => { if (enabled && !active) e.currentTarget.style.borderColor = 'var(--line-subtle)'; }}>
                            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 6, marginBottom: 8 }}>
                              <div style={{ fontSize: 13, fontWeight: 700, color: active ? 'var(--acid)' : 'var(--text)', lineHeight: 1.2 }}>{p.name}</div>
                              <div className="num" style={{ fontSize: 9, color: 'var(--text-3)', letterSpacing: '0.04em', whiteSpace: 'nowrap' }}>{p.tag}</div>
                            </div>
                            <div style={{ fontSize: 11.5, color: 'var(--text-2)', lineHeight: 1.5 }}>{p.simple}</div>
                            {!enabled && (
                              <div className="cap-sm" style={{ marginTop: 8, color: 'var(--coral)', fontSize: 9.5 }}>
                                ⚠ {reason}
                              </div>
                            )}
                          </button>
                        );
                      })}
                    </div>
                  </div>
                );
              })}
            </div>

            {/* Step 3 onwards: only appears once a strategy is chosen */}
            {activePreset && legs.length > 0 && (
              <>
                {/* Quant explanation panel — appears under selected preset */}
                {(() => {
                  const p = PRESETS.find(x => x.id === activePreset);
                  if (!p) return null;
                  return (
                    <div style={{
                      marginBottom: 24, padding: 16,
                      background: 'var(--bg-elev)',
                      border: '1px solid var(--line-subtle)',
                      borderLeft: '2px solid var(--acid)',
                    }}>
                      <div className="cap-sm" style={{ marginBottom: 10, color: 'var(--acid)' }}>↳ How {p.name} works</div>
                      <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr 1fr', gap: 16 }}>
                        <div>
                          <div className="cap-sm" style={{ marginBottom: 6 }}>Mechanics</div>
                          <div style={{ fontSize: 12, color: 'var(--text-2)', lineHeight: 1.55 }}>{p.howItWorks}</div>
                        </div>
                        <div>
                          <div className="cap-sm" style={{ marginBottom: 6, color: 'var(--mint)' }}>Best when</div>
                          <div style={{ fontSize: 12, color: 'var(--text-2)', lineHeight: 1.55 }}>{p.bestWhen}</div>
                        </div>
                        <div>
                          <div className="cap-sm" style={{ marginBottom: 6, color: 'var(--coral)' }}>Risk</div>
                          <div style={{ fontSize: 12, color: 'var(--text-2)', lineHeight: 1.55 }}>{p.risk}</div>
                        </div>
                      </div>
                    </div>
                  );
                })()}

                {/* Step 3: notional in dollars */}
                <div style={{ marginBottom: 24 }}>
                  <div className="cap-sm" style={{ marginBottom: 10 }}>③ How much do you want to spend on this strategy?</div>
                  <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
                    {[25, 100, 500, 1000, 5000].map(amt => {
                      const active = notional === amt;
                      return (
                        <button key={amt} onClick={() => setNotional(amt)} style={{
                          padding: '10px 16px',
                          background: active ? 'var(--acid-bg10)' : 'var(--bg-elev)',
                          border: '1px solid ' + (active ? 'var(--acid)' : 'var(--line-subtle)'),
                          color: active ? 'var(--acid)' : 'var(--text-2)',
                          minWidth: 84,
                        }}>
                          <span className="num" style={{ fontSize: 14, fontWeight: 600 }}>${amt.toLocaleString()}</span>
                        </button>
                      );
                    })}
                    <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginLeft: isMobile ? 0 : 8 }}>
                      <span className="cap-sm">Custom</span>
                      <span className="num" style={{ color: 'var(--text-3)' }}>$</span>
                      <input type="number" min="1" step="1" value={notional}
                             onChange={(e) => setNotional(Math.max(1, +e.target.value || 1))}
                             className="num"
                             style={{ width: 110, padding: '8px 10px', fontSize: 13,
                               background: 'var(--bg-elev-2)', border: '1px solid var(--line-subtle)',
                               color: 'var(--text)' }} />
                    </div>
                  </div>
                  <div className="cap-sm" style={{ marginTop: 8, color: 'var(--text-3)', fontSize: 10.5 }}>
                    This is the premium you'll pay across all outcome legs. Perp legs use margin (not premium) and scale proportionally.
                  </div>
                </div>

                {/* Read-only position summary */}
                <div style={{ border: '1px solid var(--line-subtle)', marginBottom: 18 }}>
                  <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--line-subtle)' }}>
                    <span className="cap-sm">↳ Your position · {legs.length} leg{legs.length === 1 ? '' : 's'}</span>
                  </div>
                  {legs.map((l, i) => {
                    const isPerp = l.kind === 'perp';
                    const accent = isPerp
                      ? (l.side === 'long' ? 'var(--mint)' : 'var(--coral)')
                      : (l.side === 'YES' ? 'var(--mint)' : 'var(--coral)');
                    const labelSuffix = (l.label && l.label !== 'YES' && l.label !== 'NO') ? ' · ' + l.label : '';
                    const desc = isPerp
                      ? `${l.side === 'long' ? 'Long' : 'Short'} ${(+l.size).toFixed(4)} ${selectedSym} perp`
                      : `Buy ${+l.size} ${l.side}${labelSuffix}`;
                    return (
                      <div key={l.id} style={{
                        display: 'grid',
                        gridTemplateColumns: isMobile ? '24px 1fr' : '32px 1fr 1fr 1fr',
                        padding: '12px 14px', alignItems: 'center', gap: isMobile ? 8 : 12,
                        borderBottom: i === legs.length - 1 ? 'none' : '1px solid var(--line-subtle)',
                      }}>
                        <span className="cap-sm" style={{ color: 'var(--text-3)' }}>{String(i + 1).padStart(2, '0')}</span>
                        <div>
                          <div style={{ fontSize: 13, fontWeight: 600, color: accent }}>{desc}</div>
                          {isMobile && (
                            <div className="num" style={{ fontSize: 11, color: 'var(--text-3)', marginTop: 4 }}>
                              @ {isPerp ? fmtUsd0(+l.price) : fmtCents(+l.price)}
                              <span style={{ color: 'var(--text-4)', margin: '0 6px' }}>·</span>
                              {isPerp ? 'margin' : 'cost ' + fmtUsd0(legCost(l))}
                            </div>
                          )}
                        </div>
                        {!isMobile && (
                          <>
                            <span className="num" style={{ fontSize: 12, color: 'var(--text-2)', textAlign: 'right' }}>
                              @ {isPerp ? fmtUsd0(+l.price) : fmtCents(+l.price)}
                            </span>
                            <span className="num" style={{ fontSize: 12, color: 'var(--text-2)', textAlign: 'right' }}>
                              {isPerp ? <span style={{ color: 'var(--text-3)' }}>margin</span> : 'cost ' + fmtUsd0(legCost(l))}
                            </span>
                          </>
                        )}
                      </div>
                    );
                  })}
                </div>

                {/* Payoff + metrics */}
                <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1.6fr 1fr', gap: 18, marginBottom: 18 }}>
                  <div style={{ border: '1px solid var(--line-subtle)', padding: 14 }}>
                    <div className="cap-sm" style={{ marginBottom: 6 }}>↳ Payoff vs hypothetical {selectedSym} price</div>
                    <PayoffChart legs={legs} target={target} currentSpot={perpMid}
                      currentMid={currentMid} sym={selectedSym} height={isMobile ? 200 : 240} />
                  </div>
                  <div style={{ border: '1px solid var(--line-subtle)', padding: 16 }}>
                    <div className="cap-sm" style={{ marginBottom: 12 }}>↳ At a glance</div>
                    <div style={{ display: 'grid', gap: 12 }}>
                      <Metric label="Cost to enter" value={fmtUsd0(totalCost)} />
                      {metrics && (
                        <>
                          <Metric label="Best case at settle" value={'+' + fmtUsd0(Math.max(0, metrics.maxG))}
                            sub={`if ${selectedSym} ends @ ${fmtUsd0(metrics.maxGAt)}`} tone="mint" />
                          <Metric label="Worst case at settle" value={fmtUsd0(metrics.maxL)}
                            sub={`if ${selectedSym} ends @ ${fmtUsd0(metrics.maxLAt)}`} tone="coral" />
                          <Metric label="Breakeven" value={
                            metrics.breakevens.length === 0 ? '—' : metrics.breakevens.map(b => fmtUsd0(b)).join(' or ')
                          } />
                        </>
                      )}
                    </div>
                  </div>
                </div>
              </>
            )}

            {!activePreset && (
              <div style={{ padding: 18, border: '1px dashed var(--line-subtle)', textAlign: 'center', color: 'var(--text-3)', fontSize: 12, marginBottom: 18 }}>
                Pick a strategy above to see your position, payoff, and the order to place.
              </div>
            )}

            <SubmitAll legs={legs} outcome={outcome} btcMid={perpMid}
              wallet={snap.wallet} onConnect={() => window.VDLive.connectWallet()}
              perpAssetId={assetMeta ? assetMeta.perpAssetId : 0} />
          </>
        )}
      </div>
    );
  }

  function Metric({ label, value, sub, tone }) {
    const color = tone === 'mint' ? 'var(--mint)' : tone === 'coral' ? 'var(--coral)' : 'var(--text)';
    return (
      <div>
        <div className="cap-sm" style={{ marginBottom: 4 }}>{label}</div>
        <div className="num" style={{ fontSize: 16, fontWeight: 600, color }}>{value}</div>
        {sub && <div className="cap-sm" style={{ fontSize: 9, color: 'var(--text-3)', marginTop: 2 }}>{sub}</div>}
      </div>
    );
  }

  window.VDLiveStrategy = { LiveStrategyBuilder };
})();
