// ============================================================
// API4ATKA mobile — sheets + root router (MobileApp)
// Все обработчики дёргают реальный backend через window.MobileAPI.
// ============================================================

// ------------------------------------------------------------
// Close Position sheet — реально закрывает позицию через /api/ex/weex/close-position
// ------------------------------------------------------------
const ClosePositionSheet = ({ p, onClose, onConfirm }) => {
  const [pctSel, setPctSel] = useState(100);
  const [orderType, setOrderType] = useState("market");
  const [limitPrice, setLimitPrice] = useState(price(p.mark));
  const [busy, setBusy] = useState(false);
  const up = p.pnl >= 0;
  const qty = +(p.qty * pctSel / 100).toFixed(6);
  const realized = +(p.pnl * pctSel / 100).toFixed(2);

  const submit = async () => {
    if (busy) return;
    setBusy(true);
    buzz(20);
    try {
      await onConfirm(p, pctSel, orderType, limitPrice, qty);
    } finally {
      setBusy(false);
    }
  };

  const foot = (
    <>
      <button className="m-button ghost lg" disabled={busy} onClick={() => { buzz(); onClose(); }}>{T("common.cancel", "Отмена")}</button>
      <button className={"m-button lg " + (up ? "long" : "short")} disabled={busy} onClick={submit}>
        {busy ? T("common.closing", "Закрываем…") : (<><MIcon name="check" size={18}/> Закрыть {orderType}</>)}
      </button>
    </>
  );

  return (
    <BottomSheet title={T("portfolio.closePosition", "Закрытие позиции")} onClose={onClose} foot={foot}>
      <div className="m-card" style={{ marginBottom: 4 }}>
        <div className="row1" style={{ display: "flex", alignItems: "center", gap: 10 }}>
          <Coin sym={p.coin}/>
          <span className="mono" style={{ fontSize: 15, fontWeight: 600 }}>{p.sym}</span>
          <span style={{ display: "flex", gap: 6, marginLeft: "auto" }}><Side side={p.side}/><span className="m-pill lev">{p.lev}x</span></span>
        </div>
        <div className="grid2" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "8px 16px", marginTop: 12, fontSize: 13 }}>
          <div><span className="muted" style={{ fontSize: 11 }}>{T("portfolio.size", "Размер")}</span><div className="mono">{p.qty} {p.qtyUnit}</div></div>
          <div><span className="muted" style={{ fontSize: 11 }}>{T("portfolio.margin", "Маржа")}</span><div className="mono">{money(p.margin)} USDT</div></div>
          <div><span className="muted" style={{ fontSize: 11 }}>{T("portfolio.entry", "Вход")}</span><div className="mono">{price(p.entry)}</div></div>
          <div><span className="muted" style={{ fontSize: 11 }}>{T("portfolio.now", "Сейчас")}</span><div className="mono">{price(p.mark)}</div></div>
        </div>
        <div style={{ marginTop: 12, paddingTop: 12, borderTop: "1px solid var(--border)", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
          <span className="muted" style={{ fontSize: 12 }}>{T("portfolio.currentPnl", "Текущий PnL")}</span>
          <span className={"mono " + (up ? "pos" : "neg")} style={{ fontSize: 18, fontWeight: 700 }}>
            {signed(p.pnl)} <span style={{ fontSize: 13 }}>({pct(p.pnlPct)})</span>
          </span>
        </div>
      </div>

      <div className="sheet-label">{T("portfolio.closePartial", "Закрыть часть")}</div>
      <div className="pct-grid">
        {[25, 50, 75, 100].map(v => (
          <button key={v} className={"pct-btn" + (pctSel === v ? " active" : "") + (v === 100 && pctSel === 100 ? " full" : "")}
            onClick={() => { buzz(); setPctSel(v); }}>{v}%</button>
        ))}
      </div>
      <div className="mono" style={{ fontSize: 13, color: "var(--text-2)", marginTop: 10 }}>
        qty: <span style={{ color: "var(--text)" }}>{qty} {p.qtyUnit}</span>
        <span style={{ color: "var(--text-4)", margin: "0 8px" }}>·</span>
        реализуем: <span className={up ? "pos" : "neg"}>{signed(realized)} USDT</span>
      </div>

      <div className="sheet-label">{T("portfolio.orderType", "Тип ордера")}</div>
      <div className="m-segmented">
        <button className={orderType === "market" ? "active" : ""} onClick={() => { buzz(); setOrderType("market"); }}>Market</button>
        <button className={orderType === "limit" ? "active" : ""} onClick={() => { buzz(); setOrderType("limit"); }}>Limit</button>
      </div>
      {orderType === "limit" && (
        <input className="m-input" style={{ marginTop: 10 }} inputMode="decimal" value={limitPrice}
          onChange={e => setLimitPrice(e.target.value)} placeholder={T("portfolio.limitPrice", "Цена лимит-ордера")}/>
      )}
    </BottomSheet>
  );
};

// ------------------------------------------------------------
// Edit Trade (TP/SL) sheet — реальное изменение через close+reopen.
// WEEX не имеет прямого endpoint'а для модификации TP/SL открытой позиции,
// поэтому бэкенд закрывает позицию по market и переоткрывает с новыми уровнями.
// Будет минимальный slippage (1-2 тика обычно).
// ------------------------------------------------------------
const EditTradeSheet = ({ p, onClose, onToast, onSaved }) => {
  const [tp, setTp] = useState(p.tp > 0 ? price(p.tp) : "");
  const [sl, setSl] = useState(p.sl > 0 ? price(p.sl) : "");
  const [busy, setBusy] = useState(false);
  const [confirmStage, setConfirmStage] = useState(false);

  const submit = async () => {
    if (busy) return;
    const tpN = parseFloat(tp);
    const slN = parseFloat(sl);
    const tpOk = tpN > 0;
    const slOk = slN > 0;
    if (!tpOk && !slOk) {
      onToast({ msg: T("error.fillTpOrSl", "Заполни хотя бы TP или SL"), err: true });
      return;
    }
    if (!confirmStage) { setConfirmStage(true); return; }
    setBusy(true);
    try {
      await window.MobileAPI.modifyPositionTpSl(
        p.sym, p.side,
        tpOk ? tpN : null,
        slOk ? slN : null,
      );
      onToast({ msg: `TP/SL для ${p.sym} обновлены` });
      onSaved && onSaved();
      onClose();
    } catch (e) {
      onToast({ msg: "Ошибка: " + (e.message || e).slice(0, 100), err: true });
    } finally {
      setBusy(false);
      setConfirmStage(false);
    }
  };

  const foot = (
    <>
      <button className="m-button ghost lg" disabled={busy} onClick={() => { buzz(); onClose(); }}>
        Отмена
      </button>
      <button className="m-button primary lg" disabled={busy} onClick={submit}>
        {busy ? T("common.applying", "Применяю…") : confirmStage ? T("common.confirm", "✓ Подтвердить") : T("common.apply", "Применить")}
      </button>
    </>
  );

  return (
    <BottomSheet title={T("portfolio.tpSlTitle", "TP / SL позиции")} onClose={onClose} foot={foot}>
      <div className="m-card" style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 12 }}>
        <Coin sym={p.coin}/>
        <span className="mono" style={{ fontSize: 15, fontWeight: 600 }}>{p.sym}</span>
        <span style={{ display: "flex", gap: 6, marginLeft: "auto" }}>
          <Side side={p.side}/>
          <span className="m-pill lev">{p.lev}x</span>
        </span>
      </div>

      <div className="sheet-label">
        <span style={{ color: "var(--long)" }}>Take Profit</span>
      </div>
      <input className="m-input" inputMode="decimal" value={tp}
        onChange={e => setTp(e.target.value)} placeholder={`текущая цена: ${price(p.mark)}`}/>

      <div className="sheet-label">
        <span style={{ color: "var(--short)" }}>Stop Loss</span>
      </div>
      <input className="m-input" inputMode="decimal" value={sl}
        onChange={e => setSl(e.target.value)} placeholder={`ликвидация: ${price(p.liq)}`}/>

      <div className="m-card" style={{ marginTop: 14, fontSize: 12, lineHeight: 1.55 }}>
        <div style={{ display: "flex", justifyContent: "space-between", padding: "4px 0" }}>
          <span className="muted">{T("portfolio.entry", "Вход")}</span><span className="mono">{price(p.entry)}</span>
        </div>
        <div style={{ display: "flex", justifyContent: "space-between", padding: "4px 0" }}>
          <span className="muted">{T("portfolio.now", "Сейчас")}</span><span className="mono">{price(p.mark)}</span>
        </div>
        <div style={{ display: "flex", justifyContent: "space-between", padding: "4px 0" }}>
          <span className="muted">{T("portfolio.margin", "Маржа")}</span><span className="mono">{money(p.margin)} USDT</span>
        </div>
        <div style={{ display: "flex", justifyContent: "space-between", padding: "4px 0" }}>
          <span className="muted">{T("portfolio.size", "Размер")}</span><span className="mono">{p.qty} {p.qtyUnit}</span>
        </div>
      </div>

      <div style={{
        display: "flex", gap: 10, padding: 14, marginTop: 14,
        background: confirmStage ? "var(--short-bg)" : "var(--warn-dim)",
        border: confirmStage ? "1px solid var(--short-dim)" : "1px solid transparent",
        borderRadius: "var(--r-2)", color: "var(--text-2)", fontSize: 13, lineHeight: 1.5,
      }}>
        <MIcon name="lock" size={18} color={confirmStage ? "var(--short)" : "var(--warn)"}/>
        <div>
          {confirmStage ? (
            <>
              <b style={{ color: "var(--short)" }}>{T("common.confirmPrompt", "Подтверди:")}</b> позиция будет <b>{T("portfolio.closedByMarket", "закрыта по market")}</b> и сразу
              переоткрыта с новыми TP/SL. Возможен slippage 0.05-0.2%.
            </>
          ) : (
            <>{T("portfolio.tpSlHint", "WEEX не даёт менять TP/SL напрямую — это будет close + reopen той же позиции с новыми уровнями. Жми «Применить», потом подтверди.")}</>
          )}
        </div>
      </div>
    </BottomSheet>
  );
};

// ------------------------------------------------------------
// Change Password sheet
// ------------------------------------------------------------
const ChangePasswordSheet = ({ onClose, onToast }) => {
  const [oldPwd, setOldPwd] = useState("");
  const [newPwd, setNewPwd] = useState("");
  const [busy, setBusy] = useState(false);

  const submit = async () => {
    if (busy) return;
    if (!oldPwd || !newPwd) { onToast({ msg: T("error.fillBothFields", "Заполни оба поля"), err: true }); return; }
    if (newPwd.length < 8) { onToast({ msg: T("error.pwdMin8", "Новый пароль минимум 8 символов"), err: true }); return; }
    setBusy(true);
    try {
      await window.MobileAPI.changePassword(oldPwd, newPwd);
      onToast({ msg: T("toast.pwdUpdated", "Пароль обновлён") });
      onClose();
    } catch (e) {
      onToast({ msg: "Ошибка: " + (e.message || e).slice(0, 80), err: true });
    } finally { setBusy(false); }
  };

  const foot = (
    <>
      <button className="m-button ghost lg" disabled={busy} onClick={() => { buzz(); onClose(); }}>{T("common.cancel", "Отмена")}</button>
      <button className="m-button primary lg" disabled={busy} onClick={submit}>
        {busy ? T("common.saving", "Сохраняем…") : T("common.save", "Сохранить")}
      </button>
    </>
  );

  return (
    <BottomSheet title={T("pwd.change", "Сменить пароль")} onClose={onClose} foot={foot}>
      <div className="sheet-label">{T("pwd.current", "Текущий пароль")}</div>
      <input className="m-input" type="password" autoComplete="current-password"
        value={oldPwd} onChange={e => setOldPwd(e.target.value)}/>
      <div className="sheet-label">{T("pwd.new", "Новый пароль")}</div>
      <input className="m-input" type="password" autoComplete="new-password"
        value={newPwd} onChange={e => setNewPwd(e.target.value)} placeholder={T("pwd.minHint", "минимум 8 символов")}/>
    </BottomSheet>
  );
};

// ------------------------------------------------------------
// TG-Connect sheet — реальный QR + Code + Import flow
// ------------------------------------------------------------
const TG_DEFAULTS_KEY = "m_tg_defaults";

const TgConnectSheet = ({ onClose, onToast, onConnected }) => {
  const [tab, setTab] = useState("qr");
  // Сохранённые api_id / api_hash из localStorage (юзер вводит один раз)
  const [apiId, setApiId] = useState("");
  const [apiHash, setApiHash] = useState("");
  const [phone, setPhone] = useState("");
  useEffect(() => {
    try {
      const d = JSON.parse(localStorage.getItem(TG_DEFAULTS_KEY) || "{}");
      if (d.api_id) setApiId(String(d.api_id));
      if (d.api_hash) setApiHash(d.api_hash);
      if (d.phone) setPhone(d.phone);
    } catch (e) {}
  }, []);
  const persist = (p) => {
    try { localStorage.setItem(TG_DEFAULTS_KEY, JSON.stringify({
      api_id: parseInt(apiId) || 0, api_hash: apiHash, phone, ...p,
    })); } catch (e) {}
  };

  // ----- QR state -----
  const [qrUrl, setQrUrl] = useState("");
  const [qrStage, setQrStage] = useState("idle"); // idle | waiting | needs_password | done | error
  const [qrErr, setQrErr] = useState("");
  const [qrPwd, setQrPwd] = useState("");
  const [busy, setBusy] = useState(false);
  const pollRef = useRef(null);

  const stopPoll = () => { if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } };
  useEffect(() => () => { stopPoll(); window.MobileAPI.tgQrCancel(); }, []);

  const startQr = async () => {
    if (busy) return;
    if (!apiId || !apiHash) { onToast({ msg: T("error.fillApiIdHash", "Заполни api_id и api_hash"), err: true }); return; }
    setBusy(true); setQrErr(""); setQrStage("waiting");
    persist({});
    try {
      const r = await window.MobileAPI.tgQrStart(apiId, apiHash);
      setQrUrl(r.url || "");
      pollRef.current = setInterval(async () => {
        try {
          const p = await window.MobileAPI.tgQrPoll();
          if (p.url && p.url !== qrUrl) setQrUrl(p.url);
          if (p.status === "waiting") return;
          if (p.status === "needs_password") {
            stopPoll(); setQrStage("needs_password"); return;
          }
          if (p.status === "connected") {
            stopPoll(); setQrStage("done");
            onToast({ msg: T("toast.tgConnected", "Telegram подключён") });
            onConnected && onConnected();
            setTimeout(onClose, 500);
            return;
          }
          if (p.status === "expired" || p.status === "error") {
            stopPoll(); setQrStage("error"); setQrErr(p.error || T("error.qrExpired", "QR истёк"));
          }
        } catch (e) { console.warn(e); }
      }, 2500);
    } catch (e) {
      setQrStage("error"); setQrErr(e.message || String(e));
    } finally { setBusy(false); }
  };

  const submitQrPwd = async () => {
    if (busy || !qrPwd) return;
    setBusy(true);
    try {
      await window.MobileAPI.tgQrPassword(qrPwd);
      setQrStage("done");
      onToast({ msg: T("toast.tgConnected", "Telegram подключён") });
      onConnected && onConnected();
      setTimeout(onClose, 500);
    } catch (e) {
      onToast({ msg: "Неверный пароль: " + (e.message || e).slice(0, 60), err: true });
    } finally { setBusy(false); }
  };

  // ----- Code state -----
  const [codeStage, setCodeStage] = useState("idle"); // idle | sent | needs_password | done
  const [code, setCode] = useState("");
  const [codePwd, setCodePwd] = useState("");

  const sendCode = async (forceSms) => {
    if (busy) return;
    if (!apiId || !apiHash || !phone) { onToast({ msg: T("error.fillApiPhone", "Заполни api_id, api_hash и phone"), err: true }); return; }
    setBusy(true);
    persist({});
    try {
      await window.MobileAPI.tgSendCode(apiId, apiHash, phone, !!forceSms);
      setCodeStage("sent");
      onToast({ msg: forceSms ? T("toast.smsSent", "SMS отправлена") : T("toast.codeSentTg", "Код отправлен в Telegram") });
    } catch (e) {
      onToast({ msg: "Ошибка: " + (e.message || e).slice(0, 80), err: true });
    } finally { setBusy(false); }
  };

  const submitCode = async () => {
    if (busy || !code) return;
    setBusy(true);
    try {
      const r = await window.MobileAPI.tgVerifyCode(code);
      if (r && r.needs_password) {
        setCodeStage("needs_password");
      } else {
        setCodeStage("done");
        onToast({ msg: T("toast.tgConnected", "Telegram подключён") });
        onConnected && onConnected();
        setTimeout(onClose, 500);
      }
    } catch (e) {
      const msg = (e.message || String(e));
      if (/password/i.test(msg)) { setCodeStage("needs_password"); }
      else onToast({ msg: "Неверный код: " + msg.slice(0, 60), err: true });
    } finally { setBusy(false); }
  };

  const submitCodePwd = async () => {
    if (busy || !codePwd) return;
    setBusy(true);
    try {
      await window.MobileAPI.tgVerifyPassword(codePwd);
      setCodeStage("done");
      onToast({ msg: T("toast.tgConnected", "Telegram подключён") });
      onConnected && onConnected();
      setTimeout(onClose, 500);
    } catch (e) {
      onToast({ msg: "Неверный пароль: " + (e.message || e).slice(0, 60), err: true });
    } finally { setBusy(false); }
  };

  // ----- Import state -----
  const [importSession, setImportSession] = useState("");

  const submitImport = async () => {
    if (busy) return;
    if (!apiId || !apiHash || !phone) { onToast({ msg: T("error.fillApiPhone", "Заполни api_id, api_hash и phone"), err: true }); return; }
    if (importSession.length < 50) { onToast({ msg: T("error.sessionTooShort", "Слишком короткая session"), err: true }); return; }
    setBusy(true);
    try {
      await window.MobileAPI.tgImportSession(apiId, apiHash, phone, importSession);
      onToast({ msg: T("toast.sessionImported", "Сессия импортирована") });
      onConnected && onConnected();
      setTimeout(onClose, 500);
    } catch (e) {
      onToast({ msg: "Ошибка импорта: " + (e.message || e).slice(0, 80), err: true });
    } finally { setBusy(false); }
  };

  // ----- shared API fields -----
  const ApiFields = () => (
    <>
      <div className="sheet-label">api_id</div>
      <input className="m-input" inputMode="numeric" value={apiId}
        onChange={e => setApiId(e.target.value.replace(/\D/g, ""))} placeholder={T("tg.apiIdPlaceholder", "напр. 1234567")}/>
      <div className="sheet-label">api_hash</div>
      <input className="m-input" value={apiHash} onChange={e => setApiHash(e.target.value.trim())}
        placeholder={T("tg.apiHashHint", "32 hex символа")}/>
      <div style={{ fontSize: 11, color: "var(--text-3)", marginTop: 6 }}>
        Получить: <a href="https://my.telegram.org/auth" target="_blank" rel="noopener noreferrer" style={{ color: "var(--accent)" }}>my.telegram.org</a> → API tools
      </div>
    </>
  );

  return (
    <BottomSheet title={T("tg.connect", "Подключить Telegram")} onClose={onClose}>
      <div className="m-segmented" style={{ marginBottom: 16 }}>
        <button className={tab === "qr" ? "active" : ""} onClick={() => { buzz(); setTab("qr"); }}>QR</button>
        <button className={tab === "code" ? "active" : ""} onClick={() => { buzz(); setTab("code"); }}>{T("tg.code", "Код")}</button>
        <button className={tab === "import" ? "active" : ""} onClick={() => { buzz(); setTab("import"); }}>{T("tg.import", "Импорт")}</button>
      </div>

      {tab === "qr" && (
        <div>
          {qrStage === "idle" && (
            <>
              <ApiFields/>
              <button className="m-button primary lg block" style={{ marginTop: 16 }} onClick={startQr} disabled={busy}>
                {busy ? "..." : T("tg.getQr", "Получить QR")}
              </button>
              <div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 12, lineHeight: 1.5, textAlign: "center" }}>
                ★ Рекомендуется. Telegram с 2024 блокирует SMS для сторонних app, а QR работает всегда.
              </div>
            </>
          )}
          {qrStage === "waiting" && (
            <div style={{ textAlign: "center" }}>
              <div style={{ width: 220, height: 220, margin: "0 auto 16px", borderRadius: "var(--r-3)", background: "#fff", display: "grid", placeItems: "center", padding: 14 }}>
                {qrUrl ? <RealQr url={qrUrl}/> : <span style={{ color: "#999" }}>{T("tg.generating", "Генерация…")}</span>}
              </div>
              <div style={{ fontSize: 14, color: "var(--text-2)", lineHeight: 1.5 }}>
                Открой Telegram → Настройки → Устройства → <b style={{ color: "var(--text)" }}>{T("tg.linkDevice", "Подключить устройство")}</b>
              </div>
              <button className="m-button ghost lg block" style={{ marginTop: 12 }}
                onClick={() => { stopPoll(); window.MobileAPI.tgQrCancel(); setQrStage("idle"); setQrUrl(""); }}>
                Отмена
              </button>
            </div>
          )}
          {qrStage === "needs_password" && (
            <div>
              <div style={{ fontSize: 14, color: "var(--text-2)", marginBottom: 12 }}>{T("tg.twoFaPrompt", "Включена 2FA — введи облачный пароль:")}</div>
              <input className="m-input" type="password" value={qrPwd} onChange={e => setQrPwd(e.target.value)}
                placeholder={T("tg.twoFaPassword", "2FA пароль")}/>
              <button className="m-button primary lg block" style={{ marginTop: 12 }} onClick={submitQrPwd} disabled={busy}>
                {busy ? "..." : T("common.confirm", "Подтвердить")}
              </button>
            </div>
          )}
          {qrStage === "done" && (
            <div style={{ textAlign: "center", padding: 24 }}>
              <MIcon name="check" size={48} color="var(--long)"/>
              <div style={{ marginTop: 12, fontSize: 16, fontWeight: 600 }}>{T("common.done", "Готово")}</div>
            </div>
          )}
          {qrStage === "error" && (
            <div>
              <div style={{ fontSize: 14, color: "var(--short)", marginBottom: 12 }}>{qrErr}</div>
              <button className="m-button lg block" onClick={() => { setQrStage("idle"); setQrErr(""); }}>{T("common.retry", "Попробовать снова")}</button>
            </div>
          )}
        </div>
      )}

      {tab === "code" && (
        <div>
          {codeStage === "idle" && (
            <>
              <ApiFields/>
              <div className="sheet-label">{T("tg.phone", "Номер телефона")}</div>
              <input className="m-input" inputMode="tel" value={phone}
                onChange={e => setPhone(e.target.value)} placeholder="+7 900 000-00-00"/>
              <button className="m-button primary lg block" style={{ marginTop: 12 }} disabled={busy}
                onClick={() => sendCode(false)}>{busy ? "..." : T("tg.sendCodeTg", "Прислать код в Telegram")}</button>
              <button className="m-button ghost lg block" style={{ marginTop: 8 }} disabled={busy}
                onClick={() => sendCode(true)}>{busy ? "..." : T("tg.requestSms", "Запросить SMS")}</button>
            </>
          )}
          {codeStage === "sent" && (
            <div>
              <div className="sheet-label">{T("tg.confirmCode", "Код подтверждения")}</div>
              <input className="m-input" inputMode="numeric" value={code}
                onChange={e => setCode(e.target.value.replace(/\D/g, ""))}
                placeholder={T("tg.fiveDigits", "5 цифр")}/>
              <button className="m-button primary lg block" style={{ marginTop: 12 }} disabled={busy}
                onClick={submitCode}>{busy ? "..." : T("common.confirm", "Подтвердить")}</button>
              <button className="m-button ghost sm block" style={{ marginTop: 8 }} disabled={busy}
                onClick={() => setCodeStage("idle")}>{T("common.back", "Назад")}</button>
            </div>
          )}
          {codeStage === "needs_password" && (
            <div>
              <div style={{ fontSize: 14, color: "var(--text-2)", marginBottom: 12 }}>{T("tg.twoFaPrompt", "Включена 2FA — введи облачный пароль:")}</div>
              <input className="m-input" type="password" value={codePwd}
                onChange={e => setCodePwd(e.target.value)} placeholder={T("tg.twoFaPassword", "2FA пароль")}/>
              <button className="m-button primary lg block" style={{ marginTop: 12 }} disabled={busy}
                onClick={submitCodePwd}>{busy ? "..." : T("common.confirm", "Подтвердить")}</button>
            </div>
          )}
          {codeStage === "done" && (
            <div style={{ textAlign: "center", padding: 24 }}>
              <MIcon name="check" size={48} color="var(--long)"/>
              <div style={{ marginTop: 12, fontSize: 16, fontWeight: 600 }}>{T("common.done", "Готово")}</div>
            </div>
          )}
        </div>
      )}

      {tab === "import" && (
        <div>
          <ApiFields/>
          <div className="sheet-label">{T("tg.phone", "Номер телефона")}</div>
          <input className="m-input" inputMode="tel" value={phone}
            onChange={e => setPhone(e.target.value)} placeholder="+7 900 000-00-00"/>
          <div className="sheet-label">{T("tg.sessionString", "Session-строка")}</div>
          <textarea className="m-input" style={{ height: 96, padding: 12, resize: "none", lineHeight: 1.5 }}
            value={importSession} onChange={e => setImportSession(e.target.value)}
            placeholder={T("tg.pasteSessionHint", "Вставь сессию из tools/export_session.py")}></textarea>
          <button className="m-button primary lg block" style={{ marginTop: 12 }} disabled={busy}
            onClick={submitImport}>{busy ? "..." : T("common.import", "Импортировать")}</button>
          <div style={{ fontSize: 12, color: "var(--warn)", marginTop: 12, display: "flex", gap: 8, alignItems: "flex-start" }}>
            <MIcon name="lock" size={14}/> Сессия шифруется и хранится только на сервере бота.
          </div>
        </div>
      )}
    </BottomSheet>
  );
};

// Настоящий QR через window.qrcode (qrcode-generator@1.4.4 уже подключён в index.html)
const RealQr = ({ url }) => {
  const ref = useRef(null);
  useEffect(() => {
    if (!ref.current || !window.qrcode || !url) return;
    try {
      const qr = window.qrcode(0, "M");
      qr.addData(url);
      qr.make();
      ref.current.innerHTML = qr.createSvgTag({ scalable: true });
      const svg = ref.current.querySelector("svg");
      if (svg) { svg.setAttribute("width", "100%"); svg.setAttribute("height", "100%"); }
    } catch (e) { console.warn("QR render:", e); }
  }, [url]);
  return <div ref={ref} style={{ width: "100%", height: "100%" }}/>;
};

// ============================================================
// Settings sheets — leverage / size / exchange / creds / bot / channels
// ============================================================

const _save = async (fn, onToast, okMsg = T("toast.saved", "Сохранено")) => {
  try { await fn(); onToast({ msg: okMsg }); return true; }
  catch (e) { onToast({ msg: "Ошибка: " + (e.message || e).slice(0, 100), err: true }); return false; }
};

// ------------------------------------------------------------
// Leverage sheet — default_leverage 1..125
// ------------------------------------------------------------
const LeverageSheet = ({ initial, onClose, onToast, onSaved }) => {
  const init = (initial && typeof initial === "object") ? initial : { value: initial || 10, mode: "fixed" };
  const [lev, setLev] = useState(init.leverage || init.value || 10);
  const [levMode, setLevMode] = useState(init.levMode || init.mode || "fixed");
  const [busy, setBusy] = useState(false);

  const submit = async () => {
    if (busy) return;
    const n = parseInt(lev);
    if (!(n >= 1 && n <= 125)) { onToast({ msg: T("lev.range", "Плечо: 1..125"), err: true }); return; }
    setBusy(true);
    const ok = await _save(
      () => window.MobileAPI.updateSettings({ default_leverage: n, leverage_mode: levMode }),
      onToast, `Плечо: ${n}x · ${levMode === "fixed" ? "fixed" : levMode}`);
    setBusy(false);
    if (ok) { onSaved && onSaved(); onClose(); }
  };

  const foot = (
    <>
      <button className="m-button ghost lg" disabled={busy} onClick={() => { buzz(); onClose(); }}>{T("common.cancel", "Отмена")}</button>
      <button className="m-button primary lg" disabled={busy} onClick={submit}>{busy ? "..." : T("common.save", "Сохранить")}</button>
    </>
  );

  return (
    <BottomSheet title={T("lev.title", "Плечо")} onClose={onClose} foot={foot}>
      <div className="sheet-label">{T("lev.mode", "Режим")}</div>
      <div className="m-segmented">
        <button className={levMode === "fixed" ? "active" : ""} onClick={() => { buzz(); setLevMode("fixed"); }}>Fixed</button>
        <button className={levMode === "from_signal" ? "active" : ""} onClick={() => { buzz(); setLevMode("from_signal"); }}>{T("lev.fromSignal", "Из сигнала")}</button>
        <button className={levMode === "max" ? "active" : ""} onClick={() => { buzz(); setLevMode("max"); }}>Max</button>
      </div>
      <div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 8, lineHeight: 1.5 }}>
        {levMode === "fixed" && T("lev.fixedDesc", "Всегда использовать заданное плечо.")}
        {levMode === "from_signal" && T("lev.fromSignalDesc", "Брать плечо из текста сигнала, иначе — fallback ниже.")}
        {levMode === "max" && T("lev.maxDesc", "Использовать максимальное доступное на бирже.")}
      </div>

      {levMode !== "max" && (
        <>
          <div className="sheet-label">Значение плеча ({levMode === "from_signal" ? "fallback" : "fixed"})</div>
          <input className="m-input" inputMode="numeric" value={lev}
            onChange={e => setLev(e.target.value.replace(/\D/g, ""))} placeholder={T("lev.rangeShort", "1..125")}/>
          <div className="pct-grid" style={{ marginTop: 10 }}>
            {[5, 10, 25, 50].map(v => (
              <button key={v} className={"pct-btn" + (parseInt(lev) === v ? " active" : "")}
                onClick={() => { buzz(); setLev(v); }}>{v}x</button>
            ))}
          </div>
        </>
      )}
    </BottomSheet>
  );
};

// ------------------------------------------------------------
// Size sheet — fixed_usdt | percent_balance
// ------------------------------------------------------------
const SizeSheet = ({ initial, onClose, onToast, onSaved }) => {
  const [mode, setMode] = useState(initial?.sizeMode || "fixed_usdt");
  const [val, setVal] = useState(
    initial?.sizeMode === "percent_balance" ? (initial?.riskPct || 1) : (initial?.posSizeUsdt || 5),
  );
  const [busy, setBusy] = useState(false);

  const submit = async () => {
    if (busy) return;
    const n = parseFloat(val);
    if (mode === "fixed_usdt") {
      if (!(n >= 0.1 && n <= 100000)) { onToast({ msg: T("size.rangeUsdt", "Размер: 0.1..100000 USDT"), err: true }); return; }
    } else {
      if (!(n >= 0.01 && n <= 100)) { onToast({ msg: T("size.rangePercent", "Процент: 0.01..100"), err: true }); return; }
    }
    setBusy(true);
    const patch = mode === "fixed_usdt"
      ? { size_mode: mode, default_position_size_usdt: n }
      : { size_mode: mode, size_percent: n };
    const ok = await _save(
      () => window.MobileAPI.updateSettings(patch),
      onToast, `Размер: ${mode === "fixed_usdt" ? n + " USDT" : n + T("size.percentBalance", "% баланса")}`);
    setBusy(false);
    if (ok) { onSaved && onSaved(); onClose(); }
  };

  const foot = (
    <>
      <button className="m-button ghost lg" disabled={busy} onClick={() => { buzz(); onClose(); }}>{T("common.cancel", "Отмена")}</button>
      <button className="m-button primary lg" disabled={busy} onClick={submit}>{busy ? "..." : T("common.save", "Сохранить")}</button>
    </>
  );

  return (
    <BottomSheet title={T("size.title", "Размер сделки")} onClose={onClose} foot={foot}>
      <div className="sheet-label">{T("lev.mode", "Режим")}</div>
      <div className="m-segmented">
        <button className={mode === "fixed_usdt" ? "active" : ""}
          onClick={() => { buzz(); setMode("fixed_usdt"); setVal(initial?.posSizeUsdt || 5); }}>Fixed USDT</button>
        <button className={mode === "percent_balance" ? "active" : ""}
          onClick={() => { buzz(); setMode("percent_balance"); setVal(initial?.riskPct || 1); }}>{T("size.percentBalance", "% баланса")}</button>
      </div>
      <div className="sheet-label">{mode === "fixed_usdt" ? T("size.inUsdt", "Размер в USDT") : T("size.percentFree", "% от свободного баланса")}</div>
      <input className="m-input" inputMode="decimal" value={val} onChange={e => setVal(e.target.value)}
        placeholder={mode === "fixed_usdt" ? T("size.example5", "напр. 5") : T("size.example1", "напр. 1")}/>
      <div className="pct-grid" style={{ marginTop: 10 }}>
        {(mode === "fixed_usdt" ? [5, 10, 25, 50] : [0.5, 1, 2, 5]).map(v => (
          <button key={v} className={"pct-btn" + (parseFloat(val) === v ? " active" : "")}
            onClick={() => { buzz(); setVal(v); }}>{v}{mode === "fixed_usdt" ? "$" : "%"}</button>
        ))}
      </div>
      <div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 14, lineHeight: 1.5 }}>
        {mode === "fixed_usdt"
          ? T("size.usdtDesc", "Каждая сделка открывается на эту сумму маржи (× плечо = notional).")
          : T("size.percentDesc", "Бот возьмёт указанный процент от свободного USDT-баланса в момент сигнала.")}
      </div>
    </BottomSheet>
  );
};

// ------------------------------------------------------------
// Exchange sheet — active_exchange
// ------------------------------------------------------------
const EX_LABELS = { weex: "WEEX", binance: "Binance", bybit: "Bybit", okx: "OKX" };
const ExchangeSheet = ({ initial, onClose, onToast, onSaved }) => {
  const [ex, setEx] = useState((initial || "weex").toLowerCase());
  const [busy, setBusy] = useState(false);

  const submit = async () => {
    if (busy) return;
    setBusy(true);
    const ok = await _save(
      () => window.MobileAPI.updateSettings({ active_exchange: ex }),
      onToast, `Биржа: ${EX_LABELS[ex] || ex}`);
    setBusy(false);
    if (ok) { onSaved && onSaved(); onClose(); }
  };

  const foot = (
    <>
      <button className="m-button ghost lg" disabled={busy} onClick={() => { buzz(); onClose(); }}>{T("common.cancel", "Отмена")}</button>
      <button className="m-button primary lg" disabled={busy} onClick={submit}>{busy ? "..." : T("common.apply", "Применить")}</button>
    </>
  );

  return (
    <BottomSheet title={T("ex.select", "Выбор биржи")} onClose={onClose} foot={foot}>
      <div className="sheet-label">{T("ex.active", "Активная биржа")}</div>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
        {(window.MobileAPI.SUPPORTED_EXCHANGES || []).map(e => (
          <button key={e}
            className={"m-button lg " + (ex === e ? "primary" : "")}
            onClick={() => { buzz(); setEx(e); }}>
            {EX_LABELS[e] || e.toUpperCase()}
          </button>
        ))}
      </div>
      <div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 14, lineHeight: 1.5 }}>
        Все позиции/баланс/история будут читаться с выбранной биржи.
        Перед сменой убедись, что API-ключ для неё уже сохранён.
      </div>
    </BottomSheet>
  );
};

// ------------------------------------------------------------
// API creds sheet
// ------------------------------------------------------------
const ApiCredsSheet = ({ exchange, currentMask, activeLabel, onClose, onToast, onSaved }) => {
  const ex = (exchange || "weex").toLowerCase();
  const needsPass = (window.MobileAPI.PASSPHRASE_REQUIRED || []).includes(ex);
  const [accounts, setAccounts] = useState([]);
  const [showAdd, setShowAdd] = useState(false);
  const [label, setLabel] = useState("default");
  const [k, setK] = useState("");
  const [s, setS] = useState("");
  const [p, setP] = useState("");
  const [busy, setBusy] = useState(false);

  const load = useCallback(async () => {
    const list = await window.MobileAPI.listExchangeAccounts(ex);
    setAccounts(list || []);
    setShowAdd((list || []).length === 0);
  }, [ex]);
  useEffect(() => { load(); }, [load]);

  const submit = async () => {
    if (busy) return;
    if (!k || !s) { onToast({ msg: T("creds.required", "key и secret обязательны"), err: true }); return; }
    if (needsPass && !p) { onToast({ msg: `${EX_LABELS[ex] || ex}: passphrase обязателен`, err: true }); return; }
    const lbl = (label || "default").trim() || "default";
    setBusy(true);
    const ok = await _save(
      () => window.MobileAPI.updateExchangeCreds(ex, k, s, p, lbl),
      onToast, `${EX_LABELS[ex] || ex}/${lbl} сохранён`);
    setBusy(false);
    if (ok) {
      setK(""); setS(""); setP("");
      setShowAdd(false);
      await load();
      onSaved && onSaved();
    }
  };

  const remove = async (lbl) => {
    if (busy) return;
    if (!confirm(`Удалить аккаунт ${EX_LABELS[ex] || ex}/${lbl}?`)) return;
    setBusy(true);
    const ok = await _save(
      () => window.MobileAPI.deleteExchangeCreds(ex, lbl),
      onToast, T("creds.toast.deleted", "Удалён"));
    setBusy(false);
    if (ok) { await load(); onSaved && onSaved(); }
  };

  const activate = async (lbl) => {
    if (busy) return;
    setBusy(true);
    const ok = await _save(
      () => window.MobileAPI.setActiveAccount(ex, lbl),
      onToast, `Активный: ${EX_LABELS[ex] || ex}/${lbl}`);
    setBusy(false);
    if (ok) { onSaved && onSaved(); onClose(); }
  };

  return (
    <BottomSheet title={`${EX_LABELS[ex] || ex} — аккаунты`} onClose={onClose}>
      {accounts.length > 0 && !showAdd && (
        <>
          <div className="sheet-label">{T("creds.section.connectedAccounts", "Подключённые аккаунты")}</div>
          {accounts.map(a => {
            const isActive = a.label === activeLabel;
            return (
              <div key={a.label} className="m-card" style={{ marginBottom: 8 }}>
                <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
                  <span className="si" style={{ width: 30, height: 30, borderRadius: 8, background: isActive ? "var(--accent-dim)" : "var(--bg-2)", display: "grid", placeItems: "center" }}>
                    <MIcon name="key" size={15} color={isActive ? "var(--accent)" : "var(--text-3)"}/>
                  </span>
                  <div style={{ flex: 1 }}>
                    <div style={{ fontWeight: 600, fontSize: 14 }}>
                      {a.label} {isActive && <span className="m-pill" style={{ background: "var(--accent-dim)", color: "var(--accent)", marginLeft: 6 }}>{T("creds.badge.active", "активный")}</span>}
                    </div>
                    <div className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>{a.key_masked}</div>
                  </div>
                </div>
                <div style={{ display: "flex", gap: 8, marginTop: 8 }}>
                  {!isActive && (
                    <button className="m-button sm" style={{ flex: 1 }} disabled={busy} onClick={() => activate(a.label)}>
                      Сделать активным
                    </button>
                  )}
                  <button className="m-button danger sm" disabled={busy} onClick={() => remove(a.label)}>
                    <MIcon name="x" size={14}/>
                  </button>
                </div>
              </div>
            );
          })}
          <button className="m-button block" style={{ marginTop: 12 }} onClick={() => { buzz(); setShowAdd(true); setLabel("default"); }}>
            <MIcon name="plus" size={16}/> Добавить ещё аккаунт
          </button>
        </>
      )}

      {showAdd && (
        <>
          <div className="sheet-label">{T("creds.row.label", "Название аккаунта (label)")}</div>
          <input className="m-input" value={label} onChange={e => setLabel(e.target.value)}
            placeholder={T("creds.placeholder.label", "напр. main / test / sub")}/>
          <div className="sheet-label">API key</div>
          <input className="m-input" value={k} onChange={e => setK(e.target.value)} autoComplete="off" spellCheck={false}/>
          <div className="sheet-label">API secret</div>
          <input className="m-input" type="password" value={s} onChange={e => setS(e.target.value)} autoComplete="off"/>
          {needsPass && (
            <>
              <div className="sheet-label">Passphrase</div>
              <input className="m-input" type="password" value={p} onChange={e => setP(e.target.value)} autoComplete="off"/>
            </>
          )}
          <div style={{ display: "flex", gap: 8, marginTop: 14 }}>
            {accounts.length > 0 && (
              <button className="m-button ghost" style={{ flex: 1 }} disabled={busy}
                onClick={() => { setShowAdd(false); setK(""); setS(""); setP(""); }}>
                Отмена
              </button>
            )}
            <button className="m-button primary" style={{ flex: 2 }} disabled={busy} onClick={submit}>
              {busy ? "..." : T("common.save", "Сохранить")}
            </button>
          </div>
          <div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 14, lineHeight: 1.5 }}>
            Можно иметь несколько ключей на одной бирже (основной + тестовый + …).
            Активный определяется здесь — все запросы (баланс, ордера) идут на него.
          </div>
        </>
      )}
    </BottomSheet>
  );
};

// ------------------------------------------------------------
// Confirm-bot token sheet
// ------------------------------------------------------------
const BotTokenSheet = ({ currentUsername, onClose, onToast, onSaved }) => {
  const [token, setToken] = useState("");
  const [busy, setBusy] = useState(false);

  const submit = async () => {
    if (busy) return;
    if (token.length < 20) { onToast({ msg: T("tg.error.tokenTooShort", "Bot token слишком короткий"), err: true }); return; }
    setBusy(true);
    const ok = await _save(
      () => window.MobileAPI.setBotToken(token),
      onToast, T("tg.toast.connected", "Confirm-Bot подключён"));
    setBusy(false);
    if (ok) { onSaved && onSaved(); onClose(); }
  };

  const unlink = async () => {
    if (busy) return;
    if (!confirm(T("tg.confirm.unlink", "Отвязать Confirm-Bot?"))) return;
    setBusy(true);
    const ok = await _save(
      () => window.MobileAPI.clearBotToken(),
      onToast, T("tg.toast.unlinked", "Confirm-Bot отвязан"));
    setBusy(false);
    if (ok) { onSaved && onSaved(); onClose(); }
  };

  const foot = (
    <>
      <button className="m-button ghost lg" disabled={busy} onClick={() => { buzz(); onClose(); }}>{T("common.cancel", "Отмена")}</button>
      <button className="m-button primary lg" disabled={busy} onClick={submit}>{busy ? "..." : T("tg.action.link", "Привязать")}</button>
    </>
  );

  return (
    <BottomSheet title={T("tg.title", "Confirm-Bot")} onClose={onClose} foot={foot}>
      {currentUsername && (
        <div style={{ fontSize: 13, color: "var(--text-2)", marginBottom: 12 }}>
          Подключён: <span className="mono" style={{ color: "var(--text)" }}>{currentUsername}</span>
        </div>
      )}
      <div className="sheet-label">{T("tg.row.botToken", "Bot token (от @BotFather)")}</div>
      <input className="m-input" value={token} onChange={e => setToken(e.target.value.trim())}
        placeholder="1234567890:AAH…"/>
      <div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 14, lineHeight: 1.5 }}>
        1) Создай бота через @BotFather → получи токен.<br/>
        2) Открой DM своему боту в Telegram и нажми <b style={{ color: "var(--text)" }}>/start</b>.<br/>
        3) Вставь токен сюда — бот пришлёт тестовое сообщение.
      </div>
      {currentUsername && (
        <button className="m-button danger block" style={{ marginTop: 16 }} disabled={busy} onClick={unlink}>
          <MIcon name="x" size={16}/> Отвязать
        </button>
      )}
    </BottomSheet>
  );
};

// ------------------------------------------------------------
// Risk guards sheet — порог дневного убытка, ликвидации, max positions
// ------------------------------------------------------------
const RiskGuardsSheet = ({ initial, onClose, onToast, onSaved }) => {
  const [dailyLoss, setDailyLoss] = useState(initial?.maxDailyLossPct || 0);
  const [liqWarn, setLiqWarn] = useState(initial?.liqWarnPct || 5);
  const [maxPos, setMaxPos] = useState(initial?.maxOpenPositions || 0);
  const [busy, setBusy] = useState(false);

  const submit = async () => {
    if (busy) return;
    setBusy(true);
    const ok = await _save(
      () => window.MobileAPI.updateSettings({
        max_daily_loss_pct: parseFloat(dailyLoss) || 0,
        liq_warn_pct: parseFloat(liqWarn) || 0,
        max_open_positions: parseInt(maxPos) || 0,
      }),
      onToast, T("risk.toast.updated", "Защита капитала обновлена"));
    setBusy(false);
    if (ok) { onSaved && onSaved(); onClose(); }
  };

  const foot = (
    <>
      <button className="m-button ghost lg" disabled={busy} onClick={() => { buzz(); onClose(); }}>{T("common.cancel", "Отмена")}</button>
      <button className="m-button primary lg" disabled={busy} onClick={submit}>{busy ? "..." : T("common.save", "Сохранить")}</button>
    </>
  );

  return (
    <BottomSheet title={T("risk.title", "Защита капитала")} onClose={onClose} foot={foot}>
      <div className="sheet-label">{T("risk.row.dailyLossLimit", "Лимит дневного убытка, %")}</div>
      <input className="m-input" inputMode="decimal" value={dailyLoss}
        onChange={e => setDailyLoss(e.target.value)} placeholder={T("risk.placeholder.offHint", "0 = выкл")}/>
      <div style={{ fontSize: 11, color: "var(--text-3)", marginTop: 4 }}>
        Бот уйдёт в DRY-RUN и пришлёт push когда убыток за день превысит этот % от баланса. 0 = выкл.
      </div>

      <div className="sheet-label">{T("risk.row.liqPushDistance", "Push о ликвидации, % расстояние")}</div>
      <input className="m-input" inputMode="decimal" value={liqWarn}
        onChange={e => setLiqWarn(e.target.value)} placeholder="5"/>
      <div style={{ fontSize: 11, color: "var(--text-3)", marginTop: 4 }}>
        Получишь push когда mark будет ближе чем N% от цены ликвидации. 0 = выкл.
      </div>

      <div className="sheet-label">{T("risk.row.maxOpenPositions", "Макс. одновременных позиций")}</div>
      <input className="m-input" inputMode="numeric" value={maxPos}
        onChange={e => setMaxPos(e.target.value.replace(/\D/g, ""))} placeholder={T("risk.placeholder.offHint", "0 = выкл")}/>
      <div style={{ fontSize: 11, color: "var(--text-3)", marginTop: 4 }}>
        Если открыто N позиций — новые сигналы будут пропускаться. 0 = выкл.
      </div>
    </BottomSheet>
  );
};

// ============================================================
// Storm-mode sheet (panic button)
// ============================================================
const StormSheet = ({ stormUntilMs, onClose, onToast, onApplied }) => {
  const [hours, setHours] = useState(1);
  const [closePos, setClosePos] = useState(true);
  const [cancelPend, setCancelPend] = useState(true);
  const [busy, setBusy] = useState(false);
  const isActive = stormUntilMs > Date.now();

  const activate = async () => {
    if (busy) return;
    if (!confirm(`Закрыть все позиции и заблокировать сигналы на ${hours}ч?`)) return;
    setBusy(true);
    try {
      const r = await window.MobileAPI.activateStorm(hours, closePos, cancelPend);
      onToast({ msg: `🛑 Шторм: закрыто ${r.closed_positions}, отменено ${r.cancelled_pending}` });
      onApplied && onApplied();
      onClose();
    } catch (e) {
      onToast({ msg: "Ошибка: " + (e.message || e).slice(0, 80), err: true });
    } finally { setBusy(false); }
  };

  const deactivate = async () => {
    if (busy) return;
    setBusy(true);
    try {
      await window.MobileAPI.deactivateStorm();
      onToast({ msg: T("storm.toast.cleared", "✅ Шторм-режим снят") });
      onApplied && onApplied();
      onClose();
    } catch (e) {
      onToast({ msg: "Ошибка: " + (e.message || e).slice(0, 80), err: true });
    } finally { setBusy(false); }
  };

  return (
    <BottomSheet title={T("storm.title", "🛑 Шторм-режим")} onClose={onClose}>
      {isActive ? (
        <>
          <div className="m-card" style={{ padding: 16, background: "var(--short-bg)", border: "1px solid var(--short-dim)", marginBottom: 14, textAlign: "center" }}>
            <div style={{ fontWeight: 700, color: "var(--short)", fontSize: 16 }}>{T("storm.status.active", "Активен")}</div>
            <div className="muted" style={{ fontSize: 12, marginTop: 4 }}>
              до {new Date(stormUntilMs).toLocaleString("ru-RU", { hour: "2-digit", minute: "2-digit", day: "2-digit", month: "short" })}
            </div>
          </div>
          <button className="m-button primary lg block" disabled={busy} onClick={deactivate}>
            {busy ? "..." : T("storm.action.clear", "Снять шторм-режим")}
          </button>
        </>
      ) : (
        <>
          <div style={{ padding: 12, background: "var(--warn-dim)", borderRadius: 8, color: "var(--text-2)", fontSize: 13, lineHeight: 1.5, marginBottom: 14 }}>
            ⚠ Это закроет ВСЕ открытые позиции market, отменит pending-сигналы и включит DRY-RUN.
          </div>

          <div className="sheet-label">{T("storm.row.duration", "На сколько часов заблокировать?")}</div>
          <div className="pct-grid" style={{ marginTop: 6 }}>
            {[1, 4, 8, 24].map(h => (
              <button key={h} className={"pct-btn" + (hours === h ? " active" : "")}
                onClick={() => { buzz(); setHours(h); }}>{h}ч</button>
            ))}
          </div>

          <div className="m-card" style={{ marginTop: 14 }}>
            <div className="m-set-row" style={{ padding: "6px 0" }}>
              <span className="sc">
                <span className="st" style={{ fontSize: 13 }}>{T("storm.row.closePositions", "Закрыть позиции")}</span>
                <span className="ss" style={{ fontSize: 11 }}>{T("storm.hint.marketAll", "Market-ордер по всем")}</span>
              </span>
              <span className={"m-switch" + (closePos ? " on" : "")}
                onClick={() => { buzz(); setClosePos(v => !v); }}/>
            </div>
            <div className="m-set-row" style={{ padding: "6px 0", borderTop: "1px solid var(--border)" }}>
              <span className="sc">
                <span className="st" style={{ fontSize: 13 }}>{T("storm.row.cancelPending", "Отменить pending-сигналы")}</span>
                <span className="ss" style={{ fontSize: 11 }}>{T("storm.hint.preventOpen", "Не дать им открыться")}</span>
              </span>
              <span className={"m-switch" + (cancelPend ? " on" : "")}
                onClick={() => { buzz(); setCancelPend(v => !v); }}/>
            </div>
          </div>

          <button className="m-button short lg block" style={{ marginTop: 14, fontWeight: 700 }}
            disabled={busy} onClick={activate}>
            {busy ? T("storm.toast.closing", "Закрываю всё…") : `🛑 АКТИВИРОВАТЬ на ${hours}ч`}
          </button>
        </>
      )}
    </BottomSheet>
  );
};

// ============================================================
// PortfolioScreen — equity curve по биржам
// ============================================================
const PortfolioScreen = ({ exchange, onToast }) => {
  const [days, setDays] = useState(30);
  const [points, setPoints] = useState([]);
  const [loading, setLoading] = useState(true);
  const [chartLibReady, setChartLibReady] = useState(!!window.LightweightCharts);
  const [chartLibFailed, setChartLibFailed] = useState(false);
  const chartRef = useRef(null);
  const lcRef = useRef(null);

  // Чарт-библиотека загружается с CDN асинхронно. На медленной сети может
  // ещё не быть готовой к рендеру PortfolioScreen — ждём до 5 секунд.
  useEffect(() => {
    if (chartLibReady) return;
    if (window.LightweightCharts) { setChartLibReady(true); return; }
    let elapsed = 0;
    const id = setInterval(() => {
      elapsed += 200;
      if (window.LightweightCharts) { setChartLibReady(true); clearInterval(id); }
      else if (elapsed >= 5000) { setChartLibFailed(true); clearInterval(id); }
    }, 200);
    return () => clearInterval(id);
  }, [chartLibReady]);

  useEffect(() => {
    (async () => {
      setLoading(true);
      const r = await window.MobileAPI.loadEquityHistory(days, exchange);
      setPoints(r.points || []);
      setLoading(false);
    })();
  }, [days, exchange]);

  useEffect(() => {
    if (!chartRef.current || !chartLibReady || !window.LightweightCharts || points.length < 2) return;
    if (lcRef.current) {
      try { lcRef.current.remove(); } catch (e) {}
      lcRef.current = null;
    }
    const chart = window.LightweightCharts.createChart(chartRef.current, {
      width: chartRef.current.clientWidth,
      height: 220,
      layout: { background: { color: "transparent" }, textColor: "#9a9a9a" },
      grid: { vertLines: { color: "rgba(255,255,255,0.05)" }, horzLines: { color: "rgba(255,255,255,0.05)" } },
      rightPriceScale: { borderColor: "rgba(255,255,255,0.05)" },
      timeScale: { borderColor: "rgba(255,255,255,0.05)", timeVisible: true },
    });
    const series = chart.addAreaSeries({
      lineColor: "#22c55e", topColor: "rgba(34,197,94,0.25)",
      bottomColor: "rgba(34,197,94,0.02)", lineWidth: 2,
    });
    const data = points
      .filter(p => p.equity > 0)
      .map(p => ({ time: Math.floor(p.ts_ms / 1000), value: parseFloat(p.equity) }));
    series.setData(data);
    chart.timeScale().fitContent();
    lcRef.current = chart;
    const ro = new ResizeObserver(() => {
      if (chartRef.current && lcRef.current) {
        lcRef.current.applyOptions({ width: chartRef.current.clientWidth });
      }
    });
    ro.observe(chartRef.current);
    return () => { ro.disconnect(); try { chart.remove(); } catch (e) {} lcRef.current = null; };
  }, [points, chartLibReady]);

  const start = points[0]?.equity || 0;
  const end = points[points.length - 1]?.equity || 0;
  const delta = end - start;
  const deltaPct = start > 0 ? (delta / start * 100) : 0;
  // Max drawdown
  let peak = 0, mdd = 0;
  for (const p of points) {
    if (p.equity > peak) peak = p.equity;
    const dd = peak > 0 ? (peak - p.equity) / peak * 100 : 0;
    if (dd > mdd) mdd = dd;
  }

  return (
    <div className="m-scroll">
      <div className="m-hero">
        <div className="muted" style={{ fontSize: 11 }}>{T("storm.row.equityNow", "Equity сейчас")}</div>
        <div className="mono" style={{ fontSize: 30, fontWeight: 700, marginTop: 6 }}>
          {money(end)} <span className="muted" style={{ fontSize: 14 }}>USDT</span>
        </div>
        <div className={"mono " + (delta >= 0 ? "pos" : "neg")} style={{ fontSize: 13, marginTop: 4 }}>
          {delta >= 0 ? "+" : ""}{money(delta)} ({delta >= 0 ? "+" : ""}{deltaPct.toFixed(2)}%) за {days} дн
        </div>
      </div>

      <div className="m-segmented" style={{ margin: "12px 14px" }}>
        {[[7, T("stats.range.7d", "7 дн")], [30, T("stats.range.30d", "30 дн")], [90, T("stats.range.90d", "90 дн")], [365, T("stats.range.year", "Год")]].map(([d, l]) => (
          <button key={d} className={days === d ? "active" : ""}
            onClick={() => { buzz(); setDays(d); }}>{l}</button>
        ))}
      </div>

      {loading && <div className="m-card" style={{ margin: "0 14px", padding: 20, textAlign: "center", color: "var(--text-3)" }}>{T("common.loading", "Загрузка…")}</div>}
      {!loading && points.length < 2 && (
        <EmptyState icon="trendUp" title={T("stats.noData", "Нет данных за период")}
          msg={T("stats.snapshotsHint", "Снепшоты собираются раз в час. Подожди немного и вернись.")}/>
      )}
      {points.length >= 2 && chartLibFailed && (
        <div className="m-card" style={{ margin: "0 14px 12px", padding: 18, textAlign: "center", color: "var(--text-3)", fontSize: 13 }}>
          График недоступен (CDN заблокирован). Цифры ниже актуальны.
        </div>
      )}
      {points.length >= 2 && !chartLibFailed && (
        <>
          <div className="m-card" style={{ margin: "0 14px 12px", padding: 8 }}>
            {chartLibReady ? (
              <div ref={chartRef} style={{ width: "100%", height: 220 }}/>
            ) : (
              <div style={{ height: 220, display: "grid", placeItems: "center", color: "var(--text-3)", fontSize: 12 }}>
                Загрузка графика…
              </div>
            )}
          </div>
          <div className="m-card" style={{ margin: "0 14px" }}>
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 12, fontSize: 12 }}>
              <div><div className="muted" style={{ fontSize: 10 }}>{T("bots.start", "Старт")}</div><div className="mono">{money(start)}</div></div>
              <div><div className="muted" style={{ fontSize: 10 }}>{T("portfolio.now", "Сейчас")}</div><div className="mono">{money(end)}</div></div>
              <div><div className="muted" style={{ fontSize: 10 }}>Max drawdown</div><div className="mono neg">-{mdd.toFixed(2)}%</div></div>
            </div>
          </div>
        </>
      )}
    </div>
  );
};

// ============================================================
// Upgrade sheet — показывается когда юзер тапает premium-фичу
// ============================================================
const UpgradeSheet = ({ feature, currentTier, onClose }) => {
  const [plan, setPlan] = useState(null);
  useEffect(() => {
    window.MobileAPI.loadPlan().then(setPlan);
  }, []);
  const tiers = plan?.available_tiers || [];

  const featureLabels = {
    forwards: T("upgrade.feature.forwards", "Пересылка сигналов"),
    multi_account: T("upgrade.feature.multiAccount", "Несколько аккаунтов на бирже"),
    ai_strategies: T("upgrade.feature.ai", "AI-стратегии"),
    auto_execute: T("upgrade.feature.auto", "Автоисполнение сигналов"),
  };

  return (
    <BottomSheet title={T("upgrade.title", "Premium-фича")} onClose={onClose}>
      <div style={{ padding: 16, background: "var(--accent-dim)", borderRadius: 8, marginBottom: 14, textAlign: "center" }}>
        <div style={{ fontWeight: 700, fontSize: 16, marginBottom: 6 }}>
          {featureLabels[feature] || feature}
        </div>
        <div className="muted" style={{ fontSize: 12 }}>
          {T("upgrade.unlockedOn", "Доступно на тарифе Premium")}
        </div>
      </div>

      <div className="sheet-label">{T("upgrade.choosePlan", "Выбери тариф")}</div>
      {tiers.map(t => {
        const isCurrent = t.tier === currentTier;
        const features = t.features || {};
        return (
          <div key={t.tier} className="m-card" style={{
            marginBottom: 10, padding: 14,
            border: isCurrent ? "1px solid var(--accent)" : "1px solid var(--border)",
            background: isCurrent ? "var(--accent-dim)" : "var(--bg-2)",
          }}>
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 8 }}>
              <span style={{ fontWeight: 700, fontSize: 15 }}>{t.label}</span>
              <span className="mono" style={{ fontSize: 16, fontWeight: 700 }}>
                {t.price_usd === 0 ? T("upgrade.free", "Бесплатно") : `$${t.price_usd}/${T("upgrade.mo", "мес")}`}
              </span>
            </div>
            <div style={{ fontSize: 12, color: "var(--text-2)", lineHeight: 1.6 }}>
              <div>{features.max_channels === -1 ? "∞" : features.max_channels} {T("upgrade.channels", "каналов-источников")}</div>
              <div style={{ color: features.auto_execute ? "var(--long)" : "var(--text-4)" }}>
                {features.auto_execute ? "✓" : "✗"} {T("upgrade.feature.auto", "Автоисполнение")}
              </div>
              <div style={{ color: features.multi_account ? "var(--long)" : "var(--text-4)" }}>
                {features.multi_account ? "✓" : "✗"} {T("upgrade.feature.multiAccount", "Несколько аккаунтов")}
              </div>
              <div style={{ color: features.forwards ? "var(--long)" : "var(--text-4)" }}>
                {features.forwards ? "✓" : "✗"} {T("upgrade.feature.forwards", "Пересылка сигналов")}
              </div>
              <div style={{ color: features.ai_strategies ? "var(--long)" : "var(--text-4)" }}>
                {features.ai_strategies ? "✓" : "✗"} {T("upgrade.feature.ai", "AI-стратегии")}
              </div>
            </div>
            {isCurrent && (
              <div style={{ marginTop: 8, fontSize: 11, color: "var(--accent)", fontWeight: 600 }}>
                {T("upgrade.current", "Текущий тариф")}
              </div>
            )}
          </div>
        );
      })}

      <div style={{ padding: 12, background: "var(--bg-2)", borderRadius: 8, fontSize: 11, color: "var(--text-2)", marginTop: 8, textAlign: "center", lineHeight: 1.5 }}>
        {T("upgrade.comingSoon", "💳 Оплата подключим скоро. Сейчас тарифы выдаются вручную — напиши админу.")}
      </div>
    </BottomSheet>
  );
};

// ============================================================
// Forwards sheet — пересылка сигналов из source-каналов в свои
// ============================================================
const ForwardsSheet = ({ onClose, onToast }) => {
  const [items, setItems] = useState([]);
  const [channels, setChannels] = useState([]);
  const [source, setSource] = useState("");
  const [dest, setDest] = useState("");
  const [busy, setBusy] = useState(false);

  const load = useCallback(async () => {
    setBusy(true);
    const [fwds, chs] = await Promise.all([
      window.MobileAPI.listForwards(),
      window.MobileAPI.listChannels(),
    ]);
    setItems(fwds || []);
    setChannels(chs || []);
    setBusy(false);
  }, []);
  useEffect(() => { load(); }, [load]);

  const add = async () => {
    if (busy) return;
    if (!dest) {
      onToast({ msg: T("forwards.error.noDest", "Укажи канал-приёмник"), err: true });
      return;
    }
    setBusy(true);
    try {
      await window.MobileAPI.addForward(dest, source ? parseInt(source) : null);
      onToast({ msg: T("forwards.added", "Правило добавлено") });
      setSource(""); setDest("");
      await load();
    } catch (e) {
      onToast({ msg: T("common.error", "Ошибка:") + " " + (e.message || e).slice(0, 100), err: true });
    } finally { setBusy(false); }
  };

  const toggle = async (f) => {
    setBusy(true);
    try {
      await window.MobileAPI.updateForward(f.id, !f.enabled);
      await load();
    } catch (e) {
      onToast({ msg: T("common.error", "Ошибка:") + " " + (e.message || e).slice(0, 100), err: true });
    } finally { setBusy(false); }
  };

  const remove = async (f) => {
    if (!confirm(T("forwards.confirmDelete", "Удалить правило?"))) return;
    setBusy(true);
    try {
      await window.MobileAPI.deleteForward(f.id);
      onToast({ msg: T("forwards.removed", "Правило удалено") });
      await load();
    } catch (e) {
      onToast({ msg: T("common.error", "Ошибка:") + " " + (e.message || e).slice(0, 100), err: true });
    } finally { setBusy(false); }
  };

  return (
    <BottomSheet title={T("forwards.title", "Пересылка сигналов")} onClose={onClose}>
      <div style={{ padding: 12, background: "var(--bg-2)", borderRadius: 8, fontSize: 12, color: "var(--text-2)", marginBottom: 14, lineHeight: 1.5 }}>
        {T("forwards.hint", "Пересылка сигналов из source-канала в твой канал/чат. Можно настроить catch-all (любой source).")}
      </div>

      <div className="sheet-label">{T("forwards.sourceLabel", "Источник (опционально)")}</div>
      <select className="m-input" value={source} onChange={e => setSource(e.target.value)}>
        <option value="">{T("forwards.anySource", "Любой канал-источник")}</option>
        {channels.map(c => (
          <option key={c.id} value={c.id}>{c.title || c.channel_ref}</option>
        ))}
      </select>

      <div className="sheet-label">{T("forwards.destLabel", "Куда пересылать (@username или -100…)")}</div>
      <input className="m-input" value={dest} onChange={e => setDest(e.target.value)}
        placeholder={T("channel.refPlaceholder", "@username, username или -100…")}/>
      <button className="m-button primary lg block" style={{ marginTop: 12 }}
        disabled={busy} onClick={add}>
        <MIcon name="plus" size={16}/> {T("forwards.add", "Добавить правило")}
      </button>

      <div className="sheet-label" style={{ marginTop: 22 }}>{T("forwards.activeList", "Активные правила")} ({items.length})</div>
      {items.length === 0 && (
        <div style={{ padding: 14, color: "var(--text-3)", fontSize: 13, textAlign: "center" }}>
          {T("forwards.empty", "Пока нет правил")}
        </div>
      )}
      {items.map(f => (
        <div key={f.id} className="m-card" style={{ marginBottom: 8, padding: 10 }}>
          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <span className="mono" style={{ fontSize: 12, fontWeight: 600, flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>
              {f.dest_channel_ref}
            </span>
            <span className={"m-switch sm" + (f.enabled ? " on" : "")}
              onClick={() => toggle(f)}/>
            <button className="m-button danger sm" disabled={busy} onClick={() => remove(f)}>
              <MIcon name="x" size={12}/>
            </button>
          </div>
          {f.source_channel_id && (
            <div className="muted" style={{ fontSize: 10, marginTop: 4 }}>
              {T("forwards.fromSource", "Из источника #")}{f.source_channel_id}
            </div>
          )}
        </div>
      ))}
    </BottomSheet>
  );
};

// ============================================================
// Symbol search — autocomplete для биржевых инструментов
// ============================================================
const SymbolSearch = ({ exchange, kind = "all", value, onPick, placeholder }) => {
  const [query, setQuery] = useState(value || "");
  const [all, setAll] = useState([]);
  const [open, setOpen] = useState(false);
  const [loading, setLoading] = useState(false);
  const debounceRef = useRef(null);

  useEffect(() => { setQuery(value || ""); }, [value]);

  // Загружаем список один раз при фокусе
  const ensureLoaded = useCallback(async () => {
    if (all.length > 0) return;
    setLoading(true);
    const data = await window.MobileAPI.loadSymbols(exchange, kind);
    setAll(data || []);
    setLoading(false);
  }, [exchange, kind, all.length]);

  // USDT/USDC-quoted всегда приоритетнее обратных контрактов (USD/BTC, USD/ETH).
  const _quotePref = (q) => (q === "USDT" ? 0 : q === "USDC" ? 1 : 9);
  const _baseScore = (base, q) => base === q ? 0 : (base.startsWith(q) ? 1 : 2);

  const filtered = useMemo(() => {
    const q = (query || "").trim().toUpperCase();
    const sorter = (a, b) => {
      const aQ = (a.quote || "").toUpperCase();
      const bQ = (b.quote || "").toUpperCase();
      const aPref = _quotePref(aQ);
      const bPref = _quotePref(bQ);
      if (aPref !== bPref) return aPref - bPref;
      if (!q) return 0;
      const aBase = (a.base || "").toUpperCase();
      const bBase = (b.base || "").toUpperCase();
      return _baseScore(aBase, q) - _baseScore(bBase, q);
    };

    if (!q) {
      return [...all].sort(sorter).slice(0, 30);
    }
    return all
      .filter(s => {
        const base = (s.base || "").toUpperCase();
        const quote = (s.quote || "").toUpperCase();
        const sym = (s.symbol || "").toUpperCase();
        // Если base начинается с query — отлично (BTC -> BTCUSDT).
        // Если quote начинается с query — пускаем (USDT-quoted всё равно
        // отсортируется наверх).
        // Но если base == "USD" и quote совпадает с query (USDBTC при q="BTC") —
        // это обратный контракт, который перебивает всё, ставим в самый конец.
        return base === q || base.startsWith(q) || sym.includes(q) || quote.startsWith(q);
      })
      .sort(sorter)
      .slice(0, 30);
  }, [query, all]);

  const pick = (s) => {
    setQuery(s.symbol);
    setOpen(false);
    onPick && onPick(s);
  };

  return (
    <div style={{ position: "relative" }}>
      <input
        className="m-input"
        value={query}
        placeholder={placeholder || T("bots.symbol.placeholder", "напр. TONUSDT")}
        onFocus={() => { ensureLoaded(); setOpen(true); }}
        onChange={(e) => {
          const v = e.target.value.toUpperCase();
          setQuery(v);
          setOpen(true);
          // debounced search uses cached `all` array — мгновенно фильтрует
          if (debounceRef.current) clearTimeout(debounceRef.current);
          debounceRef.current = setTimeout(() => {}, 200);
        }}
        onBlur={() => setTimeout(() => setOpen(false), 150)}
        autoComplete="off" spellCheck={false}
      />
      {open && (
        <div style={{
          position: "absolute", top: "100%", left: 0, right: 0,
          marginTop: 4, background: "var(--bg-1)",
          border: "1px solid var(--border)", borderRadius: "var(--r-1)",
          maxHeight: 280, overflowY: "auto", zIndex: 50,
          boxShadow: "0 6px 20px rgba(0,0,0,0.35)",
        }}>
          {loading && (
            <div style={{ padding: 12, color: "var(--text-3)", fontSize: 12, textAlign: "center" }}>
              {T("common.loading", "Загрузка…")}
            </div>
          )}
          {!loading && filtered.length === 0 && (
            <div style={{ padding: 12, color: "var(--text-3)", fontSize: 12, textAlign: "center" }}>
              {T("symbols.notFound", "Не найдено")}
            </div>
          )}
          {!loading && filtered.map(s => (
            <button key={`${s.symbol}_${s.kind}`} className="m-set-row"
              onMouseDown={() => pick(s)}
              style={{
                width: "100%", padding: "8px 12px",
                background: "transparent", border: "none", textAlign: "left",
                borderBottom: "1px solid var(--border)",
              }}>
              <span style={{ display: "flex", alignItems: "center", gap: 8, width: "100%" }}>
                <Coin sym={s.base} size="sm"/>
                <span style={{ flex: 1, minWidth: 0 }}>
                  <span className="mono" style={{ fontWeight: 600, fontSize: 13 }}>{s.symbol}</span>
                  <span className="muted" style={{ fontSize: 10, marginLeft: 6 }}>{s.base}/{s.quote}</span>
                </span>
                <span className="m-pill" style={{
                  fontSize: 9, padding: "2px 6px",
                  background: s.kind === "spot" ? "var(--accent-dim)" : "var(--warn-dim)",
                  color: s.kind === "spot" ? "var(--accent)" : "var(--warn)",
                }}>
                  {s.kind === "spot" ? "SPOT" : "FUT"}
                </span>
              </span>
            </button>
          ))}
        </div>
      )}
    </div>
  );
};

// ============================================================
// Local Bots — создание + список (UI как у OKX, движок MVP)
// ============================================================
const LocalBotCreateSheet = ({ exchange, onClose, onToast, onCreated }) => {
  const [step, setStep] = useState(1);
  const [market, setMarket] = useState("futures");  // spot | futures
  const [type, setType] = useState("grid");          // grid | dca | tppl
  const [symbol, setSymbol] = useState("");
  const [picked, setPicked] = useState(null);
  const [direction, setDirection] = useState("long");
  const [invest, setInvest] = useState("100");
  const [lev, setLev] = useState("10");
  const [minPx, setMinPx] = useState("");
  const [maxPx, setMaxPx] = useState("");
  const [gridNum, setGridNum] = useState("50");
  const [dcaAmount, setDcaAmount] = useState("10");
  const [dcaIntervalH, setDcaIntervalH] = useState("24");
  // Smart DCA (Martingale) поля
  const [dcaInitialMargin, setDcaInitialMargin] = useState("10");
  const [dcaSafetyMargin, setDcaSafetyMargin] = useState("10");
  const [dcaPriceStep, setDcaPriceStep] = useState("0.5");
  const [dcaStepMult, setDcaStepMult] = useState("1.0");  // мульт расстояния
  const [dcaMarginMult, setDcaMarginMult] = useState("1.50");  // мульт маржи (и объёма)
  const [dcaMaxSafety, setDcaMaxSafety] = useState("8");
  const [dcaTpCycle, setDcaTpCycle] = useState("1.0");
  const [dcaSlGlobal, setDcaSlGlobal] = useState("");
  // Common TP/SL для Grid (опционально)
  const [gridTp, setGridTp] = useState("");
  const [gridSl, setGridSl] = useState("");
  // Recurring Basket: список { symbol, percent }
  const [basket, setBasket] = useState([
    { symbol: "BTCUSDT", percent: 50 },
    { symbol: "ETHUSDT", percent: 50 },
  ]);
  const [basketAmount, setBasketAmount] = useState("100");
  const [basketIntervalH, setBasketIntervalH] = useState("168");
  const [basketShowAdd, setBasketShowAdd] = useState(false);
  const [busy, setBusy] = useState(false);
  const [markPx, setMarkPx] = useState(0);

  // Тянем live mark price для DCA-summary
  useEffect(() => {
    if (!symbol) { setMarkPx(0); return; }
    let alive = true;
    (async () => {
      try {
        const px = await window.MobileAPI.loadMarkPrice(exchange, symbol);
        if (!alive) return;
        const parsed = parseFloat(px) || 0;
        setMarkPx(parsed);
        // Auto-fill grid range ±5% если поля пустые
        if (parsed > 0 && type === "grid" && !minPx && !maxPx) {
          setMinPx((parsed * 0.95).toPrecision(6));
          setMaxPx((parsed * 1.05).toPrecision(6));
        }
      } catch {}
    })();
    return () => { alive = false; };
  }, [symbol, exchange, type]);

  // SPOT не имеет плеча/short
  const isSpot = market === "spot";
  useEffect(() => {
    if (isSpot && direction !== "long") setDirection("long");
    if (isSpot) setLev("1");
  }, [isSpot]);

  const submit = async () => {
    if (busy) return;
    const config = { market };
    if (type === "grid") {
      const lo = parseFloat(minPx), hi = parseFloat(maxPx), n = parseInt(gridNum);
      if (!(lo > 0 && hi > lo && n >= 2 && n <= 500)) {
        onToast({ msg: T("bots.validation.grid", "min<max и 2≤сетка≤500"), err: true }); return;
      }
      // Block: mark должен быть внутри диапазона, иначе бот не сможет ставить ордера
      if (markPx > 0 && (markPx <= lo || markPx >= hi)) {
        onToast({
          msg: T("bots.validation.gridOutOfRange",
            "Текущая цена " + markPx.toPrecision(6) + " вне диапазона " + lo + ".." + hi + ". Используй пресет ±5%"),
          err: true,
        });
        return;
      }
      // Валидация на НОТИОНАЛ (margin × leverage), не на маржу.
      // WEEX min_notional = $5, ставим запас 1.5× = $7.5.
      const investNum = parseFloat(invest) || 0;
      const levNum = parseInt(lev) || 1;
      const perLevelMargin = investNum > 0 && n > 0 ? (investNum / n) : 0;
      const perLevelNotional = perLevelMargin * levNum;
      const MIN_NOTIONAL_PER_LEVEL = 7.5;  // 5 USDT × 1.5
      if (perLevelNotional > 0 && perLevelNotional < MIN_NOTIONAL_PER_LEVEL) {
        const minMargin = (MIN_NOTIONAL_PER_LEVEL * n / levNum).toFixed(2);
        onToast({
          msg: T("bots.validation.gridTooSmall",
            "Маловато: " + perLevelNotional.toFixed(2) + " USDT нотионала на уровень × " + n + " ордеров (плечо " + levNum + "x). " +
            "Минимум $" + MIN_NOTIONAL_PER_LEVEL + " нотионала/уровень. Подними плечо или маржу до $" + minMargin),
          err: true,
        });
        return;
      }
      config.min_price = lo; config.max_price = hi; config.grid_num = n;
    }
    if (type === "dca") {
      // Smart-Martingale DCA (OKX-style)
      const im = parseFloat(dcaInitialMargin);
      const sm = parseFloat(dcaSafetyMargin);
      const ps = parseFloat(dcaPriceStep);
      const stepMult = parseFloat(dcaStepMult);
      const mm = parseFloat(dcaMarginMult);
      const ms = parseInt(dcaMaxSafety);
      const tp = parseFloat(dcaTpCycle);
      if (!(im > 0 && sm > 0 && ps > 0 && tp > 0 && ms >= 1 && ms <= 50)) {
        onToast({ msg: T("bots.validation.dcaSmart", "Заполни все поля корректно"), err: true });
        return;
      }
      config.initial_margin = im;
      config.safety_margin = sm;
      config.price_step_pct = ps;
      config.price_step_multiplier = stepMult || 1.0;  // НОВЫЙ — мульт расстояния
      config.margin_multiplier = mm || 1.50;            // ОДИН мульт маржи/объёма
      config.max_safety_orders = ms;
      config.tp_per_cycle_pct = tp;
      config.sl_pct = parseFloat(dcaSlGlobal) || 0;
      config.direction = isSpot ? "long" : direction;
    }
    if (type === "tppl") {
      // Бот «TP/SL» убран в пользу встроенных полей в Grid/DCA, но мы не ломаем
      // старые записи: если кто-то выбрал — просто переадресуем как мини-grid с 2 уровнями.
      onToast({ msg: T("bots.tpplDeprecated", "Тип TP/SL встроен в Grid и DCA — выбери один из них"), err: true });
      return;
    }
    // Опциональный TP/SL для Grid
    if (type === "grid") {
      if (parseFloat(gridTp) > 0) config.tp_price = parseFloat(gridTp);
      if (parseFloat(gridSl) > 0) config.sl_price = parseFloat(gridSl);
    }
    // Recurring Basket
    if (type === "recurring") {
      const totalPct = basket.reduce((s, b) => s + (parseFloat(b.percent) || 0), 0);
      if (basket.length === 0) {
        onToast({ msg: T("bots.recurring.empty", "Добавь хотя бы один токен"), err: true });
        return;
      }
      if (Math.abs(totalPct - 100) > 0.01) {
        onToast({ msg: T("bots.recurring.sum100", `Сумма % должна = 100 (сейчас ${totalPct.toFixed(1)})`), err: true });
        return;
      }
      const amt = parseFloat(basketAmount);
      const iv = parseInt(basketIntervalH);
      if (!(amt > 0 && iv >= 1)) {
        onToast({ msg: T("bots.recurring.invalid", "Сумма>0 и интервал≥1ч"), err: true });
        return;
      }
      config.basket = basket
        .filter(b => b.symbol && b.percent > 0)
        .map(b => ({ symbol: b.symbol.toUpperCase(), percent: parseFloat(b.percent) }));
      config.amount_per_purchase = amt;
      config.interval_hours = iv;
      config.market = "spot";
    }
    // Для recurring symbol — плейсхолдер "BASKET", реальные символы внутри config.basket
    const sym = type === "recurring"
      ? "BASKET"
      : (symbol || "").trim().toUpperCase();
    if (type !== "recurring" && !/^[A-Z0-9]{2,15}USDT$/.test(sym)) {
      onToast({ msg: T("bots.symbol.format", "Формат: BTCUSDT, TONUSDT, SOLUSDT…"), err: true });
      return;
    }
    setBusy(true);
    try {
      const finalInvest = type === "recurring"
        ? (parseFloat(basketAmount) || 100)
        : (parseFloat(invest) || 100);
      const basePayload = {
        exchange: (exchange || "okx").toLowerCase(),
        symbol: sym,
        bot_type: type,
        direction: (type === "dca" || type === "recurring" || isSpot) ? "long" : direction,
        investment_usdt: finalInvest,
        leverage: (isSpot || type === "recurring") ? 1 : (parseInt(lev) || 1),
        config,
      };
      // Conflict guard: проверяем не дублируется ли бот, не WEEX+spot и т.п.
      const check = await window.MobileAPI.checkLocalBotConflict(basePayload);
      const conf = check && check.conflict;
      let acknowledge = false;
      if (conf) {
        if (conf.severity === "block") {
          onToast({ msg: conf.message || "Невозможно создать бот", err: true });
          setBusy(false);
          return;
        }
        if (conf.severity === "warn") {
          if (!window.confirm(conf.message + "\n\nВсё равно создать?")) {
            setBusy(false);
            return;
          }
          acknowledge = true;
        }
        if (conf.severity === "info") {
          onToast({ msg: conf.message });
        }
      }
      await window.MobileAPI.createLocalBot({
        ...basePayload, acknowledge_conflict: acknowledge,
      });
      onToast({ msg: T("bots.createdHint", "Бот создан · нажми ▶ чтобы запустить") });
      onCreated && onCreated();
      onClose();
    } catch (e) {
      onToast({ msg: T("common.error", "Ошибка:") + " " + (e.message || e).slice(0, 100), err: true });
    } finally { setBusy(false); }
  };

  const foot = (
    <>
      {step > 1 && <button className="m-button ghost lg" disabled={busy} onClick={() => setStep(s => s - 1)}>{T("common.back", "Назад")}</button>}
      {step < 2 ? (
        <button className="m-button primary lg" onClick={() => setStep(s => s + 1)}>{T("common.next", "Далее")}</button>
      ) : (
        <button className="m-button primary lg" disabled={busy} onClick={submit}>
          {busy ? "..." : T("bots.create", "Создать бота")}
        </button>
      )}
    </>
  );

  return (
    <BottomSheet title={T("bots.createLocal", "Создать локального бота")} onClose={onClose} foot={foot}>
      {step === 1 && (
        <>
          <div className="sheet-label">{T("bots.market", "Рынок")}</div>
          <div className="m-segmented">
            <button className={market === "futures" ? "active" : ""}
              onClick={() => { buzz(); setMarket("futures"); }}>{T("mode.futures", "Фьючерсы")}</button>
            <button className={market === "spot" ? "active" : ""}
              onClick={() => {
                buzz();
                // На биржах без spot API (WEEX) предлагаем fallback на futures 1x
                const noSpotApi = (exchange || "").toLowerCase() === "weex";
                if (noSpotApi) {
                  const ok = confirm(T("bots.spot.fallbackConfirm",
                    "На WEEX нет API для спот-ботов. Открыть как фьючерсный с плечом 1x? " +
                    "Это эквивалент спота по риску (нет ликвидации при цене > 0), но через фьюч-кошелёк."));
                  if (ok) {
                    setMarket("futures");
                    setLev("1");
                    onToast({ msg: T("bots.spot.fallbackApplied",
                      "Будет открыт как futures 1x (эквивалент спота)") });
                  }
                  return;
                }
                setMarket("spot");
              }}>{T("mode.spot", "Спот")}</button>
          </div>

          <div className="sheet-label">{T("bots.type", "Тип бота")}</div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 8 }}>
            {[
              { k: "grid", label: "Grid", desc: T("bots.type.gridTPSL", "Сетка с TP/SL") },
              { k: "dca", label: "Smart-DCA", desc: T("bots.type.dcaSmart", "Мартингейл") },
              { k: "recurring", label: "Basket", desc: T("bots.type.recurring", "Корзина токенов") },
            ].map(t => (
              <button key={t.k}
                className={"m-card" + (type === t.k ? " active-card" : "")}
                onClick={() => { buzz(); setType(t.k); }}
                style={{
                  padding: 10, textAlign: "left",
                  border: type === t.k ? "1px solid var(--accent)" : "1px solid var(--border)",
                  background: type === t.k ? "var(--accent-dim)" : "var(--bg-2)",
                }}>
                <div style={{ fontWeight: 700, marginBottom: 2, fontSize: 13 }}>{t.label}</div>
                <div className="muted" style={{ fontSize: 10 }}>{t.desc}</div>
              </button>
            ))}
          </div>

          {type !== "recurring" && <>
            <div className="sheet-label">{T("bots.symbol", "Символ")}</div>
            <SymbolSearch exchange={exchange}
              kind={market === "spot" ? "spot" : "futures"}
              value={symbol}
              placeholder={T("bots.symbol.placeholder", "напр. TONUSDT")}
              onPick={(s) => { setSymbol(s.symbol); setPicked(s); }}/>
          </>}

          {type === "grid" && !isSpot && (
            <>
              <div className="sheet-label">{T("bots.side", "Направление")}</div>
              <div className="m-segmented">
                <button className={direction === "long" ? "active" : ""} onClick={() => { buzz(); setDirection("long"); }}>Long</button>
                <button className={direction === "short" ? "active" : ""} onClick={() => { buzz(); setDirection("short"); }}>Short</button>
                <button className={direction === "neutral" ? "active" : ""} onClick={() => { buzz(); setDirection("neutral"); }}>Neutral</button>
              </div>
            </>
          )}

          {type !== "recurring" && <>
            <div className="sheet-label">{T("bots.investment", "Инвестиции (USDT)")}</div>
            <input className="m-input" inputMode="decimal" value={invest}
              onChange={e => setInvest(e.target.value)}/>
          </>}

          {!isSpot && type !== "recurring" && (
            <>
              <div className="sheet-label">{T("lev.title", "Плечо")}</div>
              <input className="m-input" inputMode="numeric" value={lev}
                onChange={e => setLev(e.target.value.replace(/\D/g, ""))}/>
              <div className="pct-grid" style={{ marginTop: 4 }}>
                {[1, 3, 5, 10, 20, 50].map(n => (
                  <button key={n} className={"pct-btn" + (parseInt(lev) === n ? " active" : "")}
                    style={{ fontSize: 11, padding: "5px 0" }}
                    onClick={() => { buzz(); setLev(String(n)); }}>{n}x</button>
                ))}
              </div>
            </>
          )}
        </>
      )}

      {step === 2 && type === "grid" && (() => {
        const lo = parseFloat(minPx) || 0;
        const hi = parseFloat(maxPx) || 0;
        const n = parseInt(gridNum) || 0;
        const px = markPx || 0;
        const investNum = parseFloat(invest) || 0;
        const levNum = parseInt(lev) || 1;
        const isLong = (direction || "long").toLowerCase() === "long";
        // Per-level расчёты
        const perLevelMargin = n > 0 ? investNum / n : 0;
        const perLevelNotional = perLevelMargin * levNum;
        // Initial bulk-buy: для LONG это уровни ВЫШЕ mark (нотионалом)
        let bulkLevels = 0;
        if (lo > 0 && hi > lo && n > 1 && px > 0 && lo <= px && px <= hi) {
          const step = (hi - lo) / n;
          for (let i = 0; i < n; i++) {
            const p = lo + i * step;
            if (isLong ? p > px : p < px) bulkLevels++;
          }
        }
        const bulkNotional = bulkLevels * perLevelNotional;
        const bulkMargin = bulkLevels * perLevelMargin;
        const totalNotional = n * perLevelNotional;
        // Биржевые комиссии maker (для лимиток). WEEX ≈ 0.02%, OKX 0.02%, Binance 0.02%, Bybit 0.02%.
        const MAKER_FEE = 0.0002;
        // Step% — у нас arithmetic grid, поэтому относительно текущей mark.
        const stepPctOfMark = px > 0 && stepAbs > 0 ? (stepAbs / px * 100) : 0;
        // Прибыль за цикл = step% × notional/level − 2×maker_fee × notional/level
        // (комиссия и на open и на close).
        const grossPerCycle = perLevelNotional * (stepPctOfMark / 100);
        const feesPerCycle = perLevelNotional * MAKER_FEE * 2;
        const netPerCycle = grossPerCycle - feesPerCycle;
        // Минимальный прибыльный шаг = 2 × maker_fee × 1.5 (запас)
        const minProfitableStepPct = MAKER_FEE * 100 * 2 * 1.5;
        const stepTooSmall = stepPctOfMark > 0 && stepPctOfMark < minProfitableStepPct;
        // Worst-case loss: цена ушла к нижней границе (для long).
        // Максимальная позиция = total_notional / avg(lo, mark)
        // Просадка = qty × (lo - mark) (или mark - hi для short)
        let worstCaseLoss = 0;
        let liqPrice = 0;
        if (totalNotional > 0 && px > 0 && lo > 0 && hi > 0) {
          // Avg accumulation price ≈ (lo + mark) / 2 для long
          const avgAccum = isLong ? (lo + px) / 2 : (hi + px) / 2;
          const maxPosCoins = totalNotional / avgAccum;
          worstCaseLoss = isLong
            ? maxPosCoins * Math.max(0, avgAccum - lo)
            : maxPosCoins * Math.max(0, hi - avgAccum);
          // Liq price (приближение): avg × (1 − 1/lev + maint), maint=0.005
          const maint = 0.005;
          liqPrice = isLong
            ? avgAccum * (1 - (1 / levNum - maint))
            : avgAccum * (1 + (1 / levNum - maint));
        }
        // Безопасность кредитного плеча для Grid: рекомендуется 1-3x
        const highLeverage = levNum > 5;
        const lossExceedsInvestment = worstCaseLoss > investNum;
        // Валидация диапазона: mark должен быть строго внутри
        const markOutOfRange = px > 0 && lo > 0 && hi > 0 && (px <= lo || px >= hi);
        const stepAbs = (hi > lo && n > 0) ? (hi - lo) / n : 0;
        const stepPct = px > 0 && stepAbs > 0 ? (stepAbs / px * 100) : 0;
        // mark±5% пресет (для авто-заполнения)
        const presetLo = px > 0 ? (px * 0.95).toPrecision(6) : "";
        const presetHi = px > 0 ? (px * 1.05).toPrecision(6) : "";
        return (
        <>
          {px > 0 && (
            <div className="m-card" style={{
              padding: 10, marginBottom: 8, background: "var(--bg-2)",
              border: "1px solid var(--border)", borderRadius: 10, fontSize: 12,
            }}>
              <div style={{ display: "flex", justifyContent: "space-between" }}>
                <span style={{ color: "var(--text-2)" }}>{T("bots.grid.mark", "Текущая цена")}</span>
                <span className="mono" style={{ fontWeight: 700 }}>{px.toPrecision(6)}</span>
              </div>
              <button className="m-button ghost sm block" style={{ marginTop: 6 }}
                onClick={() => { buzz(); setMinPx(presetLo); setMaxPx(presetHi); }}>
                {T("bots.grid.preset", "Заполнить ±5% от цены")}
              </button>
            </div>
          )}
          {/* Live calc summary — критично знать размер initial bulk-buy ДО старта */}
          {investNum > 0 && n > 0 && (
            <div className="m-card" style={{
              padding: 12, marginBottom: 10, background: "var(--bg-2)",
              border: "1px solid var(--border)", borderRadius: 10,
            }}>
              <div style={{ fontSize: 11, color: "var(--text-2)", marginBottom: 6 }}>
                {T("bots.grid.summary", "Расчёт (live)")}
              </div>
              <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 6, fontSize: 12 }}>
                <div style={{ color: "var(--text-2)" }}>{T("bots.grid.marginPerLevel", "Маржа/уровень")}</div>
                <div className="mono" style={{ textAlign: "right" }}>{perLevelMargin.toFixed(2)} USDT</div>
                <div style={{ color: "var(--text-2)" }}>{T("bots.grid.notionalPerLevel", "Нотионал/уровень")}</div>
                <div className="mono" style={{ textAlign: "right", fontWeight: 700,
                    color: perLevelNotional < 7.5 ? "var(--short)" : undefined }}>
                  {perLevelNotional.toFixed(2)} USDT {perLevelNotional < 7.5 ? "⚠" : ""}
                </div>
                <div style={{ color: "var(--text-2)" }}>{T("bots.grid.totalNotional", "Всего нотионал")}</div>
                <div className="mono" style={{ textAlign: "right" }}>{totalNotional.toFixed(2)} USDT</div>
                {bulkLevels > 0 && (
                  <>
                    <div style={{ color: "var(--short)" }}>
                      {T("bots.grid.bulkBuyAtStart", "Initial bulk-buy (сразу)")}
                    </div>
                    <div className="mono" style={{ textAlign: "right", color: "var(--short)", fontWeight: 700 }}>
                      {bulkNotional.toFixed(0)} USDT
                      <div style={{ fontSize: 10, fontWeight: 400 }}>
                        ({bulkLevels} {T("bots.grid.levels", "ур.")} × маржа {bulkMargin.toFixed(0)})
                      </div>
                    </div>
                  </>
                )}
                {netPerCycle > 0 && (
                  <>
                    <div style={{ color: "var(--text-2)" }}>{T("bots.grid.netPerCycle", "Прибыль/цикл (after fees)")}</div>
                    <div className="mono pos" style={{ textAlign: "right" }}>
                      ≈ {netPerCycle.toFixed(3)} USDT
                      <div style={{ fontSize: 10, fontWeight: 400 }}>
                        ({stepPctOfMark.toFixed(3)}% шаг − {(MAKER_FEE * 100 * 2).toFixed(2)}% fees)
                      </div>
                    </div>
                  </>
                )}
                {stepTooSmall && (
                  <div style={{ gridColumn: "1 / -1", marginTop: 4, padding: 6,
                    background: "var(--warn-dim)", borderRadius: 6, fontSize: 11,
                    color: "var(--warn)", lineHeight: 1.4 }}>
                    ⚠ {T("bots.grid.stepUnprofitable",
                      "Шаг " + stepPctOfMark.toFixed(3) + "% слишком мал — комиссии " +
                      (MAKER_FEE * 100 * 2).toFixed(2) + "% за пару поглотят прибыль. " +
                      "Уменьшите grid_num или расширьте диапазон (нужно ≥" + minProfitableStepPct.toFixed(2) + "%/шаг).")}
                  </div>
                )}
                {worstCaseLoss > 0 && (
                  <>
                    <div style={{ color: "var(--text-2)", marginTop: 4 }}>
                      {T("bots.grid.worstLoss", "Макс. убыток (выход из диапазона)")}
                    </div>
                    <div className="mono neg" style={{ textAlign: "right", marginTop: 4, fontWeight: 700 }}>
                      −{worstCaseLoss.toFixed(0)} USDT
                      <div style={{ fontSize: 10, fontWeight: 400 }}>
                        ({(worstCaseLoss / Math.max(investNum, 1) * 100).toFixed(0)}% от инвестиций)
                      </div>
                    </div>
                  </>
                )}
                {liqPrice > 0 && (
                  <>
                    <div style={{ color: "var(--text-2)" }}>
                      {T("bots.grid.liqPrice", "Цена ликвидации (~)")}
                    </div>
                    <div className="mono" style={{ textAlign: "right", color: "var(--short)" }}>
                      {liqPrice.toPrecision(6)}
                    </div>
                  </>
                )}
                {(highLeverage || lossExceedsInvestment) && (
                  <div style={{ gridColumn: "1 / -1", marginTop: 6, padding: 8,
                    background: "var(--short-bg)", border: "1px solid var(--short-dim)",
                    borderRadius: 6, fontSize: 11, color: "var(--short)", lineHeight: 1.4 }}>
                    ⚠ {highLeverage
                      ? "Плечо " + levNum + "x высокое для Grid — на OKX рекомендуют 1-3x. " +
                        "При выходе цены из диапазона убыток в " + (worstCaseLoss / Math.max(investNum, 1)).toFixed(1) +
                        "× превысит инвестиции."
                      : "Макс. убыток " + worstCaseLoss.toFixed(0) + " USDT > инвестиций " + investNum.toFixed(0) +
                        " USDT. Снизьте плечо или увеличьте инвестиции."}
                  </div>
                )}
              </div>
              {bulkNotional > 0 && (
                <div style={{ marginTop: 8, fontSize: 10, color: "var(--text-3)", lineHeight: 1.4 }}>
                  {T("bots.grid.bulkBuyHint",
                    "💡 При старте бот сразу купит позицию на " + bulkNotional.toFixed(0) + " USDT нотионала. " +
                    "Это inventory для верхних SELL-уровней. Колебание цены ±1% = ±" +
                    (bulkNotional * 0.01).toFixed(2) + " USDT просадка.")}
                </div>
              )}
            </div>
          )}
          <div className="sheet-label">{T("bots.grid.range", "Диапазон цен сетки")}</div>
          <div style={{ display: "flex", gap: 8 }}>
            <input className="m-input" inputMode="decimal" value={minPx}
              onChange={e => setMinPx(e.target.value)} placeholder={T("bots.grid.min", "Мин цена")} style={{ flex: 1 }}/>
            <input className="m-input" inputMode="decimal" value={maxPx}
              onChange={e => setMaxPx(e.target.value)} placeholder={T("bots.grid.max", "Макс цена")} style={{ flex: 1 }}/>
          </div>
          {markOutOfRange && (
            <div style={{
              marginTop: 6, padding: 8, borderRadius: 8,
              background: "var(--warn-dim)", color: "var(--warn)",
              fontSize: 11, lineHeight: 1.4,
            }}>
              ⚠ {T("bots.grid.outOfRange",
                "Текущая цена {px} вне диапазона {lo}..{hi}. Бот не сможет ставить ордера.")
                .replace("{px}", px.toPrecision(6))
                .replace("{lo}", lo).replace("{hi}", hi)}
            </div>
          )}

          <div className="sheet-label">{T("bots.grid.count", "Число ордеров в сетке")}</div>
          <input className="m-input" inputMode="numeric" value={gridNum}
            onChange={e => setGridNum(e.target.value.replace(/\D/g, ""))}/>
          <div className="pct-grid" style={{ marginTop: 6 }}>
            {[20, 50, 100, 200].map(nv => (
              <button key={nv} className={"pct-btn" + (parseInt(gridNum) === nv ? " active" : "")}
                onClick={() => { buzz(); setGridNum(String(nv)); }}>{nv}</button>
            ))}
          </div>
          {stepAbs > 0 && (
            <div style={{ marginTop: 4, fontSize: 11, color: "var(--text-2)" }}>
              {T("bots.grid.step", "Шаг сетки")}: <span className="mono">{stepAbs.toPrecision(4)}</span>
              {" "}({stepPct.toFixed(3)}%)
            </div>
          )}

          <div style={{ marginTop: 14, padding: 12, background: "var(--bg-2)", borderRadius: 8, fontSize: 12, color: "var(--text-2)", lineHeight: 1.5 }}>
            {T("bots.grid.hint", "💡 Шаг сетки = (макс − мин) / число ордеров.")}
          </div>

          {/* Опциональные TP/SL для всей позиции сетки */}
          <div className="sheet-label" style={{ marginTop: 10 }}>{T("bots.grid.tpslOptional", "Take-Profit / Stop-Loss (опционально)")}</div>
          <div style={{ display: "flex", gap: 8 }}>
            <input className="m-input" inputMode="decimal" value={gridTp}
              onChange={e => setGridTp(e.target.value)}
              placeholder={T("bots.tpPrice", "TP цена")} style={{ flex: 1 }}/>
            <input className="m-input" inputMode="decimal" value={gridSl}
              onChange={e => setGridSl(e.target.value)}
              placeholder={T("bots.slPrice", "SL цена")} style={{ flex: 1 }}/>
          </div>
          <div style={{ fontSize: 10, color: "var(--text-3)", marginTop: 4 }}>
            {T("bots.grid.tpslHint", "Если задано — бот закроет всю позицию когда mark достигнет уровня.")}
          </div>
        </>
        );
      })()}

      {step === 2 && type === "dca" && (() => {
        // OKX-style live summary: считаем нужную маржу, нотионал, avg, ликвидацию
        const im = parseFloat(dcaInitialMargin) || 0;
        const sm = parseFloat(dcaSafetyMargin) || 0;
        const ps = parseFloat(dcaPriceStep) || 0;
        const stepMult = parseFloat(dcaStepMult) || 1.0;
        const mm = parseFloat(dcaMarginMult) || 1.5;
        const ms = Math.max(0, Math.min(50, parseInt(dcaMaxSafety) || 0));
        const levN = parseInt(lev) || 1;
        const isLong = direction === "long";
        // markPx из state выше (live fetch)
        // Геом. прогрессия: total = im + sm*(mm^N - 1)/(mm - 1)
        let total_margin = im;
        let total_qty = markPx > 0 ? (im * levN / markPx) : 0;
        let avg_entry_num = markPx * total_qty;
        let last_entry = markPx;
        let cum_pct_offset = 0;
        for (let n = 1; n <= ms; n++) {
          const step_pct_n = ps * Math.pow(stepMult, n - 1);
          cum_pct_offset += step_pct_n;
          const entry_n = isLong
            ? markPx * (1 - cum_pct_offset / 100)
            : markPx * (1 + cum_pct_offset / 100);
          const margin_n = sm * Math.pow(mm, n - 1);
          const qty_n = entry_n > 0 ? (margin_n * levN / entry_n) : 0;
          total_margin += margin_n;
          total_qty += qty_n;
          avg_entry_num += entry_n * qty_n;
          last_entry = entry_n;
        }
        const avg_entry = total_qty > 0 ? avg_entry_num / total_qty : markPx;
        const total_notional = total_margin * levN;
        // Liq price (приближение для isolated USDT-perp): maint_margin≈0.005
        const maint = 0.005;
        const liq_price = isLong
          ? avg_entry * (1 - (1 / levN - maint))
          : avg_entry * (1 + (1 / levN - maint));
        const max_dd_pct = markPx > 0 ? Math.abs((last_entry - markPx) / markPx * 100) : 0;
        return (
        <>
          {/* OKX-style live summary вверху */}
          <div className="m-card" style={{
            padding: 12, marginBottom: 10,
            background: "var(--bg-2)", border: "1px solid var(--border)",
            borderRadius: 10,
          }}>
            <div style={{ fontSize: 11, color: "var(--text-2)", marginBottom: 6 }}>
              {T("bots.dca.summary", "Расчёт (live)")}
            </div>
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 6, fontSize: 12 }}>
              <div style={{ color: "var(--text-2)" }}>{T("bots.dca.totalMargin", "Нужно маржи")}</div>
              <div className="mono" style={{ textAlign: "right", fontWeight: 700 }}>
                {total_margin.toFixed(2)} USDT
              </div>
              <div style={{ color: "var(--text-2)" }}>{T("bots.dca.totalNotional", "Нотионал")}</div>
              <div className="mono" style={{ textAlign: "right" }}>
                {total_notional.toFixed(2)} USDT
              </div>
              <div style={{ color: "var(--text-2)" }}>{T("bots.dca.avgEntry", "Средняя вход")}</div>
              <div className="mono" style={{ textAlign: "right" }}>
                {avg_entry > 0 ? avg_entry.toPrecision(6) : "—"}
              </div>
              <div style={{ color: "var(--text-2)" }}>{T("bots.dca.liqPrice", "Цена ликвидации (~)")}</div>
              <div className="mono" style={{ textAlign: "right", color: "var(--short)" }}>
                {liq_price > 0 ? liq_price.toPrecision(6) : "—"}
              </div>
              <div style={{ color: "var(--text-2)" }}>{T("bots.dca.maxDD", "Просадка к посл.")}</div>
              <div className="mono" style={{ textAlign: "right" }}>
                {max_dd_pct.toFixed(2)}%
              </div>
            </div>
          </div>

          <div style={{ padding: 10, background: "var(--bg-2)", borderRadius: 8, fontSize: 11, color: "var(--text-2)", lineHeight: 1.5, marginBottom: 10 }}>
            {T("bots.dca.smartHint", "Smart-Martingale: бот открывает позицию, при движении цены против ставит страховочные ордера с растущей маржой. При достижении TP за цикл — закрывает всё и стартует новый цикл.")}
          </div>

          <div style={{ display: "flex", gap: 8 }}>
            <div style={{ flex: 1 }}>
              <div className="sheet-label">{T("bots.dca.initialMargin", "Маржа начального")}</div>
              <input className="m-input" inputMode="decimal" value={dcaInitialMargin}
                onChange={e => setDcaInitialMargin(e.target.value)} placeholder="USDT"/>
            </div>
            <div style={{ flex: 1 }}>
              <div className="sheet-label">{T("bots.dca.safetyMargin", "Маржа страховочного")}</div>
              <input className="m-input" inputMode="decimal" value={dcaSafetyMargin}
                onChange={e => setDcaSafetyMargin(e.target.value)} placeholder="USDT"/>
            </div>
          </div>

          <div className="sheet-label">{T("bots.dca.priceStep", "Шаг цены между ордерами, %")}</div>
          <input className="m-input" inputMode="decimal" value={dcaPriceStep}
            onChange={e => setDcaPriceStep(e.target.value)} placeholder="0.5"/>
          <div className="pct-grid" style={{ marginTop: 4 }}>
            {["0.3", "0.5", "1.0", "2.0"].map(v => (
              <button key={v} className={"pct-btn" + (dcaPriceStep === v ? " active" : "")}
                onClick={() => { buzz(); setDcaPriceStep(v); }}>{v}%</button>
            ))}
          </div>

          <div style={{ display: "flex", gap: 8 }}>
            <div style={{ flex: 1 }}>
              <div className="sheet-label">{T("bots.dca.stepMult", "Шаг цены ×")}</div>
              <input className="m-input" inputMode="decimal" value={dcaStepMult}
                onChange={e => setDcaStepMult(e.target.value)} placeholder="1.0"/>
              <div className="muted" style={{ fontSize: 10, marginTop: 2 }}>
                {T("bots.dca.stepMultHint", ">1 = сетка расширяется")}
              </div>
            </div>
            <div style={{ flex: 1 }}>
              <div className="sheet-label">{T("bots.dca.marginMult", "Маржа ×")}</div>
              <input className="m-input" inputMode="decimal" value={dcaMarginMult}
                onChange={e => setDcaMarginMult(e.target.value)} placeholder="1.50"/>
              <div className="muted" style={{ fontSize: 10, marginTop: 2 }}>
                {T("bots.dca.marginMultHint", "размер растёт каждый шаг")}
              </div>
            </div>
          </div>

          <div className="sheet-label">{T("bots.dca.maxSafety", "Макс. страховочных ордеров")}</div>
          <input className="m-input" inputMode="numeric" value={dcaMaxSafety}
            onChange={e => setDcaMaxSafety(e.target.value.replace(/\D/g, ""))}/>
          <div className="pct-grid" style={{ marginTop: 4 }}>
            {[4, 6, 8, 12].map(n => (
              <button key={n} className={"pct-btn" + (parseInt(dcaMaxSafety) === n ? " active" : "")}
                onClick={() => { buzz(); setDcaMaxSafety(String(n)); }}>{n}</button>
            ))}
          </div>

          <div style={{ display: "flex", gap: 8 }}>
            <div style={{ flex: 1 }}>
              <div className="sheet-label">{T("bots.dca.tpCycle", "TP за цикл, %")}</div>
              <input className="m-input" inputMode="decimal" value={dcaTpCycle}
                onChange={e => setDcaTpCycle(e.target.value)} placeholder="1.0"/>
            </div>
            <div style={{ flex: 1 }}>
              <div className="sheet-label">{T("bots.dca.slGlobal", "Stop-loss, % (опц.)")}</div>
              <input className="m-input" inputMode="decimal" value={dcaSlGlobal}
                onChange={e => setDcaSlGlobal(e.target.value)} placeholder="—"/>
            </div>
          </div>
        </>
        );
      })()}

      {step === 2 && type === "recurring" && (
        <>
          <div style={{ padding: 10, background: "var(--bg-2)", borderRadius: 8, fontSize: 11, color: "var(--text-2)", lineHeight: 1.5, marginBottom: 10 }}>
            {T("bots.recurring.hint", "Бот периодически закупает корзину токенов на сумму X USDT, распределяя по %. Идеально для пассивного DCA.")}
          </div>

          <div className="sheet-label" style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
            <span>{T("bots.recurring.basket", "Корзина токенов")} ({basket.length})</span>
            <button className="m-button sm" onClick={() => { buzz(); setBasketShowAdd(true); }}>
              <MIcon name="plus" size={12}/> {T("common.add", "Добавить")}
            </button>
          </div>

          {basketShowAdd && (
            <div className="m-card" style={{ padding: 10, marginBottom: 8 }}>
              <SymbolSearch exchange={exchange} kind="spot"
                placeholder={T("bots.symbol.placeholder", "напр. TONUSDT")}
                onPick={(s) => {
                  setBasket(prev => [...prev, { symbol: s.symbol, percent: 0 }]);
                  setBasketShowAdd(false);
                }}/>
              <button className="m-button ghost sm block" style={{ marginTop: 6 }}
                onClick={() => setBasketShowAdd(false)}>
                {T("common.cancel", "Отмена")}
              </button>
            </div>
          )}

          {basket.map((b, i) => (
            <div key={i} className="m-card" style={{ padding: 10, marginBottom: 6, display: "flex", alignItems: "center", gap: 8 }}>
              <Coin sym={(b.symbol || "").replace(/USDT$/, "")} size="sm"/>
              <span className="mono" style={{ fontWeight: 600, flex: 1 }}>{b.symbol}</span>
              <input className="m-input" inputMode="decimal" value={b.percent}
                style={{ width: 80, textAlign: "right" }}
                onChange={e => {
                  const v = parseFloat(e.target.value) || 0;
                  setBasket(prev => prev.map((x, j) => j === i ? { ...x, percent: v } : x));
                }}/>
              <span className="muted" style={{ fontSize: 12 }}>%</span>
              <button className="m-button danger sm"
                onClick={() => setBasket(prev => prev.filter((_, j) => j !== i))}>
                <MIcon name="x" size={12}/>
              </button>
            </div>
          ))}

          <div style={{
            padding: 8, marginTop: 6, marginBottom: 14,
            background: Math.abs(basket.reduce((s, b) => s + (parseFloat(b.percent) || 0), 0) - 100) < 0.01
              ? "var(--long-bg)" : "var(--warn-dim)",
            borderRadius: 6, textAlign: "center", fontSize: 12,
          }}>
            {T("bots.recurring.totalPct", "Сумма")}:&nbsp;
            <span className="mono">{basket.reduce((s, b) => s + (parseFloat(b.percent) || 0), 0).toFixed(1)}% / 100%</span>
          </div>

          <div className="sheet-label">{T("bots.recurring.amount", "Сумма за цикл (USDT)")}</div>
          <input className="m-input" inputMode="decimal" value={basketAmount}
            onChange={e => setBasketAmount(e.target.value)}/>

          <div className="sheet-label">{T("bots.dca.interval", "Интервал (часы)")}</div>
          <input className="m-input" inputMode="numeric" value={basketIntervalH}
            onChange={e => setBasketIntervalH(e.target.value.replace(/\D/g, ""))}/>
          <div className="pct-grid" style={{ marginTop: 6 }}>
            {[24, 72, 168, 720].map(h => (
              <button key={h} className={"pct-btn" + (parseInt(basketIntervalH) === h ? " active" : "")}
                onClick={() => { buzz(); setBasketIntervalH(String(h)); }}>
                {h === 24 ? T("bots.interval.1d", "1д")
                  : h === 72 ? "3д"
                  : h === 168 ? T("bots.interval.1w", "1нед")
                  : T("bots.interval.1mo", "1мес")}
              </button>
            ))}
          </div>
        </>
      )}

      {step === 2 && type === "tppl" && (
        <>
          <div className="sheet-label">{T("bots.tppl.entry", "Цена входа (опционально, иначе market)")}</div>
          <input className="m-input" inputMode="decimal" value={tppl_entry}
            onChange={e => setTpplEntry(e.target.value)} placeholder={T("bots.tppl.market", "пусто = по рынку")}/>
          <div className="sheet-label">{T("bots.tpPrice", "TP цена")}</div>
          <input className="m-input" inputMode="decimal" value={tppl_tp}
            onChange={e => setTpplTp(e.target.value)}/>
          <div className="sheet-label">{T("bots.slPrice", "SL цена")}</div>
          <input className="m-input" inputMode="decimal" value={tppl_sl}
            onChange={e => setTpplSl(e.target.value)}/>
          <div style={{ marginTop: 12, padding: 12, background: "var(--bg-2)", borderRadius: 8, fontSize: 12, color: "var(--text-2)", lineHeight: 1.5 }}>
            {T("bots.tppl.hint", "Бот откроет позицию и будет держать пока цена не достигнет TP или SL — затем закроет market.")}
          </div>
        </>
      )}
    </BottomSheet>
  );
};

// ============================================================
// LocalBotDetailsSheet — полная инфа о локальном боте (как у OKX)
// ============================================================
const LocalBotDetailsSheet = ({ botId, onClose, onToast, onChanged }) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [calcPx, setCalcPx] = useState("");
  const [tab, setTab] = useState("overview"); // overview | params | fills

  const load = useCallback(async () => {
    setLoading(true);
    const d = await window.MobileAPI.loadLocalBotDetails(botId);
    setData(d);
    if (d?.mark_px) setCalcPx(String(d.mark_px));
    setLoading(false);
  }, [botId]);
  useEffect(() => { load(); }, [load]);

  // Авто-обновление каждые 15с
  useEffect(() => {
    const id = setInterval(() => { load(); }, 15000);
    return () => clearInterval(id);
  }, [load]);

  const toggleState = async () => {
    if (!data?.bot) return;
    try {
      if (data.bot.state === "running") {
        await window.MobileAPI.stopLocalBot(botId);
        onToast({ msg: T("bots.stopped", "Остановлен") });
      } else {
        await window.MobileAPI.startLocalBot(botId);
        onToast({ msg: T("bots.started", "Запущен") });
      }
      await load();
      onChanged && onChanged();
    } catch (e) {
      onToast({ msg: T("common.error", "Ошибка:") + " " + (e.message || e).slice(0, 80), err: true });
    }
  };

  const remove = async () => {
    if (!data?.bot) return;
    if (!confirm(T("bots.confirmDelete", "Удалить бота? PnL будет потерян."))) return;
    try {
      await window.MobileAPI.deleteLocalBot(botId);
      onToast({ msg: T("bots.deleted", "Удалён") });
      onChanged && onChanged();
      onClose();
    } catch (e) {
      onToast({ msg: T("common.error", "Ошибка:") + " " + (e.message || e).slice(0, 80), err: true });
    }
  };

  if (loading || !data) {
    return (
      <BottomSheet title={T("common.loading", "Загрузка…")} onClose={onClose}>
        <div style={{ padding: 20, textAlign: "center", color: "var(--text-3)" }}>
          {T("common.loading", "Загрузка…")}
        </div>
      </BottomSheet>
    );
  }

  const { bot, config, state, mark_px, fills } = data;
  const cfg = config || {};
  const st = state || {};
  const isDca = bot.bot_type === "dca";
  const isGrid = bot.bot_type === "grid";
  const isLong = (bot.direction || "long") === "long";
  const sign = isLong ? 1 : -1;

  // Расчёты
  const avgEntry = parseFloat(st.avg_entry || 0);
  const totalQty = parseFloat(st.total_qty || 0);
  const totalMargin = parseFloat(st.total_margin || bot.investment_usdt || 0);
  const positionUsdt = avgEntry > 0 ? totalQty * avgEntry : 0;
  const lev = bot.leverage || 1;

  // Live unrealized PnL
  const liveFloatPnl = (mark_px > 0 && avgEntry > 0)
    ? (mark_px - avgEntry) * totalQty * sign : 0;
  const livePnlPct = totalMargin > 0 ? liveFloatPnl / totalMargin * 100 : 0;
  const totalPnl = parseFloat(bot.total_pnl || 0) + liveFloatPnl;

  // TP / SL целевая цена
  const tpCycle = parseFloat(cfg.tp_per_cycle_pct || 0);
  const slCycle = parseFloat(cfg.sl_pct || 0);
  const tpPrice = avgEntry > 0 && tpCycle > 0
    ? (isLong ? avgEntry * (1 + tpCycle / 100) : avgEntry * (1 - tpCycle / 100))
    : 0;
  const slPrice = avgEntry > 0 && slCycle > 0
    ? (isLong ? avgEntry * (1 - slCycle / 100) : avgEntry * (1 + slCycle / 100))
    : (parseFloat(cfg.sl_price) || 0);
  const tpPriceDirect = parseFloat(cfg.tp_price || 0);
  const slPriceDirect = parseFloat(cfg.sl_price || 0);

  // Калькулятор «если цена будет»
  const targetPx = parseFloat(calcPx);
  const validTarget = Number.isFinite(targetPx) && targetPx > 0;
  const calcPnl = validTarget && avgEntry > 0 && totalQty > 0
    ? (targetPx - avgEntry) * totalQty * sign : 0;
  const calcRoi = totalMargin > 0 ? calcPnl / totalMargin * 100 : 0;
  const calcDeltaPct = validTarget && mark_px > 0
    ? (targetPx - mark_px) / mark_px * 100 : 0;

  // Сколько safety-ордеров уже сработало
  const fillsList = Array.isArray(fills) ? fills : [];
  const initialFill = fillsList.find(f => f.type === "initial");
  const safetyFills = fillsList.filter(f => f.type === "safety");
  const maxSafety = parseInt(cfg.max_safety_orders || 0);

  const fmtTime = (ms) => {
    if (!ms) return "—";
    const d = new Date(ms);
    return d.toLocaleString("ru-RU", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" });
  };

  return (
    <BottomSheet title={`${bot.symbol}`} onClose={onClose}>
      {/* ── Header card ─────────────────────── */}
      <div className="m-card" style={{ marginBottom: 12 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 10 }}>
          <Coin sym={(bot.symbol || "").replace(/USDT$/, "")}/>
          <div style={{ flex: 1 }}>
            <div style={{ fontWeight: 700, fontSize: 15 }}>{bot.symbol}</div>
            <div className="muted" style={{ fontSize: 11 }}>
              {isDca ? "Smart-DCA" : isGrid ? "Grid" : bot.bot_type.toUpperCase()}
              {" · "}{bot.exchange.toUpperCase()}
              {" · "}<span style={{ color: bot.state === "running" ? "var(--long)" : "var(--text-3)" }}>
                {bot.state}
              </span>
              {!isLong || true ? " · " + bot.direction : ""}
              {lev > 1 && ` · ${lev}x`}
            </div>
          </div>
        </div>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
          <span className="muted" style={{ fontSize: 12 }}>PnL</span>
          <span className={"mono " + (totalPnl >= 0 ? "pos" : "neg")} style={{ fontSize: 22, fontWeight: 700 }}>
            {totalPnl >= 0 ? "+" : ""}{money(totalPnl)} USDT
            <span style={{ fontSize: 13, marginLeft: 6, opacity: 0.7 }}>
              ({livePnlPct >= 0 ? "+" : ""}{livePnlPct.toFixed(2)}%)
            </span>
          </span>
        </div>
      </div>

      {/* ── Tabs ───────────────────────────── */}
      <div className="m-segmented" style={{ marginBottom: 14 }}>
        <button className={tab === "overview" ? "active" : ""}
          onClick={() => { buzz(); setTab("overview"); }}>{T("bots.tab.overview", "Обзор")}</button>
        <button className={tab === "params" ? "active" : ""}
          onClick={() => { buzz(); setTab("params"); }}>{T("bots.tab.params", "Параметры")}</button>
        <button className={tab === "fills" ? "active" : ""}
          onClick={() => { buzz(); setTab("fills", "Ордера"); }}>{T("bots.tab.fills", "Ордера")} ({fillsList.length})</button>
      </div>

      {tab === "overview" && (
        <>
          {/* Live position */}
          <div className="sheet-label">{T("bots.live", "Текущая позиция")}</div>
          <div className="m-card" style={{ marginBottom: 12 }}>
            <InfoRow label={T("bots.markPrice", "Текущая цена")}
              value={mark_px > 0 ? String(mark_px) : "—"}/>
            <InfoRow label={T("bots.avgEntry", "Средняя цена входа")}
              value={avgEntry > 0 ? avgEntry.toFixed(6) : "—"}/>
            <InfoRow label={T("bots.positionSize", "Размер позиции")}
              value={totalQty > 0 ? `${totalQty.toFixed(4)} ${bot.symbol.replace(/USDT$/, "")}` : "—"}/>
            <InfoRow label={T("bots.positionNotional", "Нотионал")}
              value={positionUsdt > 0 ? `${money(positionUsdt)} USDT` : "—"}/>
            <InfoRow label={T("bots.usedMargin", "Использовано маржи")}
              value={`${money(totalMargin)} USDT`}/>
            <InfoRow label={T("bots.unrealizedPnl", "Нереализованный PnL")}
              value={`${liveFloatPnl >= 0 ? "+" : ""}${money(liveFloatPnl)} USDT`}
              tone={liveFloatPnl >= 0 ? "pos" : "neg"}/>
            {/* Liquidation price — приближение для isolated USDT-perp */}
            {avgEntry > 0 && lev > 1 && (() => {
              const isLng = (bot.direction || "long").toLowerCase() === "long";
              const maint = 0.005;
              const liqPx = isLng
                ? avgEntry * (1 - (1 / lev - maint))
                : avgEntry * (1 + (1 / lev - maint));
              return (
                <InfoRow label={T("bots.liqPrice", "Цена ликвидации (~)")}
                  value={liqPx > 0 ? liqPx.toPrecision(6) : "—"}
                  tone="neg"/>
              );
            })()}
          </div>

          {/* Калькулятор */}
          <div className="sheet-label" style={{ display: "flex", alignItems: "center", gap: 6 }}>
            <MIcon name="layers" size={13} color="var(--accent)"/>
            {T("bots.calc.title", "Калькулятор: «если цена будет…»")}
          </div>
          <div className="m-card" style={{ marginBottom: 12 }}>
            <input className="m-input" inputMode="decimal" value={calcPx}
              onChange={e => setCalcPx(e.target.value.replace(",", "."))}
              placeholder={mark_px > 0 ? String(mark_px) : "0.00"}/>
            <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 6, marginTop: 8 }}>
              {[-10, -5, 5, 10].map(p => (
                <button key={p} className="pct-btn" style={{ fontSize: 11 }}
                  onClick={() => {
                    buzz();
                    if (mark_px > 0) setCalcPx((mark_px * (1 + p / 100)).toFixed(mark_px > 1000 ? 1 : 4));
                  }}>{p > 0 ? "+" : ""}{p}%</button>
              ))}
            </div>
            {validTarget && (
              <div style={{ marginTop: 10, paddingTop: 10, borderTop: "1px solid var(--border)" }}>
                <InfoRow label={T("bots.calc.priceMove", "Движение цены")}
                  value={`${calcDeltaPct >= 0 ? "+" : ""}${calcDeltaPct.toFixed(2)}%`}
                  tone={calcDeltaPct >= 0 ? "pos" : "neg"}/>
                <InfoRow label={T("bots.calc.pnlAt", "PnL при этой цене")}
                  value={`${calcPnl >= 0 ? "+" : ""}${money(calcPnl)} USDT`}
                  tone={calcPnl >= 0 ? "pos" : "neg"}/>
                <InfoRow label={T("bots.roiOnInvestment", "ROI от маржи")}
                  value={`${calcRoi >= 0 ? "+" : ""}${calcRoi.toFixed(2)}%`}
                  tone={calcRoi >= 0 ? "pos" : "neg"}/>
              </div>
            )}
          </div>

          {/* TP / SL */}
          {(tpPrice > 0 || slPrice > 0 || tpPriceDirect > 0 || slPriceDirect > 0) && (
            <>
              <div className="sheet-label">{T("bots.tpSl", "Take Profit / Stop Loss")}</div>
              <div className="m-card" style={{ marginBottom: 12 }}>
                {(tpPrice > 0 || tpPriceDirect > 0) && (
                  <InfoRow label={T("bots.tpPrice", "TP цена")}
                    value={`${(tpPrice || tpPriceDirect).toFixed(6)} ${tpCycle > 0 ? `(${tpCycle}% от avg)` : ""}`}
                    tone="pos"/>
                )}
                {(slPrice > 0 || slPriceDirect > 0) && (
                  <InfoRow label={T("bots.slPrice", "SL цена")}
                    value={`${(slPrice || slPriceDirect).toFixed(6)} ${slCycle > 0 ? `(${slCycle}% от avg)` : ""}`}
                    tone="neg"/>
                )}
              </div>
            </>
          )}

          {/* Smart-DCA состояние */}
          {isDca && (
            <>
              <div className="sheet-label">{T("bots.dca.progress", "Прогресс DCA")}</div>
              <div className="m-card" style={{ marginBottom: 12 }}>
                <InfoRow label={T("bots.dca.safetyDone", "Сработавших страховочных")}
                  value={`${safetyFills.length} / ${maxSafety || "∞"}`}/>
                <InfoRow label={T("bots.dca.cyclesDone", "Завершённых циклов")}
                  value={String(st.cycles_done || 0)}/>
                <InfoRow label={T("bots.dca.entryFirst", "Начальная цена входа")}
                  value={initialFill ? String(initialFill.price) : "—"}/>
                <InfoRow label={T("bots.dca.lastEntry", "Последняя цена входа")}
                  value={st.last_entry_price ? String(st.last_entry_price) : "—"}/>
              </div>
              {/* Manual buy — особенно нужно когда max_safety исчерпан */}
              <button className="m-button block" style={{ marginBottom: 12 }}
                onClick={async () => {
                  buzz();
                  if (!confirm(T("bots.dca.manualBuyConfirm",
                    "Докупить вручную ещё один уровень DCA (по текущей рыночной цене)?")))
                    return;
                  try {
                    const r = await fetch(`/api/me/local-bots/${botId}/manual-safety`,
                      { method: "POST", credentials: "include" });
                    const data = await r.json();
                    if (r.ok) {
                      onToast({ msg: T("bots.dca.manualBuyDone",
                        "Докупка выполнена: ") + (data.qty || "?") + " @ " + (data.price || "?") });
                      onChanged && onChanged();
                    } else {
                      onToast({ msg: (data.detail || "Ошибка"), err: true });
                    }
                  } catch (exc) {
                    onToast({ msg: String(exc), err: true });
                  }
                }}>
                {T("bots.dca.manualBuy", "📥 Докупить вручную")}
              </button>
            </>
          )}
          {/* Grid состояние */}
          {isGrid && (
            <>
              <div className="sheet-label">{T("bots.grid.progress", "Состояние сетки")}</div>
              <div className="m-card" style={{ marginBottom: 12 }}>
                <InfoRow label={T("bots.grid.range", "Диапазон")}
                  value={st.range_lo && st.range_hi
                    ? `${st.range_lo} — ${st.range_hi}`
                    : (cfg.min_price && cfg.max_price ? `${cfg.min_price} — ${cfg.max_price}` : "—")}/>
                <InfoRow label={T("bots.grid.gridCount", "Уровней в сетке")}
                  value={String((st.levels || []).length || cfg.grid_num || 0)}/>
                <InfoRow label={T("bots.grid.tradesDone", "Сделок завершено")}
                  value={String(st.trades_count || 0)}/>
                <InfoRow label={T("bots.grid.cyclesDone", "Циклов (buy+sell)")}
                  value={String(Math.floor((st.trades_count || 0) / 2))}/>
                <InfoRow label={T("bots.grid.inRange", "В диапазоне")}
                  value={st.in_range === false
                    ? T("bots.grid.outOfRange.paused", "❌ Вне диапазона — пауза")
                    : T("bots.grid.inRange.ok", "✅ Активен")}
                  tone={st.in_range === false ? "neg" : "pos"}/>
                {st.initial_bulk_done && st.initial_bulk_qty > 0 && (
                  <InfoRow label={T("bots.grid.bulkBuy", "Стартовая позиция")}
                    value={`${Number(st.initial_bulk_qty).toFixed(4)} @ ${st.initial_bulk_price}`}/>
                )}
              </div>
            </>
          )}
        </>
      )}

      {tab === "params" && (
        <>
          <div className="sheet-label">{T("bots.basic", "Основные")}</div>
          <div className="m-card" style={{ marginBottom: 12 }}>
            <InfoRow label={T("bots.type", "Тип")} value={bot.bot_type.toUpperCase()}/>
            <InfoRow label={T("bots.exchange", "Биржа")} value={bot.exchange.toUpperCase()}/>
            <InfoRow label={T("bots.side", "Направление")} value={bot.direction}/>
            <InfoRow label={T("bots.invested", "Инвестировано")} value={`${money(bot.investment_usdt)} USDT`}/>
            <InfoRow label={T("lev.title", "Плечо")} value={`${bot.leverage}x`}/>
          </div>

          {isDca && (
            <>
              <div className="sheet-label">{T("bots.dca.params", "Параметры стратегии")}</div>
              <div className="m-card" style={{ marginBottom: 12 }}>
                <InfoRow label={T("bots.dca.initialMargin", "Маржа начального")}
                  value={`${cfg.initial_margin || 0} USDT`}/>
                <InfoRow label={T("bots.dca.safetyMargin", "Маржа страховочного")}
                  value={`${cfg.safety_margin || 0} USDT`}/>
                <InfoRow label={T("bots.dca.priceStep", "Шаг цены")} value={`${cfg.price_step_pct || 0}%`}/>
                <InfoRow label={T("bots.dca.volMult", "Объём ×")} value={String(cfg.volume_multiplier || 1)}/>
                <InfoRow label={T("bots.dca.marginMult", "Маржа ×")} value={String(cfg.margin_multiplier || 1)}/>
                <InfoRow label={T("bots.dca.maxSafety", "Макс. страховочных")} value={String(cfg.max_safety_orders || 0)}/>
                <InfoRow label={T("bots.dca.tpCycle", "TP за цикл")} value={`${cfg.tp_per_cycle_pct || 0}%`}/>
                <InfoRow label={T("bots.dca.slGlobal", "SL")} value={cfg.sl_pct ? `${cfg.sl_pct}%` : "—"}/>
              </div>
            </>
          )}

          {isGrid && (
            <>
              <div className="sheet-label">{T("bots.gridParams", "Параметры сетки")}</div>
              <div className="m-card" style={{ marginBottom: 12 }}>
                <InfoRow label={T("bots.range", "Диапазон")} value={`${cfg.min_price || 0} – ${cfg.max_price || 0}`}/>
                <InfoRow label={T("bots.ordersCount", "Число ордеров")} value={String(cfg.grid_num || 0)}/>
              </div>
            </>
          )}

          <div className="sheet-label">{T("bots.uptime", "Время работы")}</div>
          <div className="m-card" style={{ marginBottom: 12 }}>
            <InfoRow label={T("bots.createdAt", "Создан")} value={fmtTime(bot.created_at)}/>
            {bot.started_at && (
              <InfoRow label={T("bots.startedAt", "Запущен")} value={fmtTime(bot.started_at)}/>
            )}
            <InfoRow label={T("bots.totalTrades", "Всего сделок")} value={String(bot.trades_count || 0)}/>
          </div>
        </>
      )}

      {tab === "fills" && (
        <>
          {fillsList.length === 0 ? (
            <div style={{ padding: 16, color: "var(--text-3)", textAlign: "center", fontSize: 13 }}>
              {T("bots.noFills", "Сделок пока не было")}
            </div>
          ) : (
            <>
              <div className="sheet-label">{T("bots.fillsHistory", "История ордеров")} ({fillsList.length})</div>
              {fillsList.slice().reverse().map((f, i) => {
                const isInit = f.type === "initial";
                return (
                  <div key={i} className="m-card" style={{ marginBottom: 6, padding: 10, display: "flex", alignItems: "center", gap: 10 }}>
                    <span style={{
                      width: 6, height: 32, borderRadius: 3,
                      background: isInit ? "var(--accent)" : "var(--warn)",
                    }}/>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ fontSize: 12, fontWeight: 600 }}>
                        {isInit ? T("bots.fill.initial", "Начальный") : `${T("bots.fill.safety", "Страховочный")} #${f.n}`}
                      </div>
                      <div className="muted" style={{ fontSize: 10 }}>
                        {fmtTime(f.ts)} · qty {Number(f.qty).toFixed(4)}
                      </div>
                    </div>
                    <div style={{ textAlign: "right" }}>
                      <div className="mono" style={{ fontSize: 12, fontWeight: 600 }}>{f.price}</div>
                      <div className="muted mono" style={{ fontSize: 10 }}>
                        {Number(f.margin).toFixed(2)} USDT
                      </div>
                    </div>
                  </div>
                );
              })}
            </>
          )}
        </>
      )}

      {/* Action buttons */}
      <div style={{ display: "flex", gap: 8, marginTop: 14, marginBottom: 20 }}>
        <button className={"m-button lg " + (bot.state === "running" ? "" : "primary")}
          style={{ flex: 2 }} onClick={toggleState}>
          {bot.state === "running"
            ? T("bots.stop", "⏸ Стоп")
            : T("bots.start", "▶ Старт")}
        </button>
        <button className="m-button danger lg" onClick={remove}>
          <MIcon name="x" size={16}/>
        </button>
      </div>
    </BottomSheet>
  );
};

// ------------------------------------------------------------
// Bot details sheet — все поля + sub-orders + калькулятор PnL@price
// ------------------------------------------------------------
const _num = (v) => {
  const n = parseFloat(v);
  return Number.isFinite(n) ? n : 0;
};

const InfoRow = ({ label, value, tone }) => (
  <div style={{ display: "flex", justifyContent: "space-between", padding: "6px 0", borderBottom: "1px solid var(--border)" }}>
    <span className="muted" style={{ fontSize: 12 }}>{label}</span>
    <span className={"mono" + (tone ? " " + tone : "")} style={{ fontSize: 13, fontWeight: 500 }}>{value}</span>
  </div>
);

const BotDetailsSheet = ({ bot, exchange, onClose, onToast }) => {
  const [data, setData] = useState(null);
  const [orders, setOrders] = useState([]);
  const [loading, setLoading] = useState(true);
  const [calcPx, setCalcPx] = useState("");
  // Учитывать ли арбитражи сетки в калькуляторе
  const [useGridArb, setUseGridArb] = useState(true);
  // Какие sub-orders показывать
  const [ordersTab, setOrdersTab] = useState("all"); // all|buy|sell

  useEffect(() => {
    let stopped = false;
    (async () => {
      try {
        const r = await window.MobileAPI.loadBotDetails(exchange, bot.id, bot.type || "contract_grid");
        if (stopped) return;
        if (!r) {
          onToast && onToast({ msg: T("bots.error.loadDetails", "Ошибка загрузки деталей бота"), err: true });
          onClose && onClose();
          return;
        }
        const d = r?.details || r?.detail || r || {};
        const so = r?.subOrders || r?.sub_orders || [];
        setData({ ...d, markPx: r?.markPx || d.markPx || d.runPx });
        setOrders(so);
        const def = parseFloat(r?.markPx) || parseFloat(d.markPx) || parseFloat(d.runPx);
        if (def) setCalcPx(String(def));
      } catch (e) {
        if (stopped) return;
        onToast && onToast({ msg: "Ошибка: " + (e.message || e).slice(0, 80), err: true });
        onClose && onClose();
      } finally {
        if (!stopped) setLoading(false);
      }
    })();
    return () => { stopped = true; };
  }, [exchange, bot.id, bot.type]);

  if (loading) {
    return (
      <BottomSheet title={bot.symbol || T("bots.title", "Бот")} onClose={onClose}>
        <div style={{ padding: 20, color: "var(--text-3)", textAlign: "center" }}>{T("bots.loadingDetails", "Загрузка деталей…")}</div>
      </BottomSheet>
    );
  }

  // Парсинг ключевых значений
  const d = data || {};
  const dir = String(d.direction || "long").toLowerCase();
  const sign = dir === "long" ? 1 : -1;
  const inst = String(d.instId || d.symbol || bot.symbol || "").toUpperCase();
  const investment = _num(d.investment || d.sz || bot.invested);
  const lever = _num(d.lever || d.leverage || 0);
  const runPx = _num(d.runPx || 0);
  const minPx = _num(d.minPx || 0);
  const maxPx = _num(d.maxPx || 0);
  const gridNum = _num(d.gridNum || 0);
  const singleAmt = _num(d.singleAmt || 0);
  const activeOrdNum = _num(d.activeOrdNum || 0);
  const liqPx = _num(d.liqPx || 0);
  const actualLever = _num(d.actualLever || 0);
  const ordFrozen = _num(d.ordFrozen || 0);
  const tpPx = _num(d.tpTriggerPx || 0);
  const slPx = _num(d.slTriggerPx || 0);

  const totalPnl = _num(d.totalPnl || bot.pnl);
  const pnlRatio = _num(d.pnlRatio || 0) * 100;
  const floatProfit = _num(d.floatProfit || 0);
  const gridProfit = _num(d.gridProfit || 0);
  const fundingFee = _num(d.fundingFee || 0);
  const fee = _num(d.fee || 0);
  const eq = _num(d.eq || 0);
  const availEq = _num(d.availEq || 0);
  const annualRate = _num(d.annualizedRate || 0) * 100;
  const totalAnnualRate = _num(d.totalAnnualizedRate || 0) * 100;
  const arbitrageNum = _num(d.arbitrageNum || 0);
  const tradeNum = _num(d.tradeNum || 0);

  const cTime = _num(d.cTime || 0);
  const uTime = _num(d.uTime || 0);
  const runtimeMs = uTime && cTime ? (uTime - cTime) : 0;
  const runtimeDays = runtimeMs > 0 ? (runtimeMs / 86400000) : 0;

  // runPx у OKX для grid = **start price** позиции (безубыток).
  // markPx (если backend вернул) = текущая mark price биржи.
  const markPx = _num(data?.markPx || d.markPx || 0) || runPx;

  // Самая точная позиция бота — обратной калибровкой от живого floatProfit:
  //   floatProfit = (markPx − startPx) × position × direction
  //   → position = floatProfit / (markPx − startPx) / direction
  // Это даст ТОЧНОЕ положение, которое использует сам OKX.
  // Fallback (если markPx == runPx или floatProfit мал): eq × actualLever.
  let positionUnits = 0;
  if (markPx > 0 && Math.abs(markPx - runPx) > 0.000001 && Math.abs(floatProfit) > 0.01) {
    positionUnits = floatProfit / ((markPx - runPx) * sign);
  } else {
    const effectiveNotional = (eq > 0 && actualLever > 0)
      ? eq * actualLever
      : investment * lever * 0.5;
    positionUnits = markPx > 0 ? effectiveNotional / markPx : 0;
  }
  positionUnits = Math.abs(positionUnits);
  const positionUsdt = positionUnits * markPx;

  // Средняя прибыль с одного арбитража сетки (уже заработанная):
  // gridProfit / arbitrageNum. Используем для прогноза «сколько ещё grid
  // отработает при движении target».
  const avgArbProfit = arbitrageNum > 0 ? gridProfit / arbitrageNum : 0;
  const gridStep = gridNum > 0 && maxPx > minPx ? (maxPx - minPx) / gridNum : 0;

  // ---- Калькулятор «при цене X получу Y USDT» (точный для grid) ---------
  // Считаем компоненты отдельно:
  //   1) FLOAT profit = (target - startPx) × position × direction
  //      → 0 в безубытке, +24 USDT при 2.18 если start=1.915
  //   2) GRID profit = текущий + предполагаемый прирост от новых арбитражей
  //   3) Funding + fees — берём как есть
  //   4) Total = 1 + 2 + 3
  const targetPx = parseFloat(calcPx);
  const validTarget = Number.isFinite(targetPx) && targetPx > 0;

  // (1) Float profit при target. positionUnits = const (упрощение — реально
  // grid уменьшает экспозицию вверх на ~singleAmt за каждый step, но это
  // даёт всего ~5% поправку для типичных диапазонов).
  const priceMove = validTarget ? (targetPx - runPx) : 0;
  const targetFloatPnl = validTarget ? priceMove * positionUnits * sign : 0;

  // (2) Дополнительные grid-арбитражи: новые арбитражи сработают только при
  // движении от ТЕКУЩЕЙ markPx (не от startPx), так как заработанные уже
  // зафиксированы. Один арбитраж ≈ один шаг сетки.
  const futureMove = validTarget && markPx > 0 ? Math.abs(targetPx - markPx) : 0;
  const stepsCrossed = gridStep > 0 ? futureMove / gridStep : 0;
  const expectedExtraArb = useGridArb ? Math.floor(stepsCrossed) * avgArbProfit : 0;
  const targetGridPnl = gridProfit + expectedExtraArb;

  // (3) Funding + fees — оставляем как есть (для прогноза не пересчитываем).
  const baseFeeFunding = fundingFee + fee;

  // (4) Итого
  const estimatedFloat = targetFloatPnl;
  const estimatedPnl = validTarget
    ? targetFloatPnl + targetGridPnl + baseFeeFunding
    : totalPnl;
  const estimatedRoi = investment > 0 ? estimatedPnl / investment * 100 : 0;
  const deltaPct = validTarget && runPx > 0 ? (targetPx - runPx) / runPx * 100 : 0;
  const reachesLiq = validTarget && liqPx > 0 && (
    (dir === "long" && targetPx <= liqPx) ||
    (dir === "short" && targetPx >= liqPx)
  );
  const outOfRange = validTarget && (targetPx < minPx || targetPx > maxPx);

  const setTargetPct = (pct) => {
    const base = markPx > 0 ? markPx : runPx;
    if (base > 0) setCalcPx((base * (1 + pct / 100)).toFixed(base > 1000 ? 1 : 4));
  };

  const fmtTime = (ms) => {
    if (!ms) return "—";
    const d = new Date(ms);
    return d.toLocaleString("ru-RU", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" });
  };

  const fmtDur = (ms) => {
    if (!ms) return "—";
    const days = Math.floor(ms / 86400000);
    const hrs = Math.floor((ms % 86400000) / 3600000);
    if (days > 0) return `${days}д ${hrs}ч`;
    const mins = Math.floor((ms % 3600000) / 60000);
    return `${hrs}ч ${mins}м`;
  };

  // Сортировка sub-orders новые → старые
  const ordersSorted = [...orders].sort((a, b) => _num(b.uTime || b.cTime) - _num(a.uTime || a.cTime));
  const filteredOrders = ordersTab === "all"
    ? ordersSorted
    : ordersSorted.filter(o => String(o.side).toLowerCase() === ordersTab);

  return (
    <BottomSheet title={inst} onClose={onClose}>
      {/* ── Заголовок и hero PnL ─────────────── */}
      <div className="m-card" style={{ marginBottom: 10 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 10 }}>
          <Coin sym={inst.replace(/-?USDT.*$/i, "").replace(/USDT$/, "")}/>
          <div style={{ flex: 1 }}>
            <div style={{ fontWeight: 600 }}>{inst}</div>
            <div className="muted" style={{ fontSize: 11, marginTop: 2 }}>
              {BOT_TYPE_LABELS[bot.type] || bot.type || d.algoOrdType} · {String(d.state || "running")}
              {lever > 0 && <> · {lever}x {dir}</>}
            </div>
          </div>
        </div>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
          <span className="muted" style={{ fontSize: 12 }}>{T("stats.pnl", "PnL")}</span>
          <span className={"mono " + (totalPnl >= 0 ? "pos" : "neg")} style={{ fontSize: 22, fontWeight: 700 }}>
            {totalPnl >= 0 ? "+" : ""}{money(totalPnl)} USDT
            <span style={{ fontSize: 13, marginLeft: 6, opacity: 0.7 }}>({pnlRatio >= 0 ? "+" : ""}{pnlRatio.toFixed(2)}%)</span>
          </span>
        </div>
      </div>

      {/* ── Калькулятор PnL @ price ──────────── */}
      <div className="sheet-label" style={{ display: "flex", alignItems: "center", gap: 6 }}>
        <MIcon name="layers" size={13} color="var(--accent)"/> Калькулятор: «если цена будет…»
      </div>
      <div className="m-card" style={{ marginBottom: 14 }}>
        <input className="m-input" inputMode="decimal" value={calcPx}
          onChange={(e) => setCalcPx(e.target.value.replace(",", "."))}
          placeholder={runPx > 0 ? String(runPx) : "0.00"}/>
        <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 6, marginTop: 8 }}>
          {[-10, -5, 5, 10].map(p => (
            <button key={p} className="pct-btn"
              style={{ fontSize: 11, padding: "6px 0" }}
              onClick={() => { buzz(); setTargetPct(p); }}>
              {p > 0 ? "+" : ""}{p}%
            </button>
          ))}
        </div>
        {validTarget && (
          <div style={{ marginTop: 12, paddingTop: 10, borderTop: "1px solid var(--border)" }}>
            <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 8, fontSize: 12 }}>
              <span className="muted">{T("bots.calc.priceMove", "Движение цены")}</span>
              <span className="mono" style={{ color: deltaPct >= 0 ? "var(--long)" : "var(--short)" }}>
                {deltaPct >= 0 ? "+" : ""}{deltaPct.toFixed(2)}%
                <span className="muted" style={{ marginLeft: 4 }}>({(targetPx - runPx).toFixed(4)})</span>
              </span>
            </div>

            {/* Компоненты PnL */}
            <div style={{ padding: "10px 0", borderTop: "1px dashed var(--border)" }}>
              <div style={{ display: "flex", justifyContent: "space-between", fontSize: 12, marginBottom: 4 }}>
                <span className="muted">{T("bots.calc.pricePnl", "Price-PnL (без сетки)")}</span>
                <span className={"mono " + (targetFloatPnl >= 0 ? "pos" : "neg")}>
                  {targetFloatPnl >= 0 ? "+" : ""}{money(targetFloatPnl)} USDT
                </span>
              </div>
              <div style={{ display: "flex", justifyContent: "space-between", fontSize: 12, marginBottom: 4 }}>
                <span className="muted">
                  Сетка {useGridArb ? `(+${Math.floor(stepsCrossed)} арб.)` : T("bots.calc.earnedOnly", "(только заработано)")}
                </span>
                <span className="mono pos">
                  +{money(targetGridPnl)} USDT
                </span>
              </div>
              <div style={{ display: "flex", justifyContent: "space-between", fontSize: 12, marginBottom: 4 }}>
                <span className="muted">Funding + fees</span>
                <span className={"mono " + (baseFeeFunding >= 0 ? "pos" : "neg")}>
                  {baseFeeFunding >= 0 ? "+" : ""}{money(baseFeeFunding)} USDT
                </span>
              </div>
            </div>

            {/* Тоггл grid-арбитража */}
            <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "6px 0", borderTop: "1px solid var(--border)", borderBottom: "1px solid var(--border)", marginBottom: 10 }}>
              <span style={{ fontSize: 11, color: "var(--text-2)" }}>
                Учитывать арбитражи сетки <span className="muted">(~{avgArbProfit.toFixed(4)}$/арб)</span>
              </span>
              <span className={"m-switch sm" + (useGridArb ? " on" : "")}
                onClick={() => { buzz(); setUseGridArb(v => !v); }}/>
            </div>

            <div style={{ display: "flex", justifyContent: "space-between", fontSize: 13, marginBottom: 6 }}>
              <span style={{ fontWeight: 600 }}>{T("bots.totalPnl", "Итого PnL")}</span>
              <span className={"mono " + (estimatedPnl >= 0 ? "pos" : "neg")} style={{ fontWeight: 700, fontSize: 16 }}>
                {estimatedPnl >= 0 ? "+" : ""}{money(estimatedPnl)} USDT
              </span>
            </div>
            <div style={{ display: "flex", justifyContent: "space-between", fontSize: 12 }}>
              <span className="muted">{T("bots.roiOnInvestment", "ROI от инвестиций")}</span>
              <span className={"mono " + (estimatedRoi >= 0 ? "pos" : "neg")} style={{ fontWeight: 600 }}>
                {estimatedRoi >= 0 ? "+" : ""}{estimatedRoi.toFixed(2)}%
              </span>
            </div>

            {outOfRange && (
              <div style={{ marginTop: 10, padding: 10, background: "var(--warn-dim)", border: "1px solid var(--warn)", borderRadius: 6, color: "var(--warn)", fontSize: 11, display: "flex", gap: 6, alignItems: "flex-start" }}>
                <MIcon name="lock" size={12}/>
                Цена вне диапазона сетки {minPx}–{maxPx}. Бот остановится, дальнейшие арбитражи не сработают.
              </div>
            )}
            {reachesLiq && (
              <div style={{ marginTop: 10, padding: 10, background: "var(--short-bg)", border: "1px solid var(--short-dim)", borderRadius: 6, color: "var(--short)", fontSize: 11, display: "flex", gap: 6, alignItems: "center" }}>
                <MIcon name="lock" size={12}/> Цена ниже/выше ликвидации ({liqPx}) — позиция будет ликвидирована
              </div>
            )}

            <div style={{ fontSize: 10, color: "var(--text-4)", marginTop: 8, lineHeight: 1.4 }}>
              Price-PnL = (target − start_price) × позиция. Безубыток позиции при start_price = {runPx}.
              Арбитражей сетки: предположение «один на каждый шаг», реальность зависит от колебаний.
            </div>
          </div>
        )}
      </div>

      {/* ── Состав PnL ────────────────────────── */}
      <div className="sheet-label">{T("bots.pnlBreakdown", "Состав PnL")}</div>
      <div className="m-card" style={{ marginBottom: 14 }}>
        <InfoRow label={T("bots.gridProfitFixed", "Прибыль с сетки (фикс.)")} value={`${gridProfit >= 0 ? "+" : ""}${money(gridProfit)} USDT`} tone={gridProfit >= 0 ? "pos" : "neg"}/>
        <InfoRow label={T("bots.unrealizedPnl", "Нереализованный PnL")} value={`${floatProfit >= 0 ? "+" : ""}${money(floatProfit)} USDT`} tone={floatProfit >= 0 ? "pos" : "neg"}/>
        <InfoRow label={T("bots.fundingFees", "Funding fees")} value={`${fundingFee >= 0 ? "+" : ""}${money(fundingFee)} USDT`} tone={fundingFee >= 0 ? "pos" : "neg"}/>
        <InfoRow label={T("bots.fees", "Комиссии")} value={`${money(fee)} USDT`} tone="neg"/>
        <InfoRow label={T("bots.annualYield", "Годовая доходность")} value={`${annualRate.toFixed(2)}%`}/>
      </div>

      {/* ── Параметры сетки ───────────────────── */}
      <div className="sheet-label">{T("bots.gridParams", "Параметры сетки")}</div>
      <div className="m-card" style={{ marginBottom: 14 }}>
        <InfoRow label={T("bots.range", "Диапазон")} value={`${minPx} – ${maxPx}`}/>
        <InfoRow label={T("bots.entryPriceBreakeven", "Цена входа (безубыток)")} value={String(runPx)}/>
        {markPx > 0 && markPx !== runPx && (
          <InfoRow label={T("bots.currentPrice", "Текущая цена")} value={String(markPx)}/>
        )}
        <InfoRow label={T("bots.ordersCount", "Число ордеров")} value={String(gridNum)}/>
        <InfoRow label={T("bots.activeOrders", "Активные ордера")} value={String(activeOrdNum)}/>
        <InfoRow label={T("bots.orderSize", "Размер ордера")} value={`${singleAmt} ${inst.includes("USDT") ? T("bots.contract", "контракт") : "USDT"}`}/>
        <InfoRow label={T("bots.triggeredArbs", "Сработавших арбитражей")} value={String(arbitrageNum)}/>
        <InfoRow label={T("bots.totalTrades", "Всего сделок")} value={String(tradeNum)}/>
      </div>

      {/* ── TP/SL ────────────────────────────── */}
      <div className="sheet-label">{T("bots.tpSl", "Take Profit / Stop Loss")}</div>
      <div className="m-card" style={{ marginBottom: 14 }}>
        {tpPx > 0 || slPx > 0 ? (
          <>
            <InfoRow label={T("bots.tpPrice", "TP цена")} value={tpPx > 0 ? String(tpPx) : T("common.notSet", "не задано")} tone={tpPx > 0 ? "pos" : ""}/>
            <InfoRow label={T("bots.slPrice", "SL цена")} value={slPx > 0 ? String(slPx) : T("common.notSet", "не задано")} tone={slPx > 0 ? "neg" : ""}/>
          </>
        ) : (
          <div style={{ color: "var(--text-3)", fontSize: 12, textAlign: "center", padding: 6 }}>
            TP/SL для бота не настроены — установи их в OKX-app
          </div>
        )}
      </div>

      {/* ── Финансы / маржа ──────────────────── */}
      <div className="sheet-label">{T("bots.investmentAndMargin", "Инвестиции и маржа")}</div>
      <div className="m-card" style={{ marginBottom: 14 }}>
        <InfoRow label={T("bots.invested", "Инвестировано")} value={`${money(investment)} USDT`}/>
        <InfoRow label={T("bots.currentEquity", "Текущая equity")} value={`${money(eq)} USDT`}/>
        <InfoRow label={T("bots.available", "Доступно")} value={`${money(availEq)} USDT`}/>
        <InfoRow label={T("bots.inOrders", "В ордерах")} value={`${money(ordFrozen)} USDT`}/>
        <InfoRow label={T("bots.positionSize", "Размер позиции")} value={`${positionUnits.toFixed(2)} ${inst.replace(/-?USDT.*$/i, "")} ≈ ${money(positionUsdt)} USDT`}/>
      </div>

      {/* ── Риск ─────────────────────────────── */}
      <div className="sheet-label">{T("risk.title", "Риск")}</div>
      <div className="m-card" style={{ marginBottom: 14 }}>
        <InfoRow label={T("lev.title", "Плечо")} value={`${lever}x`}/>
        <InfoRow label={T("bots.effectiveLeverage", "Фактическое плечо")} value={actualLever.toFixed(4) + "x"}/>
        <InfoRow label={T("bots.liqPrice", "Цена ликвидации")} value={liqPx > 0 ? String(liqPx) : "—"} tone={liqPx > 0 ? "neg" : ""}/>
        {runPx > 0 && liqPx > 0 && (
          <InfoRow label={T("bots.distanceToLiq", "Дистанция до liq")}
            value={`${(Math.abs((runPx - liqPx) / runPx) * 100).toFixed(2)}%`}
            tone={Math.abs((runPx - liqPx) / runPx) * 100 < 10 ? "neg" : ""}/>
        )}
      </div>

      {/* ── Тайминги ─────────────────────────── */}
      <div className="sheet-label">{T("bots.uptime", "Время работы")}</div>
      <div className="m-card" style={{ marginBottom: 14 }}>
        <InfoRow label={T("bots.createdAt", "Создан")} value={fmtTime(cTime)}/>
        <InfoRow label={T("bots.running", "Работает")} value={fmtDur(runtimeMs)}/>
        <InfoRow label={T("bots.lastUpdate", "Последнее обновление")} value={fmtTime(uTime)}/>
      </div>

      {/* ── Sub-orders ───────────────────────── */}
      <div className="sheet-label">Последние ордера сетки ({orders.length})</div>
      <div className="m-segmented" style={{ marginBottom: 8 }}>
        <button className={ordersTab === "all" ? "active" : ""} onClick={() => { buzz(); setOrdersTab("all"); }}>{T("common.all", "Все")}</button>
        <button className={ordersTab === "buy" ? "active" : ""} onClick={() => { buzz(); setOrdersTab("buy"); }}>Buy</button>
        <button className={ordersTab === "sell" ? "active" : ""} onClick={() => { buzz(); setOrdersTab("sell"); }}>Sell</button>
      </div>
      {filteredOrders.length === 0 && (
        <div style={{ padding: 14, color: "var(--text-3)", fontSize: 12, textAlign: "center" }}>
          Ордеров не найдено
        </div>
      )}
      <div style={{ marginBottom: 20 }}>
        {filteredOrders.slice(0, 30).map(o => {
          const side = String(o.side).toLowerCase();
          const isBuy = side === "buy";
          return (
            <div key={o.ordId} className="m-card" style={{ marginBottom: 6, display: "flex", alignItems: "center", gap: 10, padding: "8px 12px" }}>
              <span style={{
                width: 6, height: 32, borderRadius: 3,
                background: isBuy ? "var(--long)" : "var(--short)",
              }}/>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 12, display: "flex", gap: 6 }}>
                  <span style={{ color: isBuy ? "var(--long)" : "var(--short)", fontWeight: 600, textTransform: "uppercase" }}>{side}</span>
                  <span className="mono">{_num(o.sz)} @ {_num(o.px)}</span>
                </div>
                <div className="muted" style={{ fontSize: 10 }}>
                  {fmtTime(_num(o.uTime || o.cTime))} · {o.state}
                </div>
              </div>
              <div className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>
                fee {_num(o.fee).toFixed(4)}
              </div>
            </div>
          );
        })}
      </div>
    </BottomSheet>
  );
};

// ------------------------------------------------------------
// Stats sheet — per-channel + TP1/TP2/TP3 hit rate
// ------------------------------------------------------------
const StatsSheet = ({ onClose, onToast }) => {
  const [period, setPeriod] = useState(30);
  const [channels, setChannels] = useState(null);
  const [tps, setTps] = useState(null);
  const [slip, setSlip] = useState(null);
  const [busy, setBusy] = useState(true);

  const load = useCallback(async () => {
    setBusy(true);
    const [c, t, sl] = await Promise.all([
      window.MobileAPI.loadChannelStats(period),
      window.MobileAPI.loadTpStats(period),
      window.MobileAPI.loadSlippageStats(period),
    ]);
    setChannels(c || []);
    setTps(t);
    setSlip(sl);
    setBusy(false);
  }, [period]);
  useEffect(() => { load(); }, [load]);

  return (
    <BottomSheet title={T("stats.title", "Статистика")} onClose={onClose}>
      <div className="m-segmented" style={{ marginBottom: 14 }}>
        {[[7, T("stats.range.7d", "7 дн")], [30, T("stats.range.30d", "30 дн")], [90, T("stats.range.90d", "90 дн")], [365, T("stats.range.year", "Год")]].map(([d, l]) => (
          <button key={d} className={period === d ? "active" : ""}
            onClick={() => { buzz(); setPeriod(d); }}>{l}</button>
        ))}
      </div>

      <div className="sheet-label">{T("stats.tpSlHits", "Достижение TP / SL")}</div>
      <div className="m-card">
        {busy && <div className="muted" style={{ padding: 8 }}>{T("common.loading", "Загрузка…")}</div>}
        {!busy && (() => {
          // Нормализуем форму: tps может прийти как null, {}, или {total, tp_hits, tp_rates, sl_before_tp1_rate}
          const t = tps || {};
          const total = Number(t.total || 0);
          const tpHits = Array.isArray(t.tp_hits) ? t.tp_hits : [0, 0, 0];
          const tpRates = Array.isArray(t.tp_rates) ? t.tp_rates
            : tpHits.map(h => total > 0 ? Math.round(h / total * 1000) / 10 : 0);
          const slRate = Number(t.sl_before_tp1_rate || 0);
          return (
            <>
              <div style={{ fontSize: 11, color: "var(--text-3)", marginBottom: 8 }}>
                {total > 0
                  ? `${T("stats.tpsl.fromTrades", "Из")} ${total} ${T("stats.tpsl.trades", "сделок")}:`
                  : T("stats.tpsl.noData", "Нет закрытых сделок с TP/SL за период — ниже нули для справки.")}
              </div>
              <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 8 }}>
                {[0, 1, 2].map(i => (
                  <div key={i} style={{
                    background: total > 0 ? "var(--long-bg)" : "var(--bg-2)",
                    border: `1px solid ${total > 0 ? "var(--long-dim)" : "var(--border)"}`,
                    borderRadius: 6, padding: 10, textAlign: "center",
                  }}>
                    <div className="muted" style={{ fontSize: 10 }}>TP{i + 1}</div>
                    <div className={"mono " + (total > 0 ? "pos" : "")}
                         style={{ fontSize: 18, fontWeight: 700,
                                  color: total > 0 ? undefined : "var(--text-2)" }}>
                      {tpRates[i] || 0}%
                    </div>
                    <div className="muted" style={{ fontSize: 10 }}>
                      {tpHits[i] || 0} {T("stats.tpsl.outOf", "из")} {total}
                    </div>
                  </div>
                ))}
              </div>
              <div style={{
                marginTop: 10, padding: 10,
                background: total > 0 ? "var(--short-bg)" : "var(--bg-2)",
                border: `1px solid ${total > 0 ? "var(--short-dim)" : "var(--border)"}`,
                borderRadius: 6, display: "flex", justifyContent: "space-between",
                alignItems: "center",
              }}>
                <div>
                  <div className="muted" style={{ fontSize: 10 }}>{T("stats.slBeforeTp1", "SL до TP1")}</div>
                  <div style={{ fontSize: 12, color: "var(--text-2)" }}>{T("stats.slBeforeTp1.desc", "сбило стоп раньше первого тейка")}</div>
                </div>
                <div className={"mono " + (total > 0 ? "neg" : "")}
                     style={{ fontSize: 20, fontWeight: 700,
                              color: total > 0 ? undefined : "var(--text-2)" }}>
                  {slRate}%
                </div>
              </div>
            </>
          );
        })()}
      </div>

      {slip && slip.samples > 0 && (
        <>
          <div className="sheet-label">{T("stats.slippage", "Slippage-анализ")}</div>
          <div className="m-card" style={{ marginBottom: 14 }}>
            <div style={{ fontSize: 11, color: "var(--text-3)", marginBottom: 8 }}>
              {T("stats.slippage.intro", "Разница между интендированной и реальной ценой исполнения. Сколько вы платите на «налоге» исполнения.")}
            </div>
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 10, fontSize: 12 }}>
              <div>
                <div className="muted" style={{ fontSize: 10 }}>{T("stats.slippage.total", "Потеряно за период")}</div>
                <div className="mono neg" style={{ fontWeight: 700 }}>-{money(slip.total_slip_usdt)}</div>
              </div>
              <div>
                <div className="muted" style={{ fontSize: 10 }}>{T("stats.slippage.avg", "Среднее на сделку")}</div>
                <div className="mono neg">-{money(slip.avg_slip_per_trade)}</div>
              </div>
              <div>
                <div className="muted" style={{ fontSize: 10 }}>{T("stats.slippage.pct", "% от объёма")}</div>
                <div className="mono">{slip.slip_pct_of_volume}%</div>
              </div>
            </div>
            {slip.worst_5 && slip.worst_5.length > 0 && (
              <div style={{ marginTop: 10, paddingTop: 10, borderTop: "1px solid var(--border)" }}>
                <div className="muted" style={{ fontSize: 10, marginBottom: 6 }}>{T("stats.slippage.worst", "Худшие сделки")}</div>
                {slip.worst_5.slice(0, 3).map(w => (
                  <div key={w.trade_id} style={{ display: "flex", justifyContent: "space-between", fontSize: 11, padding: "2px 0" }}>
                    <span className="mono">{w.symbol}</span>
                    <span className="mono neg">-{money(w.slip_usdt)} ({w.slip_pct}%)</span>
                  </div>
                ))}
              </div>
            )}
          </div>
        </>
      )}

      <div className="sheet-label">{T("channel.sources", "Каналы-источники")}</div>
      {busy && <div className="muted" style={{ padding: 8 }}>{T("common.loading", "Загрузка…")}</div>}
      {channels && channels.length === 0 && (
        <div className="muted" style={{ padding: 14, textAlign: "center", fontSize: 13 }}>
          Нет данных за период
        </div>
      )}
      {channels && channels.map(c => (
        <div key={c.channel} className="m-card" style={{ marginBottom: 8 }}>
          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <MIcon name="plane" size={13} color="var(--accent)"/>
            <span style={{ fontWeight: 600, flex: 1, fontSize: 13 }}>{c.channel}</span>
            <span className={"mono " + (c.total_pnl >= 0 ? "pos" : "neg")} style={{ fontWeight: 700 }}>
              {c.total_pnl >= 0 ? "+" : ""}{money(c.total_pnl)} USDT
            </span>
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 8, marginTop: 10, fontSize: 11 }}>
            <div><div className="muted">{T("stats.trades", "Сделок")}</div><div className="mono">{c.count}</div></div>
            <div><div className="muted">Winrate</div><div className="mono">{c.win_rate}%</div></div>
            <div><div className="muted">Avg PnL</div><div className={"mono " + (c.avg_pnl >= 0 ? "pos" : "neg")}>{c.avg_pnl >= 0 ? "+" : ""}{c.avg_pnl}</div></div>
            <div><div className="muted">R/R</div><div className="mono">{c.avg_rr ?? "—"}</div></div>
          </div>
          <div style={{ marginTop: 6, fontSize: 10, color: "var(--text-3)" }}>
            {c.wins}W / {c.losses}L
          </div>
        </div>
      ))}
    </BottomSheet>
  );
};

// ------------------------------------------------------------
// Push notifications sheet
// ------------------------------------------------------------
const PushSheet = ({ onClose, onToast }) => {
  const [supported, setSupported] = useState(true);
  const [perm, setPerm] = useState("default");
  const [enabled, setEnabled] = useState(false);
  const [busy, setBusy] = useState(false);

  const load = async () => {
    setSupported(await window.MobileAPI.pushIsSupported());
    setPerm(await window.MobileAPI.pushGetPermission());
    const s = await window.MobileAPI.pushStatus();
    setEnabled(!!s.enabled);
  };
  useEffect(() => { load(); }, []);

  const toggle = async () => {
    if (busy) return;
    setBusy(true);
    try {
      if (enabled) {
        await window.MobileAPI.pushDisable();
        onToast({ msg: T("push.off", "Push выключен") });
      } else {
        await window.MobileAPI.pushEnable();
        onToast({ msg: T("push.onCheckTest", "Push включён — проверь тестом") });
      }
      await load();
    } catch (e) {
      onToast({ msg: "Ошибка: " + (e.message || e).slice(0, 100), err: true });
    } finally { setBusy(false); }
  };

  const test = async () => {
    if (busy) return;
    setBusy(true);
    try {
      const r = await window.MobileAPI.pushTest();
      onToast({ msg: `Тест отправлен · доставлено ${r.delivered}` });
    } catch (e) {
      onToast({ msg: "Ошибка: " + (e.message || e).slice(0, 100), err: true });
    } finally { setBusy(false); }
  };

  return (
    <BottomSheet title={T("push.title", "Push-уведомления")} onClose={onClose}>
      {!supported && (
        <div style={{ padding: 14, background: "var(--short-bg)", borderRadius: 8, marginBottom: 12, color: "var(--text-2)", fontSize: 13 }}>
          Браузер не поддерживает Web Push. На iOS Safari → нужно «Добавить на главный экран» сначала.
        </div>
      )}
      <div className="m-card" style={{ display: "flex", alignItems: "center", gap: 12 }}>
        <div style={{ flex: 1 }}>
          <div style={{ fontWeight: 600 }}>{T("push.notifications", "Уведомления")}</div>
          <div className="muted" style={{ fontSize: 12, marginTop: 2 }}>
            {enabled ? T("push.enabledDesc", "Включены — придут даже когда браузер закрыт") : T("push.disabled", "Выключены")}
          </div>
        </div>
        <span className={"m-switch" + (enabled ? " on" : "")} onClick={toggle}/>
      </div>

      {enabled && (
        <button className="m-button block" style={{ marginTop: 12 }} disabled={busy} onClick={test}>
          <MIcon name="bell" size={16}/> Прислать тестовое
        </button>
      )}

      <div className="sheet-label" style={{ marginTop: 18 }}>{T("push.whatComes", "Что приходит")}</div>
      <div className="m-card" style={{ fontSize: 13, lineHeight: 1.7 }}>
        <div>{T("push.item.newSignal", "📨 Новый сигнал из канала (manual-режим)")}</div>
        <div>{T("push.item.positionOpen", "✅ Открытие позиции (auto/manual)")}</div>
        <div>{T("push.item.firstTp", "🎯 Первый TP сработал → SL в БУ")}</div>
        <div>{T("push.item.positionClose", "🏁 Закрытие позиции (с PnL)")}</div>
        <div>{T("push.item.orderError", "❌ Ошибка размещения ордера")}</div>
      </div>

      <div style={{ marginTop: 14, padding: 12, background: "var(--bg-2)", borderRadius: 8, fontSize: 12, color: "var(--text-2)", lineHeight: 1.5 }}>
        💡 Чтобы пуши приходили на iPhone, сначала добавь сайт на главный экран (Safari → Поделиться → На экран «Домой»), потом включи push здесь.
      </div>
    </BottomSheet>
  );
};

// ------------------------------------------------------------
// Per-exchange signal settings sheet
// ------------------------------------------------------------
const ExchangeSignalsSheet = ({ onClose, onToast, onSaved, initialMode }) => {
  const [items, setItems] = useState([]);
  const [busy, setBusy] = useState(false);
  const [sigMode, setSigMode] = useState(initialMode || "global");

  const changeSigMode = async (m) => {
    if (busy || m === sigMode) return;
    buzz();
    setSigMode(m);
    setBusy(true);
    await _save(() => window.MobileAPI.updateSettings({ signal_settings_mode: m }), onToast,
      m === "global" ? "Настройки: глобально" : "Настройки: раздельно по биржам");
    setBusy(false);
    onSaved && onSaved();
  };

  const load = async () => {
    const rows = await window.MobileAPI.loadExchangeSignals();
    // normalize to всех бирж
    const supported = window.MobileAPI.SUPPORTED_EXCHANGES;
    const byEx = {};
    for (const r of (rows || [])) byEx[(r.exchange || "").toLowerCase()] = r;
    const full = supported.map(e => ({
      exchange: e,
      signal_enabled: byEx[e]?.signal_enabled ?? false,
      leverage_mode: byEx[e]?.leverage_mode || "fixed",
      default_leverage: byEx[e]?.default_leverage || 10,
    }));
    setItems(full);
  };
  useEffect(() => { load(); }, []);

  const update = async (exchange, fields) => {
    if (busy) return;
    setBusy(true);
    const ok = await _save(
      () => window.MobileAPI.updateExchangeSignal(exchange, fields),
      onToast,
      `${EX_LABELS[exchange] || exchange}: сохранено`,
    );
    setBusy(false);
    if (ok) { await load(); onSaved && onSaved(); }
  };

  return (
    <BottomSheet title={T("ex.signalsByExchange", "Сигналы по биржам")} onClose={onClose}>
      <div className="sheet-label">{T("ex.settingsMode", "Настройки размера/плеча")}</div>
      <div className="m-segmented" style={{ marginBottom: 8 }}>
        <button className={sigMode === "global" ? "active" : ""} disabled={busy}
          onClick={() => changeSigMode("global")}>{T("ex.modeGlobal", "Глобально")}</button>
        <button className={sigMode === "per_exchange" ? "active" : ""} disabled={busy}
          onClick={() => changeSigMode("per_exchange")}>{T("ex.modePerEx", "Раздельно")}</button>
      </div>
      <div style={{ fontSize: 12, color: "var(--text-3)", marginBottom: 14, lineHeight: 1.5 }}>
        {sigMode === "global"
          ? T("ex.modeGlobalDesc", "Все биржи используют общие размер/плечо из раздела «Торговля».")
          : T("ex.modePerExDesc", "У каждой биржи своё плечо ниже. Размер берётся из общих настроек.")}
      </div>
      {items.map(it => (
        <div key={it.exchange} className="m-card" style={{ marginBottom: 10 }}>
          <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
            <span className="si" style={{ width: 30, height: 30, borderRadius: 8, display: "grid", placeItems: "center", background: "var(--bg-2)" }}>
              <MIcon name="key" size={15}/>
            </span>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 14, fontWeight: 600 }}>{EX_LABELS[it.exchange] || it.exchange.toUpperCase()}</div>
              <div className="muted" style={{ fontSize: 11, marginTop: 2 }}>
                {it.signal_enabled ? T("signals.enabled", "Сигналы включены") : T("signals.disabled", "Сигналы отключены")}
              </div>
            </div>
            <span className={"m-switch" + (it.signal_enabled ? " on" : "")}
              onClick={() => { buzz(); update(it.exchange, { signal_enabled: !it.signal_enabled }); }}/>
          </div>
          {it.signal_enabled && sigMode === "per_exchange" && (
            <div style={{ marginTop: 12, paddingTop: 12, borderTop: "1px solid var(--border)" }}>
              <div className="muted" style={{ fontSize: 11, marginBottom: 6 }}>{T("lev.title", "Плечо")}</div>
              <div className="m-segmented" style={{ marginBottom: 8 }}>
                <button className={it.leverage_mode === "fixed" ? "active" : ""}
                  onClick={() => { buzz(); update(it.exchange, { leverage_mode: "fixed" }); }}>Fixed</button>
                <button className={it.leverage_mode === "from_signal" ? "active" : ""}
                  onClick={() => { buzz(); update(it.exchange, { leverage_mode: "from_signal" }); }}>Signal</button>
                <button className={it.leverage_mode === "max" ? "active" : ""}
                  onClick={() => { buzz(); update(it.exchange, { leverage_mode: "max" }); }}>Max</button>
              </div>
              {it.leverage_mode !== "max" && (
                <div style={{ display: "flex", gap: 6, alignItems: "center" }}>
                  <input className="m-input" inputMode="numeric"
                    style={{ height: 38, fontSize: 14, flex: 1 }}
                    value={it.default_leverage}
                    onChange={e => setItems(prev => prev.map(p =>
                      p.exchange === it.exchange ? { ...p, default_leverage: parseInt(e.target.value) || 1 } : p))}/>
                  <button className="m-button sm"
                    onClick={() => update(it.exchange, { default_leverage: parseInt(it.default_leverage) || 10 })}
                    disabled={busy}>OK</button>
                </div>
              )}
            </div>
          )}
        </div>
      ))}
    </BottomSheet>
  );
};

// ------------------------------------------------------------
// Channels sheet — список + добавление + удаление
// ------------------------------------------------------------
const ChannelsSheet = ({ onClose, onToast, onSaved }) => {
  const [items, setItems] = useState([]);
  const [ref, setRef] = useState("");
  const [label, setLabel] = useState("");
  const [busy, setBusy] = useState(false);

  const load = async () => {
    const list = await window.MobileAPI.listChannels();
    setItems(list || []);
  };
  useEffect(() => { load(); }, []);

  const add = async () => {
    if (busy) return;
    if (!ref) { onToast({ msg: T("channel.enterRef", "Укажи @username или ID"), err: true }); return; }
    setBusy(true);
    const ok = await _save(
      () => window.MobileAPI.addChannel(ref, label),
      onToast, `Канал ${ref} добавлен`);
    setBusy(false);
    if (ok) { setRef(""); setLabel(""); await load(); onSaved && onSaved(); }
  };

  const remove = async (c) => {
    if (busy) return;
    if (!confirm(`Удалить канал ${c.label || c.ref}?`)) return;
    setBusy(true);
    const ok = await _save(
      () => window.MobileAPI.removeChannel(c.id),
      onToast, T("channel.removed", "Канал удалён"));
    setBusy(false);
    if (ok) { await load(); onSaved && onSaved(); }
  };

  return (
    <BottomSheet title={T("channel.sources", "Каналы-источники")} onClose={onClose}>
      <div className="sheet-label">{T("channel.title", "Канал")}</div>
      <input className="m-input" value={ref} onChange={e => setRef(e.target.value)}
        placeholder={T("channel.refPlaceholder", "@username, username или -100…")}/>
      <div className="sheet-label">{T("channel.descOptional", "Описание (необязательно)")}</div>
      <input className="m-input" value={label} onChange={e => setLabel(e.target.value)}
        placeholder={T("channel.descPlaceholder", "напр. «основной сигналист»")}/>
      <button className="m-button primary lg block" style={{ marginTop: 12 }} disabled={busy} onClick={add}>
        <MIcon name="plus" size={16}/> Добавить
      </button>

      <div className="sheet-label" style={{ marginTop: 22 }}>Подключённые ({items.length})</div>
      {items.length === 0 && (
        <div style={{ padding: 14, color: "var(--text-3)", fontSize: 13, textAlign: "center" }}>
          Пока нет каналов
        </div>
      )}
      {items.map(c => <ChannelCardEditable key={c.id} c={c} busy={busy} onChanged={load} onRemove={() => remove({ id: c.id, ref: c.channel_ref, label: c.label })} onToast={onToast}/>)}
    </BottomSheet>
  );
};

// Карточка канала с inline-настройками (entry_type + default_sl_pct)
const ChannelCardEditable = ({ c, busy, onChanged, onRemove, onToast }) => {
  const [open, setOpen] = useState(false);
  const [et, setEt] = useState(c.default_entry_type || "signal");
  const [sl, setSl] = useState(String(c.default_sl_pct || 0));
  const [saving, setSaving] = useState(false);
  const save = async () => {
    if (saving) return;
    setSaving(true);
    try {
      const slNum = Math.max(0, Math.min(50, parseFloat(sl) || 0));
      await window.MobileAPI.updateChannel(c.id, {
        default_entry_type: et,
        default_sl_pct: slNum,
      });
      onToast && onToast({ msg: T("channels.saved", "Сохранено") });
      setOpen(false);
      if (onChanged) await onChanged();
    } catch (e) {
      onToast && onToast({ msg: T("common.error", "Ошибка") + ": " + (e.message || e).slice(0, 60), err: true });
    } finally { setSaving(false); }
  };
  const entryLabel = { signal: T("ch.et.signal", "как в сигнале"), market: T("ch.et.market", "всегда market"), limit: T("ch.et.limit", "всегда limit") }[c.default_entry_type || "signal"];
  return (
    <div className="m-card" style={{ marginBottom: 8 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 10, padding: 10 }}>
        <span className="si" style={{ width: 28, height: 28, borderRadius: 8, display: "grid", placeItems: "center", background: "var(--bg-2)", color: "var(--text-2)" }}>
          <MIcon name="signal" size={15}/>
        </span>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div className="mono" style={{ fontSize: 13, fontWeight: 600, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{c.label || c.channel_ref}</div>
          <div className="mono" style={{ fontSize: 10, color: "var(--text-3)" }}>
            {c.channel_ref}
            {" · "}
            <span style={{ color: "var(--text-2)" }}>{entryLabel}</span>
            {c.default_sl_pct > 0 && <span style={{ color: "var(--text-2)" }}> · SL {c.default_sl_pct}%</span>}
          </div>
        </div>
        <button className="m-button ghost sm" disabled={busy} onClick={() => setOpen(o => !o)}>
          <MIcon name="settings" size={14}/>
        </button>
        <button className="m-button danger sm" disabled={busy} onClick={onRemove}>
          <MIcon name="x" size={14}/>
        </button>
      </div>
      {open && (
        <div style={{ padding: "0 10px 12px" }}>
          <div className="sheet-label">{T("channels.entry", "Тип входа в сигнал")}</div>
          <div className="m-segmented">
            <button className={et === "signal" ? "active" : ""} onClick={() => { buzz(); setEt("signal"); }}>{T("ch.et.signal", "Как канал")}</button>
            <button className={et === "market" ? "active" : ""} onClick={() => { buzz(); setEt("market"); }}>{T("ch.et.market", "Market")}</button>
            <button className={et === "limit" ? "active" : ""} onClick={() => { buzz(); setEt("limit"); }}>{T("ch.et.limit", "Limit")}</button>
          </div>
          <div className="sheet-label" style={{ marginTop: 8 }}>{T("channels.defaultSl", "Дефолтный SL % (если в сигнале нет)")}</div>
          <input className="m-input" inputMode="decimal" value={sl}
            onChange={e => setSl(e.target.value)} placeholder="0 = не применять"/>
          <div className="pct-grid" style={{ marginTop: 4 }}>
            {["0", "3", "5", "8", "12"].map(v => (
              <button key={v} className={"pct-btn" + (sl === v ? " active" : "")}
                onClick={() => { buzz(); setSl(v); }}>{v}%</button>
            ))}
          </div>
          <div style={{ fontSize: 10, color: "var(--text-3)", margin: "6px 0 8px", lineHeight: 1.4 }}>
            {T("channels.defaultSlHint", "Если сигнал из этого канала пришёл без SL — бот поставит SL на этом %% от entry. 0 = пропустит сигнал.")}
          </div>
          <button className="m-button block" disabled={saving} onClick={save}>
            {saving ? T("common.saving", "Сохраняю…") : T("common.save", "Сохранить")}
          </button>
        </div>
      )}
    </div>
  );
};

// ------------------------------------------------------------
// Confirm "close all" sheet
// ------------------------------------------------------------
const CloseAllSheet = ({ positions, onClose, onConfirm }) => {
  const [busy, setBusy] = useState(false);
  const total = positions.reduce((s, p) => s + p.pnl, 0);
  const submit = async () => {
    if (busy) return;
    setBusy(true); buzz(20);
    try { await onConfirm(); } finally { setBusy(false); }
  };
  const foot = (
    <>
      <button className="m-button ghost lg" disabled={busy} onClick={() => { buzz(); onClose(); }}>{T("common.cancel", "Отмена")}</button>
      <button className="m-button short lg" disabled={busy} onClick={submit}>
        {busy ? T("common.closing", "Закрываем…") : (<><MIcon name="briefcase" size={18}/> Закрыть всё</>)}
      </button>
    </>
  );
  return (
    <BottomSheet title={T("portfolio.closeAllPositions", "Закрыть все позиции")} onClose={onClose} foot={foot}>
      <div style={{ fontSize: 14, color: "var(--text-2)", marginBottom: 14 }}>
        Будет закрыто <b style={{ color: "var(--text)" }}>{positions.length} позиц.</b> по market. Действие необратимо.
      </div>
      {positions.map(p => (
        <div key={p.id} style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 0", borderBottom: "1px solid var(--border)" }}>
          <Coin sym={p.coin} size="sm"/>
          <span className="mono" style={{ fontWeight: 600 }}>{p.sym}</span>
          <Side side={p.side}/>
          <span className={"mono " + (p.pnl >= 0 ? "pos" : "neg")} style={{ marginLeft: "auto", fontWeight: 700 }}>{signed(p.pnl)}</span>
        </div>
      ))}
      <div style={{ display: "flex", justifyContent: "space-between", marginTop: 14, fontSize: 15 }}>
        <span style={{ color: "var(--text-2)" }}>{T("portfolio.totalPnl", "Суммарный PnL")}</span>
        <span className={"mono " + (total >= 0 ? "pos" : "neg")} style={{ fontWeight: 700, fontSize: 18 }}>{signed(total)} USDT</span>
      </div>
    </BottomSheet>
  );
};

// ============================================================
// Root: MobileApp
// ============================================================
const MobileApp = () => {
  // Подписка на смену языка — форсит re-render всего дерева когда юзер
  // переключает RU↔EN. Без этого M_I18N.setLocale() меняет словарь, но
  // компоненты остаются с прежними строками до ручного refresh.
  window.useLocale && window.useLocale();

  const [data, setData] = useState(window.M_DATA);
  const [tab, setTab] = useState(() => (location.hash || "#home").slice(1));
  // Слушаем hashchange — позволяет внешним shell'ам (DesktopMobileShell)
  // менять активную вкладку через `location.hash = "#signals"`.
  React.useEffect(() => {
    const onHash = () => {
      const t = (location.hash || "#home").slice(1).split("?")[0] || "home";
      if (t !== tab) setTab(t);
    };
    window.addEventListener("hashchange", onHash);
    return () => window.removeEventListener("hashchange", onHash);
  }, [tab]);
  // Режим главной: futures (по умолчанию) / spot / bots. Сохраняется в localStorage
  // отдельно от tab — переключается через ModeSwitcher только на главной.
  const [mode, setMode] = useState(() => {
    try { return localStorage.getItem("m_mode") || "futures"; } catch (e) { return "futures"; }
  });
  const changeMode = (m) => {
    setMode(m);
    try { localStorage.setItem("m_mode", m); } catch (e) {}
  };
  const [positions, setPositions] = useState([]);
  // refreshTick для локальных ботов — инкрементится при create/delete/start/stop
  // → BotsScreen useEffect-ы перерисовывают локальный список.
  const [localBotsTick, setLocalBotsTick] = useState(0);
  const [signals, setSignals] = useState([]);
  const [sheet, setSheet] = useState(null);
  const [toast, setToast] = useState(null);
  const [loading, setLoading] = useState(true);
  const undoRef = useRef(null);

  // ----- refresher -----
  // Сначала тянем settings → знаем активную биржу → параллельно
  // запрашиваем balance/positions/journal с этой биржи, чтобы не было
  // race-condition «положили creds окх но баланс пришёл от weex».
  const refresh = useCallback(async (opts = {}) => {
    if (!window.MobileAPI) return;
    const sett = await window.MobileAPI.loadSettings();
    const ex = (sett.exchange || "weex").toLowerCase();
    const [balance, poss, journal, sigs, plan] = await Promise.all([
      window.MobileAPI.loadBalance(ex),
      window.MobileAPI.loadPositions(ex),
      window.MobileAPI.loadJournal(ex),
      window.MobileAPI.loadSignals(),
      window.MobileAPI.loadPlan(),
    ]);
    setData(prev => ({ ...prev, balance, positions: poss, journal, signals: sigs, settings: sett, plan }));
    setPositions(poss);
    setSignals(sigs);
    if (!opts.silent) setLoading(false);
  }, []);

  useEffect(() => {
    refresh();
    const id = setInterval(() => refresh({ silent: true }), 20000);
    // Применяем тему/локаль из settings когда данные загрузились
    const applyPref = (s) => {
      if (s?.theme && window.M_THEME && window.M_THEME.getTheme() !== s.theme) {
        window.M_THEME.setTheme(s.theme);
      }
      if (s?.locale && window.M_I18N && window.M_I18N.getLocale() !== s.locale) {
        window.M_I18N.setLocale(s.locale);
      }
    };
    const watchPref = (prev) => prev.settings && applyPref(prev.settings);
    setData(prev => { watchPref(prev); return prev; });
    // Регистрируем service worker для PWA + push (не запрашиваем permission —
    // юзер сам включит в Settings → Push).
    if ("serviceWorker" in navigator) {
      navigator.serviceWorker.register("/sw.js", { scope: "/" }).catch(() => {});
    }
    return () => clearInterval(id);
  }, [refresh]);

  // hash sync
  useEffect(() => {
    const h = () => setTab((location.hash || "#home").slice(1));
    window.addEventListener("hashchange", h);
    return () => window.removeEventListener("hashchange", h);
  }, []);
  const go = (t) => { location.hash = t; setTab(t); };

  // ----- handlers: positions -----
  const closePos = (p) => setSheet({ kind: "close", payload: p });
  const editPos = (p) => setSheet({ kind: "edit", payload: p });

  const confirmClose = async (p, pctSel, type, limitPrice, qty) => {
    try {
      await window.MobileAPI.closePositionAction(p.sym, p.side, qty, type, limitPrice);
      setSheet(null);
      setToast({ msg: `${p.sym} ${pctSel}% → ${type}` });
      await refresh({ silent: true });
    } catch (e) {
      setToast({ msg: "Ошибка: " + (e.message || e).slice(0, 80), err: true });
    }
  };

  const triggerCloseAll = () => {
    if (!positions.length) { setToast({ msg: T("portfolio.noOpen", "Нет открытых позиций"), err: true }); return; }
    setSheet({ kind: "closeAll" });
  };

  const confirmCloseAll = async () => {
    const failed = [];
    for (const p of positions) {
      try {
        await window.MobileAPI.closePositionAction(p.sym, p.side, p.qty, "market");
      } catch (e) {
        failed.push(p.sym);
      }
    }
    setSheet(null);
    if (failed.length === 0) setToast({ msg: T("portfolio.allClosed", "Все позиции закрыты по market") });
    else setToast({ msg: `Закрыто частично, не получилось: ${failed.join(", ")}`, err: true });
    await refresh({ silent: true });
  };

  // ----- handlers: pending-signals — реальное подтверждение/отмена через /api/trades/{id}/confirm|cancel
  const approveSignal = async (s) => {
    if (!s.tradeId) { setToast({ msg: T("error.noTradeId", "Нет trade_id"), err: true }); return; }
    setSignals(prev => prev.filter(x => x.id !== s.id));
    buzz(15);
    try {
      const r = await window.MobileAPI.confirmPending(s.tradeId);
      setToast({ msg: `${s.sym} ${s.side} открыт` + (r.summary ? "" : "") });
    } catch (e) {
      setToast({ msg: "Не открылась: " + (e.message || e).slice(0, 80), err: true });
    }
    await refresh({ silent: true });
  };
  const rejectSignal = async (s) => {
    if (!s.tradeId) { setToast({ msg: T("error.noTradeId", "Нет trade_id"), err: true }); return; }
    setSignals(prev => prev.filter(x => x.id !== s.id));
    try {
      await window.MobileAPI.cancelPending(s.tradeId);
      setToast({ msg: `Сигнал ${s.sym} пропущен` });
    } catch (e) {
      setToast({ msg: "Ошибка отмены: " + (e.message || e).slice(0, 80), err: true });
    }
    await refresh({ silent: true });
  };
  const clearAllSignals = async () => {
    const n = signals.length;
    if (!n) return;
    if (!confirm(`Отменить все ${n} ожидающих сигналов?`)) return;
    setSignals([]);
    buzz(15);
    try {
      const r = await window.MobileAPI.cancelAllPending();
      setToast({ msg: `Очередь очищена · отменено ${r.cancelled || 0}` });
    } catch (e) {
      setToast({ msg: "Не удалось очистить: " + (e.message || e).slice(0, 60), err: true });
    }
    await refresh({ silent: true });
  };

  // ----- handlers: settings -----
  const onChangeMode = async (newMode) => {
    setData(prev => ({ ...prev, settings: { ...prev.settings, mode: newMode } }));
    try {
      await window.MobileAPI.updateMode(newMode);
      setToast({ msg: `Режим: ${newMode === "auto" ? "Auto" : "Manual"}` });
    } catch (e) {
      setToast({ msg: T("error.saveMode", "Не удалось сохранить режим"), err: true });
      await refresh({ silent: true });
    }
  };

  const onLogout = async () => {
    await window.MobileAPI.logout();
    location.reload();
  };

  // ----- render -----
  let screen, topBar, fab = null;
  if (tab === "home") {
    const ex = (data.settings.exchange || "weex").toLowerCase();
    topBar = (
      <>
        <TopBar online notif={signals.length} onBell={() => go("signals")}
          exchange={ex.toUpperCase()}/>
        <ModeSwitcher mode={mode} onChange={changeMode} exchange={ex}/>
      </>
    );
    if (loading && mode === "futures") {
      screen = <div className="m-scroll"><div className="m-hero"><div className="skel" style={{ width: 90, height: 12 }}/><div className="skel" style={{ width: 180, height: 30, marginTop: 12 }}/></div><div style={{ height: 10 }}/><SkelCard/><SkelCard/></div>;
    } else if (mode === "spot") {
      screen = <SpotScreen exchange={ex} onToast={setToast}/>;
    } else if (mode === "bots") {
      screen = <BotsScreen exchange={ex} onToast={setToast}
        refreshTick={localBotsTick}
        onSelectBot={(b) => setSheet({ kind: "botDetails", payload: b })}
        onCreateLocal={() => setSheet({ kind: "localBotCreate" })}
        onSelectLocalBot={(b) => setSheet({ kind: "localBotDetails", payload: b })}/>;
    } else {
      screen = <DashboardScreen data={data} positions={positions}
        onClosePos={closePos} onEditPos={editPos} exchange={ex}
        onRefresh={() => refresh({ silent: true })}/>;
    }
    // FAB: для фьючерсов «закрыть всё», для остальных режимов — Шторм
    if (mode === "futures") {
      fab = <Fab onTrigger={triggerCloseAll} onLongPress={() => setSheet({ kind: "storm" })}/>;
    } else if (mode === "spot" || mode === "bots") {
      // Маленький Storm-FAB. Используем Fab с onTrigger.
      fab = (
        <button
          className="m-fab"
          onClick={() => { buzz(20); setSheet({ kind: "storm" }); }}
          style={{
            position: "absolute", right: 16, bottom: "calc(var(--nav-h) + var(--safe-bottom) + 16px)",
            zIndex: 30, width: 56, height: 56, borderRadius: 28,
            border: "none", background: "var(--short)", color: "#fff",
            fontSize: 24, fontWeight: 700, boxShadow: "0 6px 18px rgba(239, 68, 68, 0.35)",
          }}>🛑</button>
      );
    }
  } else if (tab === "portfolio") {
    topBar = <TopBar variant="back" title={T("topbar.portfolio", "Портфель")} onBack={() => go("settings")}/>;
    screen = <PortfolioScreen exchange={(data.settings.exchange || "weex").toLowerCase()} onToast={setToast}/>;
  } else if (tab === "journal") {
    topBar = <TopBar variant="back" title={T("topbar.journal", "Журнал сделок")} onBack={() => go("home")}/>;
    screen = <JournalScreen data={data}
      onRefresh={() => refresh({ silent: true })}
      onToast={setToast}
      onCustomRange={async (range) => {
        const sett = await window.MobileAPI.loadSettings();
        const ex = (sett.exchange || "weex").toLowerCase();
        const from = range.from ? new Date(range.from + "T00:00:00Z").toISOString() : undefined;
        const to = range.to ? new Date(range.to + "T23:59:59Z").toISOString() : undefined;
        const journal = await window.MobileAPI.loadJournal(ex, { dateFrom: from, dateTo: to });
        setData(prev => ({ ...prev, journal }));
      }}/>;
  } else if (tab === "signals") {
    topBar = <TopBar variant="back" title={`Сигналы${signals.length ? ` (${signals.length} ожидают)` : ""}`} onBack={() => go("home")}/>;
    screen = <SignalsScreen signals={signals} onApprove={approveSignal} onReject={rejectSignal}
      onClearAll={clearAllSignals}
      balance={data.balance} settings={data.settings}/>;
  } else {
    topBar = <TopBar variant="title" title={T("topbar.settings", "Настройки")}/>;
    screen = <SettingsScreen data={data}
      onConnectTg={() => setSheet({ kind: "tg" })}
      onChangePwd={() => setSheet({ kind: "pwd" })}
      onLogout={onLogout}
      onChangeMode={onChangeMode}
      onChangeAutoConf={async (c) => {
        setData(prev => ({ ...prev, settings: { ...prev.settings, autoMinConfidence: c } }));
        await _save(() => window.MobileAPI.updateSettings({ auto_min_confidence: c }), setToast,
          c === "high" ? "Порог: только high" : "Порог: high + medium");
        refresh({ silent: true });
      }}
      onChangeDryRun={async (v) => {
        setData(prev => ({ ...prev, settings: { ...prev.settings, dryRun: v } }));
        await _save(() => window.MobileAPI.updateSettings({ dry_run: v }), setToast,
          v ? T("toast.dryRunOn", "DRY-RUN включён") : T("toast.dryRunOff", "DRY-RUN отключён"));
        refresh({ silent: true });
      }}
      onOpenLeverage={() => setSheet({ kind: "leverage" })}
      onOpenSize={() => setSheet({ kind: "size" })}
      onOpenExchange={() => setSheet({ kind: "exchange" })}
      onOpenApiCreds={() => setSheet({ kind: "apiCreds" })}
      onOpenBotToken={() => setSheet({ kind: "botToken" })}
      onOpenChannels={() => setSheet({ kind: "channels" })}
      onOpenExchangeSignals={() => setSheet({ kind: "exchangeSignals" })}
      onOpenStats={() => setSheet({ kind: "stats" })}
      onOpenPush={() => setSheet({ kind: "push" })}
      onOpenRiskGuards={() => setSheet({ kind: "riskGuards" })}
      onOpenPortfolio={() => { go("portfolio"); }}
      onOpenForwards={() => {
        // Forwards = premium feature. У free/pro показываем upgrade-sheet.
        const tier = data.plan?.tier || "free";
        if (data.plan?.features?.forwards) {
          setSheet({ kind: "forwards" });
        } else {
          setSheet({ kind: "upgrade", payload: { feature: "forwards", currentTier: tier } });
        }
      }}
      plan={data.plan}
      onOpenStorm={() => setSheet({ kind: "storm" })}
      onChangeTheme={async (t) => {
        setData(prev => ({ ...prev, settings: { ...prev.settings, theme: t } }));
        await _save(() => window.MobileAPI.updateSettings({ theme: t }), setToast, "");
      }}
      onChangeLocale={async (l) => {
        setData(prev => ({ ...prev, settings: { ...prev.settings, locale: l } }));
        await _save(() => window.MobileAPI.updateSettings({ locale: l }), setToast, "");
      }}
      onToast={setToast}/>;
  }

  return (
    <div className="m-app">
      {topBar}
      {screen}
      {fab}
      <BottomNav active={tab} onChange={go} badges={{ signals: signals.length }}/>

      {sheet?.kind === "close" && <ClosePositionSheet p={sheet.payload} onClose={() => setSheet(null)} onConfirm={confirmClose}/>}
      {sheet?.kind === "edit" && <EditTradeSheet p={sheet.payload}
        onClose={() => setSheet(null)} onToast={setToast}
        onSaved={() => refresh({ silent: true })}/>}
      {sheet?.kind === "tg" && <TgConnectSheet onClose={() => setSheet(null)} onToast={setToast}
        onConnected={() => refresh({ silent: true })}/>}
      {sheet?.kind === "pwd" && <ChangePasswordSheet onClose={() => setSheet(null)} onToast={setToast}/>}
      {sheet?.kind === "leverage" && <LeverageSheet
        initial={{ ...data.settings, mode: data.settings.levMode, value: data.settings.leverage }}
        onClose={() => setSheet(null)} onToast={setToast} onSaved={() => refresh({ silent: true })}/>}
      {sheet?.kind === "size" && <SizeSheet
        initial={data.settings}
        onClose={() => setSheet(null)} onToast={setToast} onSaved={() => refresh({ silent: true })}/>}
      {sheet?.kind === "exchange" && <ExchangeSheet
        initial={data.settings.exchange}
        onClose={() => setSheet(null)} onToast={setToast} onSaved={() => refresh({ silent: true })}/>}
      {sheet?.kind === "apiCreds" && <ApiCredsSheet
        exchange={data.settings.exchange}
        currentMask={data.settings.apiKeyMask}
        activeLabel={data.settings.activeAccountLabel || "default"}
        onClose={() => setSheet(null)} onToast={setToast} onSaved={() => refresh({ silent: true })}/>}
      {sheet?.kind === "botToken" && <BotTokenSheet
        currentUsername={data.settings.confirmBot}
        onClose={() => setSheet(null)} onToast={setToast} onSaved={() => refresh({ silent: true })}/>}
      {sheet?.kind === "channels" && <ChannelsSheet
        onClose={() => setSheet(null)} onToast={setToast} onSaved={() => refresh({ silent: true })}/>}
      {sheet?.kind === "exchangeSignals" && <ExchangeSignalsSheet
        initialMode={data.settings.signalSettingsMode || "global"}
        onClose={() => setSheet(null)} onToast={setToast} onSaved={() => refresh({ silent: true })}/>}
      {sheet?.kind === "stats" && <StatsSheet onClose={() => setSheet(null)} onToast={setToast}/>}
      {sheet?.kind === "push" && <PushSheet onClose={() => setSheet(null)} onToast={setToast}/>}
      {sheet?.kind === "botDetails" && <BotDetailsSheet
        bot={sheet.payload}
        exchange={(data.settings.exchange || "okx").toLowerCase()}
        onClose={() => setSheet(null)} onToast={setToast}/>}
      {sheet?.kind === "storm" && <StormSheet
        stormUntilMs={parseInt(data.settings.stormUntilMs) || 0}
        onClose={() => setSheet(null)} onToast={setToast}
        onApplied={() => refresh({ silent: true })}/>}
      {sheet?.kind === "localBotCreate" && <LocalBotCreateSheet
        exchange={(data.settings.exchange || "okx").toLowerCase()}
        onClose={() => setSheet(null)} onToast={setToast}
        onCreated={() => { refresh({ silent: true }); setLocalBotsTick(t => t + 1); }}/>}
      {sheet?.kind === "localBotDetails" && <LocalBotDetailsSheet
        botId={sheet.payload?.id}
        onClose={() => setSheet(null)} onToast={setToast}
        onChanged={() => refresh({ silent: true })}/>}
      {sheet?.kind === "forwards" && <ForwardsSheet
        onClose={() => setSheet(null)} onToast={setToast}/>}
      {sheet?.kind === "upgrade" && <UpgradeSheet
        feature={sheet.payload?.feature}
        currentTier={sheet.payload?.currentTier || "free"}
        onClose={() => setSheet(null)}/>}
      {sheet?.kind === "riskGuards" && <RiskGuardsSheet
        initial={data.settings}
        onClose={() => setSheet(null)} onToast={setToast} onSaved={() => refresh({ silent: true })}/>}
      {sheet?.kind === "closeAll" && <CloseAllSheet positions={positions} onClose={() => setSheet(null)} onConfirm={confirmCloseAll}/>}

      {toast && <Toast msg={toast.msg} action={toast.action} onAction={toast.onAction} err={toast.err} onDone={() => setToast(null)}/>}
    </div>
  );
};

window.MobileApp = MobileApp;
// Сигнал для MobileGate в app.jsx: я готов, можно рендерить меня.
// (Без события app.jsx показывал бы десктоп-Root при первом заходе пока
//  Babel асинхронно компилировал mobile-app.jsx).
window.dispatchEvent(new CustomEvent("mobile-app-ready"));
