;(function () {
/* TradePage — the trading surface for the Leveraged Trade sub-app.
   Top of page: a featured-instrument hero (price, TVChart area chart, range
   tabs, Sell/Buy CTAs). Below: a category filter row with a sort cycle pill,
   then a full-width instrument list. Tapping a list row promotes the
   instrument to the hero. Tapping Sell/Buy on the hero opens an order-ticket
   sheet that overlays the page. The page renders the scrollable body only —
   shell handles the top bar and bottom nav.

   Data flow: all symbol prices, sparks and bid/ask come from the TradeStore
   via window.useAllSymbols / window.useSymbol — ticking live as the engine
   pushes updates. Order placement dispatches through window.useTradeMutators
   so the open positions, portfolio totals and home dashboard all stay in
   sync with what happens on this page. */

/* Generate N deterministic random OHLC candles for a symbol. Used to
   give a recognisable chart shape to instruments that don't have a
   live tick feed (everything outside the dummy-feed eight). The seed
   is a small string-hash of the symbol id so each instrument has its
   own consistent shape across renders, but two non-tick symbols don't
   share the same chart. The walk converges to the symbol's current
   bid so the rendered candles read as a plausible recent history. */
function hashString(s) {
  let h = 2166136261;
  for (let i = 0; i < s.length; i++) {
    h ^= s.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  return h >>> 0;
}
function makeStaticCandles(sym, count) {
  const seed0 = hashString(sym.sym || 'x');
  let seed = seed0;
  /* xorshift32 for a cheap deterministic 0..1 stream. */
  const rand = () => {
    seed ^= seed << 13;
    seed ^= seed >>> 17;
    seed ^= seed << 5;
    seed >>>= 0;
    return seed / 4294967296;
  };
  const target = sym.bid;
  /* Per-candle volatility scaled to the instrument's typical price
     scale — 0.4% of price for forex/indices/stocks/crypto, slightly
     wider for low-priced silver/oil so wicks are visible. */
  const scale = sym.cat === 'commodities' && (sym.sym === 'XAGUSD' || sym.sym === 'WTI') ? 0.006 : 0.004;
  /* Start the walk slightly off-target so it can wander toward `bid`. */
  let close = target * (1 + (rand() - 0.5) * scale * 4);
  const candles = [];
  const now = Math.floor(Date.now() / 1000);
  for (let i = 0; i < count; i++) {
    const open = close;
    /* Drift gently toward the target so the last candle lands near
       the displayed bid; magnitude grows with i so the convergence
       feels organic rather than monotonic. */
    const driftToTarget = (target - open) * (i / count) * 0.4;
    const noise = (rand() - 0.5) * open * scale;
    close = open + driftToTarget + noise;
    const wickHigh = open * scale * rand() * 0.6;
    const wickLow = open * scale * rand() * 0.6;
    const high = Math.max(open, close) + wickHigh;
    const low = Math.min(open, close) - wickLow;
    candles.push({
      time: now - (count - 1 - i) * 60,
      open, high, low, close,
    });
  }
  /* Snap the last candle's close to the displayed bid so the chart
     terminates exactly where the price label says it does. */
  candles[candles.length - 1].close = target;
  candles[candles.length - 1].high = Math.max(candles[candles.length - 1].high, target);
  candles[candles.length - 1].low = Math.min(candles[candles.length - 1].low, target);
  return candles;
}

/* ── Star icon (watchlist toggle) ──────────────────────────────────────────
   Outline when off, filled gold when on. Stroke 1.6 to match other glyphs. */
const StarIcon = ({ on }) => (
  <svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path
      d="M12 2.5l2.92 5.92 6.53.95-4.72 4.6 1.11 6.5L12 17.4l-5.84 3.07 1.11-6.5-4.72-4.6 6.53-.95L12 2.5z"
      stroke={on ? colors.gold : colors.muted2}
      fill={on ? colors.gold : 'none'}
      strokeWidth="1.6"
      strokeLinecap="round"
      strokeLinejoin="round"
    />
  </svg>
);

/* Plain X glyph for the order-ticket close button. */
const CloseIcon = () => (
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M6 6l12 12M18 6L6 18" stroke={colors.muted} strokeWidth="2" strokeLinecap="round" />
  </svg>
);

/* Tiny chevron used by the sort cycle pill. */
const ChevDown = () => (
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M6 9l6 6 6-6" stroke={colors.ink2} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
  </svg>
);

/* ── Category filter pills + sort cycle ────────────────────────────────── */
const CATEGORIES = [
  ['all', 'All'],
  ['forex', 'Forex'],
  ['indices', 'Indices'],
  ['commodities', 'Commodities'],
  ['crypto', 'Crypto'],
  ['stocks', 'Stocks'],
];

const SORT_LABELS = ['Top', 'Movers', 'A–Z'];

const FilterRow = ({ category, setCategory, sortIdx, cycleSort }) => {
  /* Scroll-aware mask: only fade the LEFT edge once the user has
     scrolled past the start. When at scrollLeft=0 the "All" pill is
     pinned to the left edge and shouldn't be partially faded. The
     right edge always fades because the static "Top" sort button
     sits there and pills crossing under it would otherwise cut off
     mid-letter. */
  const scrollRef = React.useRef(null);
  const [scrolled, setScrolled] = React.useState(false);

  const onScroll = React.useCallback(() => {
    const el = scrollRef.current;
    if (!el) return;
    setScrolled(el.scrollLeft > 4);
  }, []);

  /* Build mask-image string. Left fade only if scrolled away from start. */
  const leftStop = scrolled ? '12px' : '0px';
  const rightFadeWidth = '28px';
  const mask = `linear-gradient(to right, transparent 0%, black ${leftStop}, black calc(100% - ${rightFadeWidth}), transparent 100%)`;

  return (
  <div style={{
    display: 'flex',
    flexDirection: 'row',
    alignItems: 'center',
    gap: '8px',
  }}>
    {/* Horizontal-scrolling pill row. */}
    <div
      ref={scrollRef}
      onScroll={onScroll}
      style={{
        flex: 1,
        display: 'flex',
        flexDirection: 'row',
        alignItems: 'center',
        gap: '6px',
        overflowX: 'auto',
        scrollbarWidth: 'none',
        maskImage: mask,
        WebkitMaskImage: mask,
      }}
    >
      {CATEGORIES.map(([key, label]) => {
        const active = category === key;
        return (
          <button
            key={key}
            onClick={() => setCategory(key)}
            style={{
              border: active ? 'none' : `1px solid ${colors.line}`,
              background: active ? colors.teal : '#fff',
              padding: '0 14px',
              margin: 0,
              cursor: 'pointer',
              font: 'inherit',
              color: 'inherit',
              textAlign: 'inherit',
              height: '32px',
              borderRadius: '9999px',
              flexShrink: 0,
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
            }}
          >
            <span style={{
              fontFamily: 'Gilroy',
              fontWeight: 600,
              fontSize: '13px',
              color: active ? '#fff' : colors.ink2,
              whiteSpace: 'nowrap',
            }}>{label}</span>
          </button>
        );
      })}
    </div>
    {/* Sort cycle — visual only; cycles through the three labels. */}
    <button
      onClick={cycleSort}
      style={{
        border: `1px solid ${colors.line}`,
        background: '#fff',
        padding: '0 10px',
        margin: 0,
        cursor: 'pointer',
        font: 'inherit',
        color: 'inherit',
        textAlign: 'inherit',
        height: '32px',
        borderRadius: '9999px',
        display: 'flex',
        alignItems: 'center',
        gap: '6px',
        flexShrink: 0,
      }}
    >
      <span style={{
        fontFamily: 'Gilroy',
        fontWeight: 600,
        fontSize: '12px',
        color: colors.ink2,
      }}>{SORT_LABELS[sortIdx]}</span>
      <ChevDown />
    </button>
  </div>
  );
};

/* ── Hero card (featured instrument) ──────────────────────────────────── */
/* Range tabs are visual only — the chart renders a fixed 20-candle
   window regardless of which one is selected. Compact labels per the
   user's spec: 1m / 5m / 1h / 1d. */
const RANGES = ['1m', '5m', '1h', '1d'];

const HeroCard = ({ sym, watched, toggleWatch, range, setRange, openTicket, onExpand }) => {
  const positive = sym.change >= 0;
  const changeColor = positive ? colors.green : colors.red;
  const changeSign = positive ? '+' : '−';
  const changeText = `${positive ? '↑' : '↓'} ${changeSign}${Math.abs(sym.change).toFixed(2)}%`;

  /* Translate the rolling spark into TVChart's { time, value } shape. The lib
     just needs strictly increasing time values; spacing them 5s apart matches
     the tick cadence well enough for a smooth area. Memoized on the spark
     reference so the chart only re-pushes on actual data change. */
  const chartData = React.useMemo(() => {
    const arr = sym && sym.spark ? sym.spark : [];
    const now = Math.floor(Date.now() / 1000);
    return arr.map((v, i) => ({
      time: now - (arr.length - 1 - i) * 5,
      value: v,
    }));
  }, [sym && sym.spark]);

  /* Static candles for symbols without a live tick feed. The dummy
     feed populates `sym.ohlc` for its 8 instruments; for everything
     else we synthesise 20 deterministic random candles seeded by the
     symbol id so the chart has a stable, recognisable shape across
     re-renders even though no price is moving. */
  const staticOhlc = React.useMemo(() => {
    if (!sym) return null;
    if (sym.realFeed === 'dummy') return null; // live feed wins
    return makeStaticCandles(sym, 20);
  }, [sym && sym.sym, sym && sym.bid]);

  return (
    <Card padding={{ horizontal: 18, vertical: 18 }} style={{ paddingBottom: '16px' }}>
      {/* Header row: mark + names left, star button right. */}
      <div style={{
        display: 'flex',
        flexDirection: 'row',
        alignItems: 'center',
        gap: '12px',
      }}>
        <window.SymbolMark sym={sym} size={36} />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{
            fontFamily: 'Gilroy',
            fontWeight: 600,
            fontSize: '18px',
            color: colors.ink,
            lineHeight: 1.1,
          }}>{sym.sym}</div>
          <div style={{
            fontFamily: 'Inter',
            fontWeight: 400,
            fontSize: '12px',
            color: colors.muted2,
            marginTop: '2px',
            overflow: 'hidden',
            textOverflow: 'ellipsis',
            whiteSpace: 'nowrap',
          }}>{sym.name}</div>
        </div>
        <button
          onClick={toggleWatch}
          aria-label={watched ? 'Remove from watchlist' : 'Add to watchlist'}
          style={{
            border: 'none',
            background: 'transparent',
            padding: 0,
            margin: 0,
            cursor: 'pointer',
            font: 'inherit',
            color: 'inherit',
            textAlign: 'inherit',
            width: '32px',
            height: '32px',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <StarIcon on={watched} />
        </button>
        <button
          onClick={onExpand}
          aria-label="Expand chart"
          style={{
            border: 'none', background: 'transparent',
            padding: '6px', margin: 0,
            cursor: 'pointer', display: 'flex', alignItems: 'center',
          }}
        >
          <svg width="22" height="22" viewBox="0 0 24 24" fill="none">
            <path d="M9 3H5a2 2 0 0 0-2 2v4 M15 3h4a2 2 0 0 1 2 2v4 M9 21H5a2 2 0 0 1-2-2v-4 M15 21h4a2 2 0 0 0 2-2v-4"
              stroke="#475467" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
          </svg>
        </button>
      </div>

      {/* Big bid + change line. */}
      <div style={{ marginTop: '14px' }}>
        <div style={{
          fontFamily: 'Gilroy',
          fontWeight: 700,
          fontSize: '32px',
          color: colors.ink,
          fontVariantNumeric: 'tabular-nums',
          lineHeight: 1.05,
        }}>{window.formatPrice(sym.bid, sym)}</div>
        <div style={{
          fontFamily: 'Inter',
          fontWeight: 600,
          fontSize: '14px',
          color: changeColor,
          marginTop: '4px',
        }}>{changeText}</div>
      </div>

      {/* TradingView Lightweight Chart. Negative side margin so the area
         bleeds slightly past the card padding for a fuller look. */}
      <div style={{
        marginTop: '16px',
        marginLeft: -4,
        marginRight: -4,
      }}>
        <window.TVChart
          data={chartData}
          ohlc={(sym && sym.ohlc) || staticOhlc}
          color={positive ? '#079455' : '#F04438'}
          height={120}
          kind="candlestick"
        />
      </div>

      {/* Range tabs — visual only. */}
      <div style={{
        marginTop: '12px',
        display: 'flex',
        flexDirection: 'row',
        gap: '6px',
        justifyContent: 'center',
      }}>
        {RANGES.map((r) => {
          const active = range === r;
          return (
            <button
              key={r}
              onClick={() => setRange(r)}
              style={{
                border: active ? 'none' : `1px solid ${colors.line}`,
                background: active ? 'rgba(0,175,171,0.10)' : '#fff',
                padding: '0 12px',
                margin: 0,
                cursor: 'pointer',
                font: 'inherit',
                color: 'inherit',
                textAlign: 'inherit',
                height: '28px',
                borderRadius: '9999px',
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center',
              }}
            >
              <span style={{
                fontFamily: 'Gilroy',
                fontWeight: 600,
                fontSize: '12px',
                color: active ? colors.teal2 : colors.muted,
              }}>{r}</span>
            </button>
          );
        })}
      </div>

      {/* Sell / Buy buttons — wide, paired, with price under the action. */}
      <div style={{
        marginTop: '16px',
        display: 'flex',
        flexDirection: 'row',
        gap: '12px',
      }}>
        <SideButton
          side="sell"
          price={window.formatPrice(sym.bid, sym)}
          onClick={() => openTicket('sell')}
        />
        <SideButton
          side="buy"
          price={window.formatPrice(sym.ask, sym)}
          onClick={() => openTicket('buy')}
        />
      </div>
    </Card>
  );
};

/* Hero Sell/Buy CTA — height 58, radius 14, two-line label. */
const SideButton = ({ side, price, onClick }) => {
  const bg = side === 'buy' ? colors.teal : colors.red;
  const label = side === 'buy' ? 'Buy' : 'Sell';
  return (
    <button
      onClick={onClick}
      style={{
        border: 'none',
        background: bg,
        padding: 0,
        margin: 0,
        cursor: 'pointer',
        font: 'inherit',
        color: 'inherit',
        textAlign: 'inherit',
        flex: 1,
        height: '58px',
        borderRadius: '14px',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        gap: '2px',
      }}
    >
      <span style={{
        fontFamily: 'Gilroy',
        fontWeight: 600,
        fontSize: '14px',
        color: '#fff',
      }}>{label}</span>
      <span style={{
        fontFamily: 'Inter',
        fontWeight: 500,
        fontSize: '12px',
        color: 'rgba(255,255,255,0.85)',
        fontVariantNumeric: 'tabular-nums',
      }}>{price}</span>
    </button>
  );
};

/* ── Instrument list row ─────────────────────────────────────────────────
   Tapping selects the row's symbol as the hero and scrolls to top. */
const InstrumentRow = ({ sym, onSelect, isLast }) => {
  const positive = sym.change >= 0;
  const pctColor = positive ? colors.green : colors.red;
  const pctText = `${positive ? '+' : '−'}${Math.abs(sym.change).toFixed(2)}%`;
  return (
    <button
      onClick={() => onSelect(sym.sym)}
      style={{
        border: 'none',
        background: 'transparent',
        padding: 0,
        margin: 0,
        cursor: 'pointer',
        font: 'inherit',
        color: 'inherit',
        textAlign: 'inherit',
        width: '100%',
        display: 'block',
      }}
    >
      <div style={{
        height: '64px',
        padding: '0 14px',
        display: 'flex',
        flexDirection: 'row',
        alignItems: 'center',
        gap: '12px',
        borderBottom: isLast ? 'none' : `1px solid ${colors.line}`,
      }}>
        <window.SymbolMark sym={sym} size={36} />
        {/* Name stack. */}
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{
            fontFamily: 'Gilroy',
            fontWeight: 600,
            fontSize: '14px',
            color: colors.ink,
            lineHeight: 1.1,
          }}>{sym.sym}</div>
          <div style={{
            fontFamily: 'Inter',
            fontWeight: 400,
            fontSize: '12px',
            color: colors.muted2,
            marginTop: '2px',
            overflow: 'hidden',
            textOverflow: 'ellipsis',
            whiteSpace: 'nowrap',
          }}>{sym.name}</div>
        </div>
        {/* Right-side stack: tiny spark + bid/ask + % pill. */}
        <div style={{
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'flex-end',
          gap: '2px',
        }}>
          <Sparkline up={positive} width={56} height={20} />
          <div style={{
            fontFamily: 'Inter',
            fontSize: '12px',
            fontVariantNumeric: 'tabular-nums',
            display: 'flex',
            flexDirection: 'row',
            gap: '4px',
            alignItems: 'baseline',
          }}>
            <span style={{ color: colors.red, fontWeight: 500 }}>{window.formatPrice(sym.bid, sym)}</span>
            <span style={{ color: colors.muted3 }}>/</span>
            <span style={{ color: colors.green, fontWeight: 500 }}>{window.formatPrice(sym.ask, sym)}</span>
          </div>
          <div style={{
            display: 'inline-flex',
            alignItems: 'center',
            justifyContent: 'center',
            padding: '1px 7px',
            borderRadius: '9999px',
            backgroundColor: positive ? 'rgba(7,148,85,0.10)' : 'rgba(240,68,56,0.10)',
            fontFamily: 'Inter',
            fontWeight: 600,
            fontSize: '11px',
            color: pctColor,
            fontVariantNumeric: 'tabular-nums',
          }}>{pctText}</div>
        </div>
      </div>
    </button>
  );
};

/* ── Order ticket sheet ─────────────────────────────────────────────────
   Stepper field — label above; numeric input; +/− buttons at the right
   that bump by `step`. The actual value is stored as a string in parent
   state so the user can type freely; the steppers parse, bump, and write
   back a fixed-decimal string. */
const StepperField = ({ label, value, setValue, step, decimals, placeholder }) => {
  /* Bump applies + or − and re-formats. Empty stays empty for sl/tp. */
  const bump = (dir) => {
    const cur = parseFloat(value);
    const base = isNaN(cur) ? 0 : cur;
    const next = Math.max(0, base + dir * step);
    setValue(next.toFixed(decimals));
  };
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
      <span style={{
        fontFamily: 'Inter',
        fontWeight: 500,
        fontSize: '12px',
        color: colors.muted,
      }}>{label}</span>
      <div style={{
        height: '48px',
        padding: '0 14px',
        backgroundColor: '#fff',
        border: `1px solid ${colors.line}`,
        borderRadius: '12px',
        display: 'flex',
        flexDirection: 'row',
        alignItems: 'center',
        gap: '8px',
      }}>
        <input
          type="text"
          inputMode="numeric"
          value={value}
          placeholder={placeholder || ''}
          onChange={(e) => setValue(e.target.value)}
          style={{
            flex: 1,
            border: 'none',
            outline: 'none',
            background: 'transparent',
            fontFamily: 'Gilroy',
            fontWeight: 600,
            fontSize: '16px',
            color: colors.ink,
            fontVariantNumeric: 'tabular-nums',
            padding: 0,
            margin: 0,
            width: '100%',
            minWidth: 0,
          }}
        />
        {/* Stacked +/− steppers, 14×14 each. */}
        <div style={{
          display: 'flex',
          flexDirection: 'column',
          gap: '2px',
        }}>
          <StepBtn dir={+1} onClick={() => bump(+1)} />
          <StepBtn dir={-1} onClick={() => bump(-1)} />
        </div>
      </div>
    </div>
  );
};

const StepBtn = ({ dir, onClick }) => (
  <button
    onClick={onClick}
    aria-label={dir > 0 ? 'increase' : 'decrease'}
    style={{
      border: `1px solid ${colors.line}`,
      background: '#fff',
      padding: 0,
      margin: 0,
      cursor: 'pointer',
      font: 'inherit',
      color: 'inherit',
      textAlign: 'inherit',
      width: '14px',
      height: '14px',
      borderRadius: '4px',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      lineHeight: 1,
    }}
  >
    <span style={{
      fontFamily: 'Gilroy',
      fontWeight: 700,
      fontSize: '11px',
      color: colors.muted,
      lineHeight: 1,
    }}>{dir > 0 ? '+' : '−'}</span>
  </button>
);

const ORDER_TICKET_ANIM_MS = 420;
const ORDER_TICKET_EASE = 'cubic-bezier(0.32, 0.72, 0.24, 1)';

const OrderTicket = ({ ticket, close }) => {
  /* Read the live store sym so the displayed bid/ask and the price snapshot
     used for placement always reflect the latest tick — not the stale value
     the hero captured when the ticket opened. */
  const sym = window.useSymbol(ticket.symId);
  const { addPosition } = window.useTradeMutators();
  /* Hook must run unconditionally — read free margin BEFORE the
     null-sym early return so React's hook order stays stable. */
  const { freeMargin } = window.useFreeMargin();

  /* Open / close choreography. `shown` toggles the scrim opacity and the
     inner sheet's translateY between 0 and 100%. Double-rAF on mount so
     the initial off-screen frame paints before the transition target
     flips, otherwise the slide animation would never engage. */
  const [shown, setShown] = React.useState(false);
  const closingRef = React.useRef(false);
  React.useEffect(() => {
    let raf2;
    const raf1 = requestAnimationFrame(() => {
      raf2 = requestAnimationFrame(() => setShown(true));
    });
    return () => {
      cancelAnimationFrame(raf1);
      if (raf2) cancelAnimationFrame(raf2);
    };
  }, []);
  /* Animated close: fade scrim + slide sheet down, then unmount via the
     parent's `close` prop after the transition window. Guards against
     double-fires from rapid scrim taps + close button. */
  const beginClose = React.useCallback(() => {
    if (closingRef.current) return;
    closingRef.current = true;
    setShown(false);
    setTimeout(close, ORDER_TICKET_ANIM_MS);
  }, [close]);

  /* The side preselects from whichever big button opened the sheet, but the
     trader can flip it inside. */
  const [side, setSide] = React.useState(ticket.side);
  const [vol, setVol] = React.useState('0.10');
  const [sl, setSl] = React.useState('');
  const [tp, setTp] = React.useState('');

  /* IMPORTANT: this useEffect MUST live before the `if (!sym) return null`
     early-return. Hooks-rules requires identical hook count per render.
     When sym briefly becomes null (race during a voice-triggered route
     change), the early-return path skipped this useEffect; the next
     render — with sym non-null — added it back and React crashed with
     "Rendered fewer hooks than expected", blanking the page.

     The effect body internally tolerates missing sym/handlers (it only
     attaches imperative handles to window.equiti.orderTicket; if the
     user calls those before sym is ready, the handler short-circuits). */
  React.useEffect(() => {
    if (!sym) return undefined;
    window.equiti = window.equiti || {};
    window.equiti.orderTicket = {
      setVolume: (v) => setVol(String(v == null ? '' : v)),
      setSide: (s) => setSide(s === 'sell' ? 'sell' : 'buy'),
      setStopLoss: (v) => setSl(String(v == null ? '' : v)),
      setTakeProfit: (v) => setTp(String(v == null ? '' : v)),
      place: handlePlace,
      close: beginClose,
      canPlace: () => !insufficientMargin && parseFloat(vol) > 0,
    };
    return () => {
      if (window.equiti) delete window.equiti.orderTicket;
    };
  });

  if (!sym) return null;

  /* Step sizes vary by instrument category. Forex & metals trade in lots
     (.01); crypto in tiny coin units; stocks in shares. SL/TP step = 1 pip
     for forex, 1 unit otherwise — close enough for a UI demo. */
  const isForex = sym.cat === 'forex';
  const volStep = 0.01;
  const volDecimals = 2;
  const priceStep = isForex ? 0.0001 : 1;
  const priceDecimals = isForex ? 5 : 2;

  /* Required margin uses the same contract sizes as P/L computation so
     forex = 100k, gold = 100, silver = 5k, indices = 1, stocks/crypto = 1.
     Leverage is the house value (1:500). */
  const volNum = parseFloat(vol) || 0;
  const contract = window.getContractSize(sym);
  const notional = volNum * contract * sym.bid;
  const margin = window.computeRequiredMargin(sym, sym.cat, volNum, sym.bid);
  /* Spread in pips: forex = 1e4 × spread, others = raw price spread. */
  const spreadRaw = sym.ask - sym.bid;
  const spreadPips = isForex ? (spreadRaw * 10000).toFixed(1) : spreadRaw.toFixed(2);
  const marginText = '$' + margin.toLocaleString('en-US', { maximumFractionDigits: 2, minimumFractionDigits: 2 });

  /* Free-margin gate — the user can't place a trade whose required
     margin exceeds what's left after existing open positions. The CTA
     dims and labels itself when blocked; handlePlace also short-circuits
     so the disabled visual + a stray rapid click don't dispatch. */
  const insufficientMargin = volNum > 0 && margin > freeMargin;
  const freeMarginText = '$' + Math.max(0, freeMargin).toLocaleString('en-US', { maximumFractionDigits: 2, minimumFractionDigits: 2 });

  const ctaBg = insufficientMargin ? colors.muted : (side === 'buy' ? colors.teal : colors.red);
  const ctaLabel = insufficientMargin
    ? 'Insufficient free margin'
    : `Place ${side === 'buy' ? 'Buy' : 'Sell'} ${vol || '0'} ${sym.sym}`;

  /* Place — dispatch a new position into the store. Open price mirrors real
     trading: buys fill on the ask, sells fill on the bid. SL/TP are visual
     only; the store doesn't yet track them. */
  const handlePlace = () => {
    if (insufficientMargin) return;
    const id = 'p' + Date.now();
    const volume = parseFloat(vol) || 0.10;
    /* Parse SL/TP — user-entered strings; only pass through if valid &
       positive. The store will drop undefined fields via spread. */
    const slNum = parseFloat(sl);
    const tpNum = parseFloat(tp);
    addPosition({
      id,
      sym: sym.sym,
      side,
      size: volume,
      open: side === 'buy' ? sym.ask : sym.bid,
      sl: isFinite(slNum) && slNum > 0 ? slNum : undefined,
      tp: isFinite(tpNum) && tpNum > 0 ? tpNum : undefined,
      openedAt: new Date().toLocaleString('en-US', {
        month: 'short',
        day: 'numeric',
        hour: '2-digit',
        minute: '2-digit',
      }),
    });
    beginClose();
    if (window.showToast) {
      window.showToast(
        'Order placed · ' + (side === 'buy' ? 'Buy' : 'Sell') + ' ' + volume + ' ' + sym.sym,
        'success'
      );
    }
  };

  /* Programmatic bridge for voice tool place_trade. setVolume drives
     the visible vol input so the user sees the lot size change as the
     agent commits to it; place + close mirror the manual buttons.
     Re-bound on every render so closures have the latest values. */
  React.useEffect(() => {
    window.equiti = window.equiti || {};
    window.equiti.orderTicket = {
      setVolume: (v) => setVol(String(v == null ? '' : v)),
      setSide: (s) => setSide(s === 'sell' ? 'sell' : 'buy'),
      setStopLoss: (v) => setSl(String(v == null ? '' : v)),
      setTakeProfit: (v) => setTp(String(v == null ? '' : v)),
      place: handlePlace,
      close: beginClose,
      canPlace: () => !insufficientMargin && parseFloat(vol) > 0,
    };
    return () => {
      if (window.equiti) delete window.equiti.orderTicket;
    };
  });

  return (
    <div
      onClick={beginClose}
      style={{
        /* position:absolute so the modal scrim is contained by the
           .app-frame (the nearest positioned ancestor) instead of
           expanding to the whole browser viewport. On desktop the
           app frame is the iPhone mockup (~390 wide); position:fixed
           would have let the scrim leak past the bezel. */
        position: 'absolute',
        top: 0, right: 0, bottom: 0, left: 0,
        background: 'rgba(14,20,32,0.45)',
        zIndex: 9999,
        display: 'flex',
        flexDirection: 'column',
        justifyContent: 'flex-end',
        opacity: shown ? 1 : 0,
        transition: 'opacity ' + ORDER_TICKET_ANIM_MS + 'ms ' + ORDER_TICKET_EASE,
      }}
    >
      <div
        onClick={(e) => e.stopPropagation()}
        style={{
          backgroundColor: '#fff',
          borderRadius: '24px 24px 0 0',
          padding: '20px',
          maxHeight: '600px',
          boxShadow: '0px -12px 40px rgba(14,20,32,0.24)',
          display: 'flex',
          flexDirection: 'column',
          gap: '14px',
          overflowY: 'auto',
          WebkitOverflowScrolling: 'touch',
          transform: shown ? 'translateY(0)' : 'translateY(100%)',
          transition: 'transform ' + ORDER_TICKET_ANIM_MS + 'ms ' + ORDER_TICKET_EASE,
        }}
      >
        {/* Drag handle pill. */}
        <div style={{
          width: '60px',
          height: '4px',
          borderRadius: '2px',
          backgroundColor: colors.line,
          alignSelf: 'center',
          marginTop: '-4px',
          marginBottom: '4px',
        }} />

        {/* Header — symbol + close. */}
        <div style={{
          display: 'flex',
          flexDirection: 'row',
          alignItems: 'center',
          gap: '12px',
        }}>
          <window.SymbolMark sym={sym} size={36} />
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{
              fontFamily: 'Gilroy',
              fontWeight: 600,
              fontSize: '16px',
              color: colors.ink,
              lineHeight: 1.1,
            }}>{sym.sym}</div>
            <div style={{
              fontFamily: 'Inter',
              fontWeight: 400,
              fontSize: '12px',
              color: colors.muted2,
              marginTop: '2px',
            }}>{sym.name}</div>
          </div>
          <button
            onClick={beginClose}
            aria-label="Close"
            style={{
              border: 'none',
              background: colors.bg,
              padding: 0,
              margin: 0,
              cursor: 'pointer',
              font: 'inherit',
              color: 'inherit',
              textAlign: 'inherit',
              width: '32px',
              height: '32px',
              borderRadius: '16px',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
            }}
          >
            <CloseIcon />
          </button>
        </div>

        {/* Side toggle. White when active, soft bg when inactive. */}
        <div style={{
          display: 'flex',
          flexDirection: 'row',
          gap: '8px',
        }}>
          {[
            ['sell', 'Sell', colors.red],
            ['buy', 'Buy', colors.teal],
          ].map(([k, lbl, accent]) => {
            const active = side === k;
            return (
              <button
                key={k}
                onClick={() => setSide(k)}
                style={{
                  border: active ? `1px solid ${accent}` : `1px solid ${colors.line}`,
                  background: active ? '#fff' : colors.bg,
                  padding: 0,
                  margin: 0,
                  cursor: 'pointer',
                  font: 'inherit',
                  color: 'inherit',
                  textAlign: 'inherit',
                  flex: 1,
                  height: '44px',
                  borderRadius: '12px',
                  display: 'flex',
                  alignItems: 'center',
                  justifyContent: 'center',
                }}
              >
                <span style={{
                  fontFamily: 'Gilroy',
                  fontWeight: 600,
                  fontSize: '14px',
                  color: active ? accent : colors.muted,
                }}>{lbl}</span>
              </button>
            );
          })}
        </div>

        {/* Numeric fields. */}
        <StepperField
          label="Volume (lots)"
          value={vol}
          setValue={setVol}
          step={volStep}
          decimals={volDecimals}
          placeholder="0.10"
        />
        <StepperField
          label="Stop Loss"
          value={sl}
          setValue={setSl}
          step={priceStep}
          decimals={priceDecimals}
          placeholder="—"
        />
        <StepperField
          label="Take Profit"
          value={tp}
          setValue={setTp}
          step={priceStep}
          decimals={priceDecimals}
          placeholder="—"
        />

        {/* Summary — required margin + free margin + spread. The free
           margin number shows what the account has left to deploy after
           existing open positions; turns red when this trade would
           exceed it. */}
        <div style={{
          fontFamily: 'Inter',
          fontWeight: 400,
          fontSize: '12px',
          color: colors.muted,
          lineHeight: 1.5,
        }}>
          <div>
            Required margin: <span style={{
              color: insufficientMargin ? colors.red : colors.ink2,
              fontVariantNumeric: 'tabular-nums',
              fontWeight: insufficientMargin ? 600 : 400,
            }}>{marginText}</span>
            {' • Free margin: '}
            <span style={{ color: colors.ink2, fontVariantNumeric: 'tabular-nums' }}>{freeMarginText}</span>
          </div>
          <div>Spread: <span style={{ color: colors.ink2, fontVariantNumeric: 'tabular-nums' }}>{spreadPips} pips</span></div>
        </div>

        {/* CTA — dispatches the position into the trade store, then closes.
           Disabled (greyed + non-interactive) when the trade would exceed
           free margin. */}
        <button
          onClick={handlePlace}
          disabled={insufficientMargin}
          style={{
            border: 'none',
            background: ctaBg,
            padding: 0,
            margin: 0,
            cursor: insufficientMargin ? 'not-allowed' : 'pointer',
            font: 'inherit',
            color: 'inherit',
            textAlign: 'inherit',
            width: '100%',
            height: '54px',
            borderRadius: '14px',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            marginTop: '4px',
            opacity: insufficientMargin ? 0.7 : 1,
          }}
        >
          <span style={{
            fontFamily: 'Gilroy',
            fontWeight: 600,
            fontSize: '16px',
            color: '#fff',
          }}>{ctaLabel}</span>
        </button>
      </div>
    </div>
  );
};

/* ── TradePage ─────────────────────────────────────────────────────────── */
const TradePage = ({ selectedSym: selectedSymProp, setSelectedSym: setSelectedSymProp }) => {
  /* Live symbol data from the TradeStore. allSymbols re-renders when the
     tick engine dispatches updateSymbol; sym is the featured row's slice.
     The shell now owns selectedSym (so Discover can change it before
     navigating). Fall back to a local state if not provided so this
     component still works in isolation. */
  const allSymbols = window.useAllSymbols();
  const [localSelectedSym, setLocalSelectedSym] = React.useState('EURUSD');
  const selectedSym = selectedSymProp != null ? selectedSymProp : localSelectedSym;
  const setSelectedSym = setSelectedSymProp != null ? setSelectedSymProp : setLocalSelectedSym;
  const sym = window.useSymbol(selectedSym);

  /* Watchlist seeded from WATCHLIST_IDS — visual only, no store update. */
  const [watch, setWatch] = React.useState(() => new Set(WATCHLIST_IDS));
  const [range, setRange] = React.useState('1m');
  const [category, setCategory] = React.useState('all');
  const [sortIdx, setSortIdx] = React.useState(0);
  const [orderTicket, setOrderTicket] = React.useState(null);
  const [expanded, setExpanded] = React.useState(false);
  /* Ref to the scroll container so a row tap can scroll the page to top. */
  const scrollRef = React.useRef(null);

  /* Programmatic bridge so the voice tool place_trade can pop the order
     ticket open before filling in volume + clicking Place. Re-binds on
     every render so the closure has the latest sym in scope.

     IMPORTANT: this useEffect MUST be declared before the `if (!sym)
     return null` early-return below — React's rules-of-hooks require
     the same hook count on every render. When `sym` was momentarily
     null (e.g. during a tab/route switch triggered by a voice tool),
     the early-return path skipped this useEffect, then when sym
     became non-null the next render added it back — React crashes
     with "Rendered fewer hooks than expected" and the whole page
     blanks out. The body of the effect handles sym being null
     internally (uses optional chaining on sym?.sym). */
  React.useEffect(() => {
    window.equiti = window.equiti || {};
    window.equiti.openOrderTicket = (args) => {
      const side = args && args.side === 'sell' ? 'sell' : 'buy';
      const targetSym = (args && args.symbol) || (sym && sym.sym);
      if (!targetSym) return false;
      setOrderTicket({ side, symId: targetSym });
      return true;
    };
    return () => {
      if (window.equiti) delete window.equiti.openOrderTicket;
    };
  }, [sym]);

  /* If the store hasn't seeded yet (rare race during boot) show nothing
     rather than throwing on a missing sym. All hooks above this line
     run unconditionally; everything below this line is render-only. */
  if (!sym) return null;

  const watched = watch.has(sym.sym);

  const toggleWatch = () => {
    setWatch((prev) => {
      const next = new Set(prev);
      if (next.has(sym.sym)) next.delete(sym.sym);
      else next.add(sym.sym);
      return next;
    });
  };

  const cycleSort = () => setSortIdx((i) => (i + 1) % SORT_LABELS.length);

  /* The ticket only needs the symbol id so it can read the live sym slice
     itself — keeps prices fresh while the sheet is open.
     Symbols without a live demo feed (`realFeed: 'dummy'`) are gated:
     the order ticket never opens and the user sees a notification at
     the top of the frame. The notification carries the symbol so its
     body copy can name the instrument. US30 has its own variant
     ('liquidityDown') so its outage reads as a price-feed problem
     rather than a closed market. */
  const openTicket = (side) => {
    if (sym.realFeed !== 'dummy') {
      const kind = sym.sym === 'US30' ? 'liquidityDown' : 'marketClosed';
      if (window.showCashFlowNotification) {
        window.showCashFlowNotification(kind, { sym: sym.sym, name: sym.name });
      }
      return;
    }
    setOrderTicket({ side, symId: sym.sym });
  };

  const onSelectRow = (symId) => {
    setSelectedSym(symId);
    if (scrollRef.current) scrollRef.current.scrollTo({ top: 0, behavior: 'smooth' });
  };

  /* Filter the live symbol list by category. "All" passes everything. */
  const list = category === 'all' ? allSymbols : allSymbols.filter((s) => s.cat === category);

  return (
    <div
      ref={scrollRef}
      style={{
        flex: 1,
        width: '100%',
        height: '100%',
        overflowY: 'auto',
        WebkitOverflowScrolling: 'touch',
        scrollbarWidth: 'none',
        backgroundColor: colors.bg,
        position: 'relative',
      }}
    >
      <div style={{
        paddingTop: '16px',
        paddingLeft: '20px',
        paddingRight: '20px',
        paddingBottom: '180px',
        display: 'flex',
        flexDirection: 'column',
        gap: '14px',
      }}>
        <HeroCard
          sym={sym}
          watched={watched}
          toggleWatch={toggleWatch}
          range={range}
          setRange={setRange}
          openTicket={openTicket}
          onExpand={() => setExpanded(true)}
        />

        <FilterRow
          category={category}
          setCategory={setCategory}
          sortIdx={sortIdx}
          cycleSort={cycleSort}
        />

        {/* List card — zero horizontal padding so rows hit edges. */}
        <Card padding={{ horizontal: 0, vertical: 6 }}>
          {list.map((s, i) => (
            <InstrumentRow
              key={s.sym}
              sym={s}
              onSelect={onSelectRow}
              isLast={i === list.length - 1}
            />
          ))}
        </Card>
      </div>

      {/* Order ticket overlay — only mounted when a ticket exists. */}
      {orderTicket ? (
        <OrderTicket
          ticket={orderTicket}
          close={() => setOrderTicket(null)}
        />
      ) : null}

      <window.ExpandedChart sym={expanded ? sym : null} onClose={() => setExpanded(false)} />
    </div>
  );
};

/* Exposed so the expanded-chart sheet can use the same static candle
   shape as the inline hero chart. */
Object.assign(window, { TradePage, makeStaticCandles });
})();
