(function (global) {
  "use strict";

  /* ---------------------------------------------------------------- helpers */

  function number(value) {
    const parsed = Number(value);
    return Number.isFinite(parsed) ? parsed : 0;
  }

  function escapeHtml(value) {
    return String(value == null ? "" : value).replace(/[&<>"']/g, function (ch) {
      return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[ch];
    });
  }

  function formatNumber(value, digits) {
    return new Intl.NumberFormat("zh-CN", {
      minimumFractionDigits: digits,
      maximumFractionDigits: digits,
    }).format(number(value));
  }

  function changeClass(value) {
    const n = number(value);
    return n > 0 ? "up" : n < 0 ? "down" : "";
  }

  function streakLabel(streak) {
    const value = Math.max(1, number(streak));
    return value === 1 ? "首板" : value + "板";
  }

  function displayCompactDate(value) {
    const text = String(value || "").replaceAll("-", "");
    if (text.length !== 8) return value || "--";
    return text.slice(0, 4) + "-" + text.slice(4, 6) + "-" + text.slice(6, 8);
  }

  function localDateString(value) {
    const year = value.getFullYear();
    const month = String(value.getMonth() + 1).padStart(2, "0");
    const day = String(value.getDate()).padStart(2, "0");
    return year + "-" + month + "-" + day;
  }

  function todayString() {
    return localDateString(new Date());
  }

  function parseLocalDate(value) {
    const parts = String(value || "").split("-").map(Number);
    return new Date(parts[0], (parts[1] || 1) - 1, parts[2] || 1);
  }

  function addDays(value, delta) {
    const date = parseLocalDate(value);
    date.setDate(date.getDate() + delta);
    return localDateString(date);
  }

  function previousWeekday(value) {
    let date = parseLocalDate(value);
    date.setDate(date.getDate() - 1);
    while (date.getDay() === 0 || date.getDay() === 6) date.setDate(date.getDate() - 1);
    return localDateString(date);
  }

  const ICONS = {
    calendar: '<path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/>',
    close: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
    "chevron-left": '<path d="m15 18-6-6 6-6"/>',
    "chevron-right": '<path d="m9 18 6-6-6-6"/>',
    "chevron-down": '<path d="m6 9 6 6 6-6"/>',
    inbox: '<path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>',
    trash: '<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6"/><path d="M14 11v6"/>',
    bot: '<path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/>',
    filter: '<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/>',
    target: '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
    "refresh-cw": '<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/>',
    plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
    search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
    send: '<path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/>',
    square: '<rect width="10" height="10" x="7" y="7" rx="1"/>',
    "alert-triangle": '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
    check: '<path d="M20 6 9 17l-5-5"/>',
    "check-check": '<path d="M18 6 7 17l-5-5"/><path d="m22 10-7.5 7.5L13 16"/>',
    star: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>',
    "scroll-text": '<path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/>',
    "calendar-check": '<path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/><path d="m9 16 2 2 4-4"/>',
    "sticky-note": '<path d="M16 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8Z"/><path d="M15 3v4a2 2 0 0 0 2 2h4"/>',
    bell: '<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>',
    lock: '<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
    user: '<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
    gem: '<path d="M6 3h12l4 6-10 13L2 9Z"/><path d="M11 3 8 9l4 13 4-13-3-6"/><path d="M2 9h20"/>',
    users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
    "sliders-horizontal": '<line x1="21" x2="14" y1="4" y2="4"/><line x1="10" x2="3" y1="4" y2="4"/><line x1="21" x2="12" y1="12" y2="12"/><line x1="8" x2="3" y1="12" y2="12"/><line x1="21" x2="16" y1="20" y2="20"/><line x1="12" x2="3" y1="20" y2="20"/><line x1="14" x2="14" y1="2" y2="6"/><line x1="8" x2="8" y1="10" y2="14"/><line x1="16" x2="16" y1="18" y2="22"/>',
    sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
    moon: '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',
    "arrow-left-right": '<path d="M8 3 4 7l4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/>',
    "log-out": '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/>',
  };

  function icon(name, size) {
    const body = ICONS[name] || "";
    return '<svg width="' + (size || 16) + '" height="' + (size || 16) + '" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + body + "</svg>";
  }

  const TRIANGLE_UP = '<svg width="6" height="6" viewBox="0 0 8 8" aria-hidden="true"><path d="M4 1.6 6.9 6.4H1.1Z" fill="currentColor"/></svg>';
  const TRIANGLE_DOWN = '<svg width="6" height="6" viewBox="0 0 8 8" aria-hidden="true"><path d="M4 6.4 1.1 1.6h5.8Z" fill="currentColor"/></svg>';

  function sortIndicatorHtml(active) {
    if (active) {
      const up = state.sort.dir === "asc";
      return '<span class="m-sort" aria-hidden="true">' + (up ? TRIANGLE_UP : TRIANGLE_DOWN) + "</span>";
    }
    return '<span class="m-sort m-sort--dual" aria-hidden="true">' + TRIANGLE_UP + TRIANGLE_DOWN + "</span>";
  }

  function starIcon(filled) {
    const d = "M12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2";
    if (filled) {
      return '<svg width="22" height="22" viewBox="0 0 24 24" aria-hidden="true"><path d="' + d + '" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>';
    }
    return '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" aria-hidden="true"><path d="' + d + '"/></svg>';
  }

  /* ---------------------------------------------------------------- state */

  const state = {
    key: "",
    requestedDate: "",
    dashboard: null,
    popularity: null,
    popularitySource: "combined",
    seq: 0,
    calCursor: null,
    sort: { key: "", dir: null },
    sortTable: { cols: null, reapply: null },
    detail: null,
    sentimentHistory: null,
    sentimentRange: 20,
    rotation: null,
    rotationSelectedSector: "",
    rotationSelectedDate: "",
    rotationMembers: null,
    auction: null,
    auctionDataset: "focus",
    themes: null,
    themesDetail: null,
    themesSelectedCode: "",
    dragon: null,
    dragonProfiles: null,
    dragonViewMode: "daily",
    dragonSelectedTrader: "",
    chat: {
      page: "",
      messages: [],
      mentorId: "",
      mentorName: "",
      mentorTagline: "",
      mentorGrade: "",
      mentorFocus: [],
      mentorSetup: null,
      followUps: [],
      streaming: false,
      streamController: null,
      streamBuffer: "",
      streamBubble: null,
      loading: false,
      historyLoaded: false,
      aborter: null,
    },
    screener: {
      data: null,
      date: "",
      view: "latest",
      strategyId: "",
      strategyName: "",
      loading: false,
    },
    tracking: {
      data: null,
      loading: false,
      refreshing: false,
    },
    review: {
      subKey: "",
      watchlist: null,
      trades: null,
      notes: null,
      dailyItems: null,
      alerts: null,
      alertStatus: "all",
      form: null,
      dirty: false,
    },
    system: {
      account: null,
      admin: null,
      adminTab: "market",
      models: [],
      accounts: [],
      editingModelId: "",
      editingUserId: "",
    },
  };

  let sheetToken = 0;

  function pageConfig(key) {
    const cfg = global.MobileNav && global.MobileNav.tableColumns ? global.MobileNav.tableColumns[key] : null;
    return cfg || null;
  }

  function defaultSortForKey(key) {
    if (key === "market/limit-up") return { key: "streak", dir: "desc" };
    if (key === "market/limit-down") return { key: "streak", dir: "desc" };
    return { key: "", dir: null };
  }

  function columnsFor(cfg) {
    if (cfg.columns) return cfg.columns[state.popularitySource] || cfg.columns.combined;
    return cfg;
  }

  function orderedColumns(cols) {
    return (cols.frozenColumns || []).concat(cols.primaryColumns || [], cols.scrollColumns || []);
  }

  function findColumn(cols, key) {
    const ordered = orderedColumns(cols);
    for (let i = 0; i < ordered.length; i += 1) {
      if (ordered[i].key === key) return ordered[i];
    }
    return null;
  }

  function isNumericType(type) {
    return ["rank", "change", "price", "rate", "money", "gap", "int", "streak", "advance", "height", "move", "score"].indexOf(type) >= 0;
  }

  function sortableColumn(col) {
    return col.type !== "stock";
  }

  function resetSortIfInvalid(cols) {
    const active = state.sort.key;
    if (active && !findColumn(cols, active)) {
      state.sort = { key: "", dir: null };
    }
  }

  function sortedRows(rows, cols) {
    resetSortIfInvalid(cols);
    const key = state.sort.key;
    const dir = state.sort.dir;
    if (!key || !dir) return rows;
    const col = findColumn(cols, key);
    if (!col || !sortableColumn(col)) return rows;
    const numeric = isNumericType(col.type);
    const factor = dir === "asc" ? 1 : -1;
    return rows.slice().sort(function (a, b) {
      if (numeric) {
        const av = number(a[key]);
        const bv = number(b[key]);
        if (av === bv) return 0;
        return (av - bv) * factor;
      }
      const av = String(a[key] == null ? "" : a[key]);
      const bv = String(b[key] == null ? "" : b[key]);
      return av.localeCompare(bv, "zh-CN") * factor;
    });
  }

  function toggleSort(key) {
    const active = state.sortTable;
    if (!active || !active.cols) return;
    const col = findColumn(active.cols, key);
    if (!col || !sortableColumn(col)) return;
    const firstDir = isNumericType(col.type) ? "desc" : "asc";
    if (state.sort.key !== key) {
      state.sort = { key: key, dir: firstDir };
    } else if (state.sort.dir === firstDir) {
      state.sort = { key: key, dir: firstDir === "desc" ? "asc" : "desc" };
    } else {
      state.sort = { key: "", dir: null };
    }
    active.reapply();
  }

  /* ---------------------------------------------------------------- cells */

  function columnWidth(col) {
    if (col.width) return col.width;
    if (col.type === "rank") return 40;
    if (col.type === "stock") return 96;
    if (col.wide) return 140;
    if (col.type === "text" || col.type === "concepts" || col.type === "outcome" || col.type === "dual") return 96;
    return 72;
  }

  function colAlign(col) {
    if (col.type === "rank") return "m-align-center";
    if (["change", "price", "rate", "money", "gap", "int", "streak", "advance", "height", "move", "score"].indexOf(col.type) >= 0) return "m-align-right";
    return "m-align-left";
  }

  function stockCell(row) {
    return '<span class="m-stock">' +
      '<span class="m-stock-name">' + escapeHtml(row.name || "--") + "</span>" +
      '<span class="m-stock-code">' + escapeHtml(row.code || "") + "</span>" +
      "</span>";
  }

  function rankCell(row, index) {
    const value = number(row.rank) > 0 ? number(row.rank) : index + 1;
    const hot = index < 3 ? '<span class="m-rank-hot">热</span>' : "";
    return '<span class="m-rank' + (index < 3 ? " is-top" : "") + '"><b>' + value + "</b>" + hot + "</span>";
  }

  function streakCell(value) {
    const n = number(value);
    if (n <= 0) return "";
    const high = n >= 4 ? " is-high" : "";
    return '<span class="m-pill' + high + '">' + streakLabel(n) + "</span>";
  }

  function numCell(value, digits, signed) {
    if (value == null || value === "") return '<span class="m-num"></span>';
    const n = number(value);
    const sign = signed && n > 0 ? "+" : "";
    return '<span class="m-num">' + sign + formatNumber(n, digits) + "</span>";
  }

  function changeCell(value) {
    if (value == null || value === "") return '<span class="m-num"></span>';
    const n = number(value);
    return '<span class="m-num ' + changeClass(n) + '">' + (n > 0 ? "+" : "") + formatNumber(n, 2) + "</span>";
  }

  function intCell(value, hideZero) {
    const n = Math.round(number(value));
    if (hideZero && n === 0) return '<span class="m-num"></span>';
    if (value == null || value === "") return '<span class="m-num"></span>';
    return '<span class="m-num">' + n.toLocaleString("zh-CN") + "</span>";
  }

  function advanceCell(value) {
    const n = number(value);
    const cls = n === 0 ? "is-neutral" : n < 20 ? "is-warning" : "is-active";
    return '<span class="m-num ' + cls + '">' + formatNumber(n, 1) + "</span>";
  }

  function outcomeCell(value) {
    const map = { "晋级": "advance", "断板": "fail", "炸板": "broken", "跌停": "down" };
    const cls = map[value] || "fail";
    return '<span class="m-tag m-tag--' + cls + '">' + escapeHtml(value || "") + "</span>";
  }

  function heightCell(value) {
    const n = number(value);
    if (n <= 0) return "";
    return '<span class="m-pill m-pill--plain">' + n + "</span>";
  }

  function moveCell(value) {
    const n = value == null ? null : number(value);
    if (n == null) return '<span class="m-num is-new">新</span>';
    if (n > 0) return '<span class="m-num up">\u2191' + n + "</span>";
    if (n < 0) return '<span class="m-num down">\u2193' + Math.abs(n) + "</span>";
    return '<span class="m-num">持平</span>';
  }

  function conceptsCell(value) {
    const list = Array.isArray(value) ? value : [];
    const text = list.slice(0, 3).join("、");
    return '<span class="m-cell-text" title="' + escapeHtml(list.join("、")) + '">' + escapeHtml(text) + "</span>";
  }

  function dualCell(value) {
    const dual = Boolean(value);
    return '<span class="m-tag ' + (dual ? "m-tag--dual" : "") + '">' + (dual ? "双榜共识" : "单榜入选") + "</span>";
  }

  function textCell(value) {
    const text = String(value == null ? "" : value);
    return '<span class="m-cell-text" title="' + escapeHtml(text) + '">' + escapeHtml(text) + "</span>";
  }

  function scoreCell(value) {
    if (value == null || value === "") return '<span class="m-num"></span>';
    return '<span class="m-num">' + formatNumber(number(value), 1) + "</span>";
  }

  function expectationCell(value) {
    if (value == null || value === "") return "";
    const map = { "超预期": "above", "符合预期": "matched", "低于预期": "below", "竞价一字": "one" };
    return '<span class="m-tag m-tag--exp-' + (map[value] || "matched") + '">' + escapeHtml(value) + "</span>";
  }

  function directionCell(value) {
    if (value == null || value === "") return "";
    const map = { "买入": "up", "卖出": "down", "持平": "flat" };
    return '<span class="m-tag m-tag--dir-' + (map[value] || "flat") + '">' + escapeHtml(value) + "</span>";
  }

  function cellHtml(col, row, index) {
    const value = row[col.key];
    switch (col.type) {
      case "stock": return stockCell(row);
      case "rank": return rankCell(row, index);
      case "streak": return streakCell(value);
      case "change": return changeCell(value);
      case "price": return numCell(value, 2);
      case "rate": return numCell(value, 2);
      case "money": return numCell(value, 2);
      case "gap": return numCell(value, 2);
      case "int": return intCell(value, col.hideZero);
      case "advance": return advanceCell(value);
      case "outcome": return outcomeCell(value);
      case "height": return heightCell(value);
      case "move": return moveCell(value);
      case "concepts": return conceptsCell(value);
      case "dual": return dualCell(value);
      case "score": return scoreCell(value);
      case "expectation": return expectationCell(value);
      case "direction": return directionCell(value);
      case "text":
      default: return textCell(value);
    }
  }

  /* ---------------------------------------------------------------- table */

  function buildTable(cols, rows, opts) {
    const frozen = cols.frozenColumns || [];
    const primary = cols.primaryColumns || [];
    const scroll = cols.scrollColumns || [];
    const ordered = frozen.concat(primary, scroll);

    let left = 0;
    const frozenLeft = frozen.map(function (col) {
      const offset = left;
      left += columnWidth(col);
      return offset;
    });

    function cellOpen(col, index, isHead, extraCls, extraAttrs) {
      const isFrozen = index < frozen.length;
      const width = columnWidth(col);
      const clamp = col.type === "text" || col.type === "concepts" ? ";max-width:" + width + "px" : "";
      const style = "min-width:" + width + "px" + clamp + (isFrozen ? ";width:" + width + "px;left:" + frozenLeft[index] + "px" : "");
      const cls = (isHead ? "m-th" : "m-td") + " " + colAlign(col) + (isFrozen ? " m-frozen" : "") + (extraCls ? " " + extraCls : "");
      return '<' + (isHead ? "th" : "td") + ' class="' + cls + '" style="' + style + '"' + (extraAttrs || "") + '>';
    }

    const head = ordered.map(function (col, i) {
      const sortable = !(opts && opts.noSort) && sortableColumn(col);
      let extraCls = "";
      let extraAttrs = "";
      let indicator = "";
      if (sortable) {
        const active = state.sort.key === col.key && Boolean(state.sort.dir);
        extraCls = " m-sortable" + (active ? " is-sorted" : "");
        extraAttrs = ' data-sort-key="' + escapeHtml(col.key) + '" aria-sort="' + (active ? (state.sort.dir === "asc" ? "ascending" : "descending") : "none") + '"';
        indicator = sortIndicatorHtml(active);
      }
      return cellOpen(col, i, true, extraCls, extraAttrs) + '<span class="m-th-inner">' + escapeHtml(col.label) + indicator + "</span></th>";
    }).join("");

    const body = rows.map(function (row, rIndex) {
      const cells = ordered.map(function (col, i) {
        return cellOpen(col, i, false) + cellHtml(col, row, rIndex) + "</td>";
      }).join("");
      const linkable = !(opts && opts.noLink);
      const code = linkable && /^\d{6}$/.test(String(row.code || "")) ? ' data-code="' + escapeHtml(row.code) + '"' : "";
      return "<tr" + code + ">" + cells + "</tr>";
    }).join("");

    return '<table class="m-table"><thead><tr>' + head + "</tr></thead><tbody>" + body + "</tbody></table>";
  }

  function skeletonHtml(rowCount) {
    const rows = [];
    for (let i = 0; i < rowCount; i += 1) {
      rows.push('<div class="m-skeleton-row"><span class="m-skeleton-bar"></span></div>');
    }
    return '<div class="m-skeleton" aria-hidden="true">' + rows.join("") + "</div>";
  }

  function emptyHtml() {
    return '<div class="m-state m-motion-rise-in">' +
      '<span class="m-state-icon">' + icon("inbox", 26) + "</span>" +
      "<p>该交易日暂无相关数据</p>" +
      '<small>可点右上角日期切换交易日</small>' +
      "</div>";
  }

  function errorHtml(message) {
    return '<div class="m-state m-state--error m-motion-fade-in">' +
      '<p>' + escapeHtml(message || "数据加载失败") + "</p>" +
      '<button class="m-btn-primary m-btn-retry" type="button" data-action="retry">重试</button>' +
      "</div>";
  }

  /* ---------------------------------------------------------------- strip */

  function stripSkeleton() {
    const cells = [];
    for (let i = 0; i < 7; i += 1) {
      cells.push('<div class="m-strip-cell"><span class="m-strip-skeleton-bar"></span></div>');
    }
    return '<div class="m-strip m-strip--skeleton" aria-hidden="true">' + cells.join("") + "</div>";
  }

  function amountLabel(amount) {
    if (amount == null || amount === "") return { value: "--", unit: "", compact: false };
    const n = number(amount);
    if (n >= 10000) {
      return { value: formatNumber(n / 10000, 2), unit: "万亿", compact: n >= 100000 };
    }
    return { value: Math.round(n).toLocaleString("zh-CN"), unit: "亿", compact: false };
  }

  function stripCell(label, valueHtml, tone, extraCls) {
    return '<div class="m-strip-cell' + (tone ? " m-strip-" + tone : "") + (extraCls ? " " + extraCls : "") + '">' +
      '<span class="m-strip-label">' + escapeHtml(label) + "</span>" +
      '<span class="m-strip-value">' + valueHtml + "</span></div>";
  }

  function intOrDash(value) {
    return value == null ? "--" : Math.round(number(value)).toLocaleString("zh-CN");
  }

  function buildStrip() {
    const dash = state.dashboard || {};
    if (!state.dashboard) return stripSkeleton();
    const overview = dash.overview || {};
    const limits = dash.limits || [];
    const maxStreak = limits.reduce(function (m, r) { return Math.max(m, number(r.streak)); }, 0);

    const score = overview.sentiment_score != null ? Math.round(number(overview.sentiment_score)) : null;
    const phase = overview.sentiment_phase || "";
    const emotionHtml = (score == null ? "--" : "<b>" + score + "</b>") +
      (phase ? '<i class="m-strip-phase">' + escapeHtml(phase) + "</i>" : "");

    const amount = amountLabel(overview.amount_billion);
    const amountHtml = "<b>" + escapeHtml(amount.value) + "</b>" +
      (amount.unit ? '<i class="m-strip-unit">' + escapeHtml(amount.unit) + "</i>" : "");
    const seal = overview.seal_rate != null ? formatNumber(overview.seal_rate, 1) + "%" : "--";

    const cells = [];
    cells.push(stripCell("情绪", emotionHtml, "", ""));
    cells.push(stripCell("涨停", "<b>" + intOrDash(overview.limit_up_count) + "</b>", "up", ""));
    cells.push(stripCell("跌停", "<b>" + intOrDash(overview.limit_down_count) + "</b>", "down", ""));
    cells.push(stripCell("炸板", "<b>" + intOrDash(overview.broken_count) + "</b>", "warn", ""));
    cells.push(stripCell("封板率", "<b>" + seal + "</b>", "", ""));
    cells.push(stripCell("成交额", amountHtml, "", amount.compact ? "m-strip-sm" : ""));
    cells.push(stripCell("最高连板", "<b>" + (maxStreak > 0 ? maxStreak : "--") + "</b>", "up", ""));

    return '<div class="m-strip">' + cells.join("") + "</div>";
  }

  /* ---------------------------------------------------------------- rows */

  function brokenLimitRate(row) {
    const name = String(row.name || "").toUpperCase();
    const code = String(row.code || "").replace(/\D/g, "");
    if (name.indexOf("ST") >= 0) return 10;
    if (/^(300|301|688|689)/.test(code)) return 20;
    if (/^(4|8|92)/.test(code)) return 30;
    return 10;
  }

  function prepareRows(key) {
    const dash = state.dashboard || {};
    if (key === "market/limit-up") return dash.limits || [];
    if (key === "market/broken") {
      return (dash.broken || []).map(function (row) {
        row.limitGap = Math.max(0, brokenLimitRate(row) - number(row.change));
        return row;
      });
    }
    if (key === "market/limit-down") return dash.down_limits || [];
    if (key === "market/yesterday") return dash.yesterday_limits || [];
    if (key === "market/performance") return normalizePerformanceRows(dash.limit_performance || []);
    if (key === "market/popularity") {
      return state.popularity ? (state.popularity[state.popularitySource] || []) : [];
    }
    return [];
  }

  function normalizePerformanceRows(rows) {
    const groups = {};
    (rows || []).forEach(function (row) {
      const level = Math.max(1, number(row.level));
      const displayLevel = Math.min(level, 5);
      const group = groups[displayLevel] || {
        level: displayLevel,
        label: displayLevel === 1 ? "昨日首板" : displayLevel === 5 ? "昨日5板+" : "昨日" + displayLevel + "板",
        count: 0,
        advanced: 0,
        positive: 0,
        changeTotal: 0,
      };
      const count = number(row.count);
      group.count += count;
      group.advanced += number(row.advanced);
      group.positive += count * number(row.positive_rate) / 100;
      group.changeTotal += count * number(row.average_change);
      groups[displayLevel] = group;
    });
    return Object.keys(groups)
      .map(Number)
      .sort(function (a, b) { return b - a; })
      .map(function (level) {
        const group = groups[level];
        return {
          level: group.level,
          label: group.label,
          count: group.count,
          advanced: group.advanced,
          advance_rate: group.count ? group.advanced / group.count * 100 : 0,
          positive_rate: group.count ? group.positive / group.count * 100 : 0,
          average_change: group.count ? group.changeTotal / group.count : 0,
        };
      });
  }

  function performanceConclusion() {
    const overview = (state.dashboard && state.dashboard.overview) || {};
    const up = number(overview.up_count);
    const down = number(overview.down_count);
    const breadthRate = up + down > 0 ? up / (up + down) * 100 : 50;
    const stance = breadthRate < 25 ? "宜守不宜攻" : breadthRate < 45 ? "控制仓位，聚焦核心" : "保持精选，跟随强势梯队";
    const phase = overview.sentiment_phase || "观察";
    return '<div class="m-conclusion">结论：<b>' + escapeHtml(stance) + "</b>，当前情绪周期「" + escapeHtml(phase) + "」。</div>";
  }

  /* ---------------------------------------------------------------- render */

  function updateHeader(title, dateText) {
    if (global.MobileRouter && global.MobileRouter.updateHeader) {
      global.MobileRouter.updateHeader({ title: title, back: true, actions: dateButtonHtml(dateText) });
    }
  }

  function dateButtonHtml(dateText) {
    return '<button class="m-date-btn" type="button" data-action="date" aria-label="选择日期">' +
      icon("calendar", 15) +
      '<span class="m-date-btn-text">' + escapeHtml(dateText || state.requestedDate || todayString()) + "</span>" +
      "</button>";
  }

  function sourceTabsHtml(cfg) {
    if (!cfg.sources || !cfg.sources.length) return "";
    const labels = { combined: "双榜综合", ths: "同花顺", dc: "东方财富" };
    return '<div class="m-source-tabs" role="tablist" aria-label="热榜来源">' +
      cfg.sources.map(function (source) {
        const active = source === state.popularitySource;
        return '<button class="m-source-tab' + (active ? " active" : "") + '" type="button" role="tab" aria-selected="' + (active ? "true" : "false") + '" data-action="source" data-source="' + source + '">' + escapeHtml(labels[source] || source) + "</button>";
      }).join("") +
      "</div>";
  }

  function renderPage(key) {
    if (isComplexPage(key)) {
      COMPLEX_PAGES[key](key);
      const loader = COMPLEX_LOADERS[key];
      if (loader) loader();
      return;
    }
    const cfg = pageConfig(key);
    if (!cfg) return;
    state.key = key;
    state.requestedDate = todayString();
    state.dashboard = null;
    state.popularity = null;
    state.popularitySource = "combined";
    state.calCursor = null;
    state.sort = defaultSortForKey(key);
    state.detail = null;

    document.getElementById("m-view").classList.add("m-view-feature");
    const title = findLabel(key) || key;
    updateHeader(title, state.requestedDate);

    const sourceTabs = sourceTabsHtml(cfg);
    const content = '<div class="m-page" data-page="' + escapeHtml(key) + '">' +
      '<div class="m-top">' + buildStrip() + "</div>" +
      sourceTabs +
      '<div class="m-table-scroll" id="m-table-scroll">' + skeletonHtml(8) + "</div>" +
      "</div>";
    document.getElementById("m-view").innerHTML = content;

    load();
  }

  function findLabel(key) {
    const hubs = global.MobileNav && global.MobileNav.hubs ? global.MobileNav.hubs : {};
    for (const hubKey in hubs) {
      const items = hubs[hubKey].items || [];
      for (const item of items) {
        if (item.key === key) return item.label;
      }
    }
    return key;
  }

  function load() {
    const key = state.key;
    const cfg = pageConfig(key);
    if (!cfg) return;
    const seq = ++state.seq;
    const requestedDate = state.requestedDate;
    const popularity = key === "market/popularity";

    const dashboardUrl = "/api/dashboard?trade_date=" + encodeURIComponent(requestedDate);
    const popUrl = "/api/popularity?trade_date=" + encodeURIComponent(requestedDate);

    const request = popularity
      ? Promise.all([global.MobileAPI.request(popUrl), global.MobileAPI.request(dashboardUrl)])
      : global.MobileAPI.request(dashboardUrl).then(function (payload) { return [payload]; });

    request.then(function (results) {
      if (seq !== state.seq) return;
      let meta = {};
      if (popularity) {
        state.popularity = results[0];
        state.dashboard = results[1];
        meta = (results[0] && results[0].meta) || {};
      } else {
        state.dashboard = results[0];
        meta = (results[0] && results[0].meta) || {};
      }
      state.requestedDate = displayCompactDate(meta.requested_date || meta.trade_date || requestedDate);
      const title = findLabel(key) || key;
      updateHeader(title, state.requestedDate);
      renderData(key, cfg);
    }).catch(function (error) {
      if (seq !== state.seq) return;
      renderError(error && error.message ? error.message : "数据加载失败");
    });
  }

  function renderData(key, cfg) {
    renderTableBody();
    renderTopArea(key);
  }

  function renderTableBody() {
    const key = state.key;
    const cfg = pageConfig(key);
    if (!cfg) return;
    const cols = columnsFor(cfg);
    state.sortTable = { cols: cols, reapply: renderTableBody };
    const rows = sortedRows(prepareRows(key), cols);
    const scroll = document.getElementById("m-table-scroll");
    if (!scroll) return;

    if (!rows.length) {
      scroll.innerHTML = emptyHtml();
    } else {
      scroll.innerHTML = buildTable(cols, rows);
    }

    scroll.classList.remove("m-motion-fade-in");
    void scroll.offsetWidth;
    scroll.classList.add("m-motion-fade-in");
  }

  function dashboardFreshnessNotice() {
    const meta = (state.dashboard && state.dashboard.meta) || {};
    if (meta.display_notice) return String(meta.display_notice);
    const requested = String(meta.requested_date || "").replace(/-/g, "");
    const actual = String(meta.trade_date || "").replace(/-/g, "");
    const compact = actual;
    const shown = /^\d{8}$/.test(compact)
      ? (Number(compact.slice(4, 6)) + " 月 " + Number(compact.slice(6, 8)) + " 日")
      : "";
    if (meta.data_status === "preparing" || (meta.carried_forward && actual && requested && actual !== requested)) {
      return shown ? ("今日数据正在准备，当前展示 " + shown) : "今日数据正在准备，当前展示最近可用数据";
    }
    if (meta.data_status === "partial" || meta.limit_data_source === "derived") {
      return meta.notice || "部分正式数据尚未到齐，当前展示日线推算结果";
    }
    return "";
  }

  function renderTopArea(key) {
    const page = document.querySelector(".m-page");
    if (!page) return;
    let top = page.querySelector(".m-top");
    let html = buildStrip();
    const freshness = dashboardFreshnessNotice();
    if (freshness) {
      html = '<div class="m-phase-notice"><strong>' + escapeHtml(freshness) + "</strong></div>" + html;
    }
    if (key === "market/performance") html += performanceConclusion();
    if (!top) {
      top = document.createElement("div");
      top.className = "m-top";
      const tabs = page.querySelector(".m-source-tabs");
      const scroll = document.getElementById("m-table-scroll");
      page.insertBefore(top, tabs || scroll);
    }
    top.innerHTML = html;
    const strip = top.querySelector(".m-strip");
    if (strip) {
      strip.classList.remove("m-motion-fade-in");
      void strip.offsetWidth;
      strip.classList.add("m-motion-fade-in");
    }
  }

  function renderError(message) {
    const scroll = document.getElementById("m-table-scroll") || document.getElementById("m-scroll");
    if (scroll) scroll.innerHTML = errorHtml(message);
    const page = document.querySelector(".m-page");
    if (page) {
      const top = page.querySelector(".m-top");
      if (top) top.remove();
    }
  }

  /* ---------------------------------------------------------------- complex pages (P2b) */

  function nextSeq() {
    return ++state.seq;
  }

  function columnsForKey(subKey) {
    const cfg = global.MobileNav && global.MobileNav.tableColumns ? global.MobileNav.tableColumns[subKey] : null;
    return cfg || null;
  }

  function mountSortableTable(containerId, subKey, rowsProvider, opts) {
    const cols = columnsForKey(subKey);
    if (!cols) return;
    const reapply = function () {
      const container = document.getElementById(containerId);
      if (!container) return;
      const rows = sortedRows(rowsProvider() || [], cols);
      if (!rows.length) {
        container.innerHTML = emptyHtml();
        return;
      }
      container.innerHTML = buildTable(cols, rows, opts || {});
    };
    state.sortTable = { cols: cols, reapply: reapply };
    reapply();
  }

  function complexFrame(key, bodyHtml) {
    return '<div class="m-page" data-page="' + escapeHtml(key) + '">' + bodyHtml + "</div>";
  }

  function complexScroll(bodyHtml) {
    return '<div class="m-scroll" id="m-scroll">' + bodyHtml + "</div>";
  }

  function resetComplexState(key) {
    state.key = key;
    state.requestedDate = todayString();
    state.dashboard = null;
    state.popularity = null;
    state.calCursor = null;
    state.sort = { key: "", dir: null };
    state.sortTable = { cols: null, reapply: null };
    state.detail = null;
    state.sentimentHistory = null;
    state.rotation = null;
    state.rotationSelectedSector = "";
    state.rotationSelectedDate = "";
    state.rotationMembers = null;
    state.auction = null;
    state.auctionDataset = "focus";
    state.themes = null;
    state.themesDetail = null;
    state.themesSelectedCode = "";
    state.dragon = null;
    state.dragonProfiles = null;
    state.dragonViewMode = "daily";
    state.dragonSelectedTrader = "";
  }

  function reloadCurrent() {
    const loader = COMPLEX_LOADERS[state.key];
    if (loader) { loader(); return; }
    load();
  }

  function clampScore(value) {
    return Math.max(0, Math.min(100, number(value)));
  }

  function phaseTone(phase) {
    return {
      "冰点": "ice", "修复": "repair", "发酵": "fermentation",
      "高潮": "climax", "分化": "divergence", "退潮": "retreat",
    }[phase] || "divergence";
  }

  function phaseBadgeHtml(phase) {
    if (!phase) return "";
    return '<span class="m-phase m-phase--' + phaseTone(phase) + '">' + escapeHtml(phase) + "</span>";
  }

  function directionTone(direction) {
    return { "升温": "up", "降温": "down", "新进": "new", "持平": "flat" }[direction] || "flat";
  }

  function formatMoneyMillion(value) {
    const parsed = number(value);
    const sign = parsed > 0 ? "+" : "";
    if (Math.abs(parsed) >= 100) return sign + formatNumber(parsed / 100, 2) + " 亿";
    return sign + formatNumber(parsed * 100, 0) + " 万";
  }

  function sentimentTrendChart(rows) {
    const W = 360, H = 168, padL = 8, padR = 30, padT = 10, padB = 20;
    const pw = W - padL - padR;
    const ph = H - padT - padB;
    if (!rows.length) return emptyChart("暂无情绪历史数据");
    function x(i) { return padL + (rows.length <= 1 ? pw / 2 : i / (rows.length - 1) * pw); }
    function y(v) { return padT + (100 - clampScore(v)) / 100 * ph; }
    const line = rows.map(function (r, i) {
      return (i ? "L" : "M") + x(i).toFixed(1) + " " + y(number(r.score)).toFixed(1);
    }).join(" ");
    const area = line + " L" + x(rows.length - 1).toFixed(1) + " " + (padT + ph).toFixed(1) +
      " L" + padL + " " + (padT + ph).toFixed(1) + " Z";
    const right = W - 4;
    const labelStep = Math.max(1, Math.ceil(rows.length / 6));
    const xLabels = rows.map(function (r, i) {
      if (i % labelStep !== 0 && i !== rows.length - 1) return "";
      const anchor = i === 0 ? "start" : i === rows.length - 1 ? "end" : "middle";
      return svgAxisText(x(i), H - 6, anchor, "", dateMMDD(displayCompactDate(r.trade_date)));
    }).join("");
    return '<svg viewBox="0 0 ' + W + " " + H + '" preserveAspectRatio="none" aria-hidden="true">' +
      svgAxisText(right, padT + 8, "end", "", "100") +
      svgAxisText(right, padT + ph / 2 + 3, "end", "", "50") +
      svgAxisText(right, padT + ph, "end", "", "0") +
      '<path class="m-sentiment-area" d="' + area + '"/>' +
      '<path class="m-sentiment-line" d="' + line + '"/>' +
      xLabels +
      "</svg>";
  }

  /* ----- 情绪周期 ----- */

  function loadSentiment() {
    const seq = nextSeq();
    const date = state.requestedDate;
    const historyUrl = "/api/sentiment/history?trade_date=" + encodeURIComponent(date) +
      "&limit=" + state.sentimentRange;
    const req = Promise.all([
      global.MobileAPI.request("/api/dashboard?trade_date=" + encodeURIComponent(date)),
      global.MobileAPI.request(historyUrl),
    ]);
    req.then(function (results) {
      if (seq !== state.seq) return;
      state.dashboard = results[0];
      state.sentimentHistory = results[1];
      const meta = (results[0] && results[0].meta) || {};
      state.requestedDate = displayCompactDate(meta.requested_date || meta.trade_date || date);
      updateHeader(findLabel(state.key) || state.key, state.requestedDate);
      renderSentiment();
    }).catch(function (error) {
      if (seq !== state.seq) return;
      renderError(error && error.message ? error.message : "情绪周期加载失败");
    });
  }

  function sentimentComponentList(components) {
    const items = Object.keys(components || {}).map(function (key) { return components[key]; });
    if (!items.length) return "";
    return '<div class="m-components">' + items.map(function (item) {
      const score = clampScore(item.score);
      return '<div class="m-component">' +
        '<div class="m-component-head"><span>' + escapeHtml(item.label) + "</span>" +
        "<b>" + formatNumber(number(item.score), 1) + "</b></div>" +
        '<div class="m-component-track"><i style="width:' + score.toFixed(1) + '%"></i></div>' +
        '<small>' + escapeHtml(item.summary || "") + " · 权重 " + number(item.weight) + "%</small>" +
        "</div>";
    }).join("") + "</div>";
  }

  function renderSentiment() {
    const scroll = document.getElementById("m-scroll");
    if (!scroll) return;
    const dash = state.dashboard || {};
    const overview = dash.overview || {};
    const history = state.sentimentHistory || {};
    const rawRows = history.rows || [];
    const latest = rawRows.length ? rawRows[rawRows.length - 1] : null;

    const rangeTabs = [20, 40, 60].map(function (n) {
      const active = state.sentimentRange === n;
      return '<button class="m-range-tab' + (active ? " active" : "") + '" type="button" data-sentiment-range="' + n + '" aria-pressed="' + (active ? "true" : "false") + '">' + n + "日</button>";
    }).join("");

    let card = "";
    if (latest) {
      const score = number(latest.score);
      const dayChange = number(latest.day_change);
      const components = latest.components || overview.sentiment_components || {};
      card =
        '<div class="m-sent-card">' +
        '<div class="m-sent-score-row"><strong class="m-sent-score">' + score + "</strong>" +
        '<div class="m-sent-score-side">' + phaseBadgeHtml(latest.phase) +
        '<span class="m-sent-direction m-sent-dir--' + directionTone(latest.direction) + '">' + escapeHtml(latest.direction) + "</span></div></div>" +
        '<div class="m-sent-label">' + escapeHtml(latest.label || "") + "</div>" +
        '<div class="m-sent-metrics">' +
        sentMetric("较前日", (dayChange > 0 ? "+" : "") + formatNumber(dayChange, 1), changeClass(dayChange)) +
        sentMetric("封板率", formatNumber(number(latest.seal_rate), 1) + "%", "") +
        sentMetric("涨停", number(latest.limit_up_count), "up") +
        sentMetric("炸板", number(latest.broken_count), "warn") +
        "</div>" +
        '<p class="m-sent-advice">' + escapeHtml(sentimentAdvice(latest.phase)) + "</p>" +
        "</div>" +
        '<div class="m-section-title">情绪趋势（近 ' + rawRows.length + ' 日）<span class="m-range-tabs">' + rangeTabs + "</span></div>" +
        '<div class="m-chart-card">' + sentimentTrendChart(rawRows) + "</div>" +
        '<div class="m-section-title">五维分项</div>' +
        sentimentComponentList(components);
    }

    scroll.innerHTML = card +
      '<div class="m-section-title">历史明细</div>' +
      '<div class="m-table-wrap" id="m-sentiment-hist"></div>';
    mountSortableTable("m-sentiment-hist", "market/sentiment/history", function () {
      const raw = (state.sentimentHistory && state.sentimentHistory.rows) || [];
      return raw.slice().reverse().map(function (r) {
        return Object.assign({}, r, { trade_date: displayCompactDate(r.trade_date) });
      });
    }, { noSort: true });
  }

  function sentMetric(label, valueHtml, tone) {
    return '<div class="m-sent-metric' + (tone ? " m-sent-" + tone : "") + '"><span>' + escapeHtml(label) + "</span><b>" + valueHtml + "</b></div>";
  }

  function sentimentAdvice(phase) {
    return {
      "冰点": "情绪处于极弱区，先观察风险释放。",
      "修复": "风险开始收敛，关注率先转强的核心。",
      "发酵": "主线与梯队正在形成，优先跟随核心。",
      "高潮": "情绪处高位，聚焦核心并主动降低后排暴露。",
      "分化": "强弱开始分层，关注承接与回流。",
      "退潮": "情绪指标继续走弱，控制仓位。",
    }[phase] || "市场结构尚未形成清晰阶段，保持观察。";
  }

  /* ----- 市场天梯 ----- */

  const expandedLadder = {};

  function loadLadder() {
    const seq = nextSeq();
    Object.keys(expandedLadder).forEach(function (level) { delete expandedLadder[level]; });
    const date = state.requestedDate;
    global.MobileAPI.request("/api/dashboard?trade_date=" + encodeURIComponent(date)).then(function (payload) {
      if (seq !== state.seq) return;
      state.dashboard = payload;
      const meta = payload.meta || {};
      state.requestedDate = displayCompactDate(meta.requested_date || meta.trade_date || date);
      updateHeader(findLabel(state.key) || state.key, state.requestedDate);
      renderLadder();
    }).catch(function (error) {
      if (seq !== state.seq) return;
      renderError(error && error.message ? error.message : "市场天梯加载失败");
    });
  }

  function ladderTierHtml(level, group, groupMap, maxLevel) {
    const label = group ? group.label : (level === 1 ? "首板" : level + "板");
    const count = group ? number(group.count) : 0;
    const gap = count === 0;
    const color = { 1: "var(--action)", 2: "var(--market-down)", 3: "var(--warning)", 4: "var(--market-up)" }[level] || "var(--text-tertiary)";
    const rate = level > 1 && count ? Math.round(count / Math.max(number((groupMap[level - 1] || {}).count), 1) * 100 * 10) / 10 : 0;
    const rateHtml = level > 1 && count ? '<span class="m-ladder-rate">较' + (level - 1) + "板 " + rate + "%</span>" : "";

    let stocksHtml = "";
    let foldBtn = "";
    if (gap) {
      stocksHtml = '<div class="m-ladder-gap">' + (level >= maxLevel ? "断层 · " + escapeHtml(label) + "及以上空缺" : "该层暂时空缺") + "</div>";
    } else {
      const stocks = (group.stocks || []).slice();
      const cap = level === 1 || level === 2 ? 8 : 0;
      const expanded = expandedLadder[level];
      const visible = (expanded || !cap) ? stocks : stocks.slice(0, cap);
      stocksHtml = visible.map(function (s) {
        const onePrice = String(s.first_time || "").startsWith("09:25") && number(s.open_times) === 0;
        const broken = number(s.open_times) >= 6;
        const amount = number(s.seal_amount_million) ? "封单 " + formatNumber(s.seal_amount_million, 0) + " 万" : "成交 " + formatNumber(s.amount_billion, 1) + " 亿";
        return '<button class="m-ladder-stock" type="button" data-ladder-stock="' + escapeHtml(s.code) + '">' +
          '<span class="m-ladder-stock-main"><strong>' + escapeHtml(s.name) + "</strong>" +
          '<small>' + escapeHtml(s.code) + "</small>" +
          (onePrice ? '<em class="m-ladder-tag">一字</em>' : "") +
          (broken ? '<em class="m-ladder-tag is-broken">烂板×' + number(s.open_times) + "</em>" : "") +
          "</span>" +
          '<span class="m-ladder-stock-sub"><b>' + escapeHtml(s.sector || s.reason || "其他") + "</b>" +
          "<small>" + escapeHtml(s.first_time && s.first_time !== "--" ? s.first_time : "时间待校正") + " · " + escapeHtml(amount) + "</small></span>" +
          "</button>";
      }).join("");
      if (cap && stocks.length > cap) {
        const remaining = stocks.length - cap;
        foldBtn = '<button class="m-ladder-fold" type="button" data-ladder-expand="' + level + '">' +
          (expanded ? "收起 \u25b4" : "展开剩余 " + remaining + " 只 \u25be") + "</button>";
      }
    }

    return '<section class="m-ladder-tier' + (gap ? " is-gap" : "") + '">' +
      '<div class="m-ladder-tier-head"><span class="m-ladder-tier-level" style="color:' + color + '">' + escapeHtml(label) + "</span>" +
      '<span class="m-ladder-tier-count">' + count + " 只</span>" + rateHtml + foldBtn + "</div>" +
      '<div class="m-ladder-stocks">' + stocksHtml + "</div></section>";
  }

  function renderLadder() {
    const scroll = document.getElementById("m-scroll");
    if (!scroll) return;
    const dash = state.dashboard || {};
    const ladders = dash.ladders || [];
    if (!ladders.length) { scroll.innerHTML = emptyHtml(); return; }
    const maxLevel = ladders.reduce(function (m, g) { return Math.max(m, number(g.level)); }, 0);
    const topVisible = Math.max(5, maxLevel);
    const groupMap = {};
    ladders.forEach(function (g) { groupMap[number(g.level)] = g; });
    const yesterday = dash.yesterday_limits || [];
    const prevMax = yesterday.reduce(function (m, r) { return Math.max(m, number(r.prior_streak)); }, 0);
    const spaceChange = prevMax && maxLevel < prevMax ? "较昨日 " + prevMax + " 板 ↓ 压缩"
      : prevMax && maxLevel > prevMax ? "较昨日 " + prevMax + " 板 ↑ 抬升" : "高度与昨日接近";
    const spaceNote = maxLevel >= 5 ? "高位梯队仍有辨识度，重点看承接。"
      : maxLevel >= 3 ? "空间位于中段，梯队延续性比绝对高度更重要。" : "高度受压缩，先看首板向二板的结构修复。";

    const apexHtml = '<div class="m-apex">' +
      '<div class="m-apex-head"><span>空间板</span><strong>' + (maxLevel ? maxLevel + " 板" : "--") + "</strong><em>" + escapeHtml(spaceChange) + "</em></div>" +
      "<small>" + escapeHtml(spaceNote) + "</small></div>";

    const tiers = [];
    for (let level = topVisible; level >= 1; level -= 1) {
      tiers.push(ladderTierHtml(level, groupMap[level], groupMap, maxLevel));
    }

    const perf = dash.limit_performance || [];
    const perfRows = perf.map(function (p) {
      const value = Math.max(0, Math.min(100, number(p.advance_rate)));
      return '<div class="m-rate-row"><span>' + escapeHtml(p.label || "昨日" + number(p.level) + "板") + "</span>" +
        '<i><b style="width:' + value.toFixed(1) + '%"></b></i><strong>' + formatNumber(value, 1) + "%</strong></div>";
    }).join("");
    const perfHtml = perfRows
      ? '<div class="m-section-title">晋级率参考（昨日梯队 → 今日）</div><div class="m-rate-list">' + perfRows + "</div>"
      : "";

    scroll.innerHTML = apexHtml + tiers.join("") + perfHtml;
  }

  /* ----- 主题轮动 ----- */

  function loadRotation() {
    const seq = nextSeq();
    const date = state.requestedDate;
    global.MobileAPI.request("/api/rotation/history?trade_date=" + encodeURIComponent(date)).then(function (payload) {
      if (seq !== state.seq) return;
      state.rotation = payload;
      state.rotationMembers = null;
      state.rotationSelectedSector = "";
      state.rotationSelectedDate = "";
      const meta = payload || {};
      state.requestedDate = displayCompactDate(meta.trade_date || date);
      updateHeader(findLabel(state.key) || state.key, state.requestedDate);
      renderRotation();
    }).catch(function (error) {
      if (seq !== state.seq) return;
      renderError(error && error.message ? error.message : "主题轮动加载失败");
    });
  }

  function renderRotation() {
    const scroll = document.getElementById("m-scroll");
    if (!scroll) return;
    const rows = (state.rotation && state.rotation.rows) || [];
    if (!rows.length) { scroll.innerHTML = emptyHtml(); return; }
    const latestDate = rows[0].trade_date;
    scroll.innerHTML = rows.map(function (day) {
      const sectors = day.sectors || [];
      const chips = sectors.map(function (sector) {
        const strength = clampScore(sector.strength);
        const heat = strength >= 90 ? "strong" : strength >= 70 ? "warm" : "mild";
        return '<button class="m-rotation-chip ' + heat + '" type="button" data-rotation-sector="' + escapeHtml(sector.name) + '" data-rotation-date="' + escapeHtml(day.trade_date) + '">' +
          '<span class="m-rotation-rank rank-' + Math.min(number(sector.rank), 4) + '">' + number(sector.rank) + "</span>" +
          '<span class="m-rotation-copy">' +
          "<strong>" + escapeHtml(sector.name) + "</strong>" +
          "<small>" + number(sector.count) + " 家 · " + formatNumber(number(sector.strength), 0) + "</small>" +
          "</span>" +
          "</button>";
      }).join("");
      return '<section class="m-rotation-day' + (day.trade_date === latestDate ? " is-latest" : "") + '">' +
        '<header><time>' + escapeHtml(displayCompactDate(day.trade_date).slice(5)) + "</time>" +
        "<span>" + sectors.length + " 个热点</span></header>" +
        '<div class="m-rotation-chips">' + chips + "</div></section>";
    }).join("");
  }

  function loadRotationMembers(sector, date) {
    const seq = nextSeq();
    const url = "/api/rotation/members?trade_date=" + encodeURIComponent(date) + "&sector=" + encodeURIComponent(sector);
    global.MobileAPI.request(url).then(function (payload) {
      if (seq !== state.seq) return;
      state.rotationMembers = payload;
      renderRotationMembersSheet(sector, payload);
    }).catch(function (error) {
      if (seq !== state.seq) return;
      state.rotationMembers = { error: error.message || "成分股加载失败" };
      renderRotationMembersSheet(sector, state.rotationMembers);
    });
  }

  function openRotationMembersSheet(sector, date) {
    openSheet(
      '<div class="m-sheet-head"><h2>' + escapeHtml(sector) + "成分股</h2>" +
      '<button class="m-sheet-close" type="button" data-sheet-close aria-label="关闭">' + icon("close", 20) + "</button></div>" +
      '<div class="m-sheet-body" id="m-rotation-members-body">' + skeletonHtml(8) + "</div>",
      { detail: false }
    );
    loadRotationMembers(sector, date);
  }

  function renderRotationMembersSheet(sector, payload) {
    const body = document.getElementById("m-rotation-members-body");
    if (!body) return;
    if (payload.error) { body.innerHTML = emptyHtml(); return; }
    const meta = payload.meta || {};
    const metaLine = '<div class="m-sheet-meta">' + escapeHtml(displayCompactDate(meta.trade_date) || "--") +
      " · " + number(meta.quoted_count) + " / " + number(meta.member_count) + " 只</div>";
    body.innerHTML = metaLine + '<div class="m-table-wrap" id="m-rotation-members-table"></div>';
    mountSortableTable("m-rotation-members-table", "market/rotation/members", function () {
      return (state.rotationMembers && state.rotationMembers.rows) || [];
    }, {});
  }

  /* ----- 竞价 ----- */

  const AUCTION_DATASETS = [
    { key: "focus", label: "重点异动" },
    { key: "all", label: "全部候选" },
    { key: "onePrice", label: "竞价一字" },
    { key: "watchlist", label: "我的自选" },
  ];

  function loadAuction() {
    const seq = nextSeq();
    const date = state.requestedDate;
    global.MobileAPI.request("/api/auction?trade_date=" + encodeURIComponent(date)).then(function (payload) {
      if (seq !== state.seq) return;
      state.auction = payload;
      const meta = payload.meta || {};
      state.requestedDate = displayCompactDate(meta.trade_date || date);
      updateHeader(findLabel(state.key) || state.key, state.requestedDate);
      renderAuction();
    }).catch(function (error) {
      if (seq !== state.seq) return;
      renderError(error && error.message ? error.message : "竞价数据加载失败");
    });
  }

  function auctionRows() {
    const data = state.auction || {};
    const datasets = {
      focus: data.focus_rows || [],
      all: data.rows || [],
      onePrice: data.one_price_rows || [],
      watchlist: data.watchlist_rows || [],
    };
    const rows = (datasets[state.auctionDataset] || []).slice();
    if (state.auctionDataset === "onePrice") {
      return rows.map(function (r) { return Object.assign({}, r, { expectation: "竞价一字" }); });
    }
    return rows;
  }

  function auctionAmountTrend(history) {
    if (!history || !history.length) return '<div class="m-inline-empty">历史竞价量能尚未形成</div>';
    const max = Math.max.apply(null, history.map(function (h) { return number(h.amount_billion); })) || 1;
    return '<div class="m-mini-bars">' + history.map(function (h, i) {
      const hgt = Math.max(8, number(h.amount_billion) / max * 100);
      const current = i === history.length - 1 ? " current" : "";
      return '<div class="m-mini-bar' + current + '" title="' + escapeHtml(h.trade_date) + " · " + formatNumber(number(h.amount_billion), 2) + ' 亿 · ' + number(h.stock_count) + ' 只">' +
        '<span style="height:' + hgt.toFixed(1) + '%"></span><small>' + escapeHtml(String(h.trade_date || "").slice(5)) + "</small></div>";
    }).join("") + "</div>";
  }

  function auctionThemeCarry(carry) {
    if (!carry || !carry.length) return "";
    const tone = { "强承接": "strong", "有承接": "steady", "分歧": "mixed", "承接弱": "weak" };
    return '<div class="m-carry">' + carry.slice(0, 6).map(function (item) {
      return '<div class="m-carry-row">' +
        '<span class="m-carry-name">' + escapeHtml(item.name) + "</span>" +
        '<span class="m-carry-leader">' + escapeHtml(item.leader || "--") + " · 昨 " + number(item.prior_limit_count) + " 只</span>" +
        '<span class="m-carry-status ' + (tone[item.status] || "mixed") + '">' + escapeHtml(item.status) + "</span>" +
        '<span class="m-carry-median ' + changeClass(item.median_change) + '">' +
        (item.median_change == null ? "暂无候选" : (number(item.median_change) > 0 ? "+" : "") + formatNumber(number(item.median_change), 2) + "%") + "</span></div>";
    }).join("") + "</div>";
  }

  function auctionPhaseNotice(meta) {
    const phase = meta.phase || "archive";
    const copy = {
      pending: ["竞价尚未开始", "9:15 进入观察期，9:25 读取最终竞价结果。"],
      observing: ["竞价观察期", "此阶段先观察盘前变化，系统将在 9:25 自动读取最终结果。"],
      selection: ["竞价筛选窗口", "最终竞价结果已经定格，请在 9:30 前完成筛选。"],
      finalized: ["今日竞价已定格", "9:30 后停止更新，仅保留用于复盘与回测。"],
      archive: ["历史竞价归档", "当前展示所选交易日的最终竞价结果。"],
    }[phase] || ["竞价状态", "当前竞价状态待确认。"];
    return '<div class="m-phase-notice m-phase-notice--' + escapeHtml(phase) + '"><strong>' + escapeHtml(copy[0]) + "</strong><span>" + escapeHtml(copy[1]) + "</span></div>";
  }

  function renderAuction() {
    const scroll = document.getElementById("m-scroll");
    if (!scroll) return;
    const payload = state.auction;
    if (!payload) return;
    const summary = payload.summary || {};
    const meta = payload.meta || {};
    const themes = payload.themes || {};

    const metrics = [
      ["竞价覆盖", formatNumber(number(summary.stock_count), 0) + " 只", ""],
      ["重点异动", formatNumber(number(summary.focus_count), 0) + " 只", "up"],
      ["竞价一字", formatNumber(number(summary.one_price_count), 0) + " 只", ""],
      ["竞价成交额", formatNumber(number(summary.amount_billion), 2) + " 亿", ""],
    ].map(function (m) {
      return '<div class="m-sum-metric' + (m[2] ? " m-sum-" + m[2] : "") + '"><span>' + escapeHtml(m[0]) + "</span><strong>" + m[1] + "</strong></div>";
    }).join("");

    const tabs = AUCTION_DATASETS.map(function (d) {
      const active = state.auctionDataset === d.key;
      return '<button class="m-source-tab' + (active ? " active" : "") + '" type="button" role="tab" aria-selected="' + (active ? "true" : "false") + '" data-action="auction-dataset" data-dataset="' + d.key + '">' + escapeHtml(d.label) + "</button>";
    }).join("");

    const carry = auctionThemeCarry(themes.carry);
    scroll.innerHTML =
      auctionPhaseNotice(meta) +
      '<div class="m-sum-strip">' + metrics + "</div>" +
      '<div class="m-section-title">竞价量能</div><div class="m-chart-card">' + auctionAmountTrend(payload.amount_history || []) + "</div>" +
      (carry ? '<div class="m-section-title">昨日强势题材承接</div><div class="m-card">' + carry + "</div>" : "") +
      '<div class="m-source-tabs" role="tablist" aria-label="竞价数据集">' + tabs + "</div>" +
      '<div class="m-table-wrap" id="m-auction-table"></div>';
    renderAuctionTable();
  }

  function renderAuctionTable() {
    mountSortableTable("m-auction-table", "market/auction", auctionRows, {});
  }

  /* ----- 题材库 ----- */

  function loadThemes() {
    const seq = nextSeq();
    const date = state.requestedDate;
    global.MobileAPI.request("/api/themes?trade_date=" + encodeURIComponent(date)).then(function (payload) {
      if (seq !== state.seq) return;
      state.themes = payload;
      const meta = payload.meta || {};
      state.requestedDate = displayCompactDate(meta.trade_date || date);
      updateHeader(findLabel(state.key) || state.key, state.requestedDate);
      renderThemes();
    }).catch(function (error) {
      if (seq !== state.seq) return;
      renderError(error && error.message ? error.message : "题材库加载失败");
    });
  }

  function renderThemes() {
    const scroll = document.getElementById("m-scroll");
    if (!scroll) return;
    const payload = state.themes;
    if (!payload) return;
    const summary = payload.summary || {};
    const items = payload.items || [];
    const metrics = [
      ["收录题材", number(summary.theme_count), "个", ""],
      ["当日上涨", number(summary.up_count), "个", "up"],
      ["当日下跌", number(summary.down_count), "个", "down"],
      ["人气题材", number(summary.hot_count), "个", "warn"],
    ].map(function (m) {
      return '<div class="m-sum-metric' + (m[3] ? " m-sum-" + m[3] : "") + '"><span>' + escapeHtml(m[0]) + "</span><strong>" + m[1] + '<small>' + escapeHtml(m[2]) + "</small></strong></div>";
    }).join("");

    const list = items.length ? '<div class="m-theme-list">' + items.map(function (item, index) {
      return '<button class="m-theme-item" type="button" data-theme-code="' + escapeHtml(item.code) + '">' +
        '<span class="m-theme-rank">' + (index + 1) + "</span>" +
        '<span class="m-theme-copy"><strong>' + escapeHtml(item.name) + "</strong>" +
        "<small>" + number(item.member_count) + " 只成分" + (item.hot_rank ? " · 人气第 " + number(item.hot_rank) : "") + "</small></span>" +
        '<b class="' + changeClass(item.change) + '">' + (item.has_quote ? (number(item.change) > 0 ? "+" : "") + formatNumber(number(item.change), 2) + "%" : "--") + "</b>" +
        "</button>";
    }).join("") + "</div>" : emptyHtml();

    scroll.innerHTML = '<div class="m-sum-strip">' + metrics + "</div>" + list;
  }

  function loadThemeDetail(code) {
    const seq = nextSeq();
    const url = "/api/themes/detail?code=" + encodeURIComponent(code) + "&trade_date=" + encodeURIComponent(state.requestedDate);
    global.MobileAPI.request(url).then(function (payload) {
      if (seq !== state.seq) return;
      state.themesDetail = payload;
      renderThemeDetailSheet(payload);
    }).catch(function (error) {
      if (seq !== state.seq) return;
      const body = document.getElementById("m-theme-detail-body");
      if (body) body.innerHTML = errorHtml(error.message || "题材详情加载失败");
    });
  }

  function openThemeDetailSheet(code) {
    openSheet(
      '<div class="m-sheet-head"><h2>题材详情</h2>' +
      '<button class="m-sheet-close" type="button" data-sheet-close aria-label="关闭">' + icon("close", 20) + "</button></div>" +
      '<div class="m-sheet-body" id="m-theme-detail-body">' + skeletonHtml(6) + "</div>",
      { detail: true }
    );
    loadThemeDetail(code);
  }

  function renderThemeDetailSheet(payload) {
    const body = document.getElementById("m-theme-detail-body");
    if (!body) return;
    const theme = payload.theme || {};
    const summary = payload.summary || {};
    const meta = payload.meta || {};
    const metrics = [
      ["成分股", number(summary.member_count) + " 只", ""],
      ["有行情", number(summary.quoted_count) + " 只", ""],
      ["上涨", number(summary.up_count) + " 只", "up"],
      ["下跌", number(summary.down_count) + " 只", "down"],
      ["换手率", formatNumber(number(theme.turnover_rate), 2) + "%", ""],
    ].map(function (m) {
      return '<div class="m-sum-metric' + (m[2] ? " m-sum-" + m[2] : "") + '"><span>' + escapeHtml(m[0]) + "</span><strong>" + m[1] + "</strong></div>";
    }).join("");

    body.innerHTML =
      '<div class="m-theme-detail-head"><strong>' + escapeHtml(theme.name || "--") + "</strong>" +
      '<b class="' + changeClass(theme.change) + '">' + (number(theme.change) > 0 ? "+" : "") + formatNumber(number(theme.change), 2) + "%</b></div>" +
      '<div class="m-theme-detail-code">' + escapeHtml(theme.code || "--") + " · " + escapeHtml(displayCompactDate(meta.trade_date) || "--") + "</div>" +
      '<div class="m-sum-strip">' + metrics + "</div>" +
      '<div class="m-section-title">成分股</div>' +
      '<div class="m-table-wrap" id="m-theme-members-table"></div>';
    mountSortableTable("m-theme-members-table", "market/themes/members", function () {
      return (state.themesDetail && state.themesDetail.members) || [];
    }, {});
  }

  /* ----- 龙虎榜 ----- */

  function loadDragon() {
    const seq = nextSeq();
    const date = state.requestedDate;
    global.MobileAPI.request("/api/dragon-tiger?trade_date=" + encodeURIComponent(date)).then(function (payload) {
      if (seq !== state.seq) return;
      state.dragon = payload;
      const meta = payload.meta || {};
      state.requestedDate = displayCompactDate(meta.trade_date || date);
      updateHeader(findLabel(state.key) || state.key, state.requestedDate);
      renderDragon();
    }).catch(function (error) {
      if (seq !== state.seq) return;
      renderError(error && error.message ? error.message : "龙虎榜加载失败");
    });
  }

  function loadDragonProfiles() {
    const seq = nextSeq();
    global.MobileAPI.request("/api/dragon-tiger/profiles").then(function (payload) {
      if (seq !== state.seq) return;
      state.dragonProfiles = payload;
      renderDragonProfiles();
    }).catch(function (error) {
      if (seq !== state.seq) return;
      const scroll = document.getElementById("m-scroll");
      if (scroll) scroll.innerHTML = errorHtml(error.message || "游资档案加载失败");
    });
  }

  function dragonTraders() {
    const payload = state.dragon || {};
    return (payload.traders || []).filter(function (item) {
      return item.identity_type === "trader" && item.recognized !== false;
    });
  }

  function renderDragon() {
    const scroll = document.getElementById("m-scroll");
    if (!scroll) return;
    const payload = state.dragon;
    if (!payload) return;
    const summary = payload.summary || {};
    const status = payload.meta && payload.meta.status;
    const traders = dragonTraders();
    const unclassified = payload.unclassified_seats || [];

    const unavailable = ["error", "unavailable"].indexOf(status) >= 0;
    const empty = status === "empty";
    if (unavailable || (empty && !traders.length && !unclassified.length)) {
      scroll.innerHTML = '<div class="m-state m-motion-rise-in"><span class="m-state-icon">' + icon("inbox", 26) + "</span>" +
        "<p>" + escapeHtml(unavailable ? "龙虎榜数据暂不可用" : payload.meta.trade_date + " 暂无龙虎榜明细") + "</p>" +
        "<small>龙虎榜明细通常在盘后陆续披露，可稍后刷新或查看前一交易日。</small></div>";
      return;
    }

    const modeTabs = '<div class="m-source-tabs" role="tablist" aria-label="龙虎榜视图">' +
      '<button class="m-source-tab active" type="button" data-action="dragon-mode" data-mode="daily">当日</button>' +
      '<button class="m-source-tab" type="button" data-action="dragon-mode" data-mode="profiles">游资档案</button>' +
      "</div>";

    const metrics = [
      ["上榜游资", number(summary.trader_count) + " 位", ""],
      ["操作明细", number(summary.operation_count) + " 条", ""],
      ["席位净买入", formatMoneyMillion(summary.seat_net_buy_million), changeClass(summary.seat_net_buy_million)],
      ["活跃股票", number(summary.active_stock_count) + " 只", ""],
    ].map(function (m) {
      return '<div class="m-sum-metric' + (m[2] ? " m-sum-" + m[2] : "") + '"><span>' + escapeHtml(m[0]) + "</span><strong>" + m[1] + "</strong></div>";
    }).join("");

    const cards = traders.map(function (trader, index) {
      const desc = trader.description || number(trader.stock_count) + " 只股票，" + number(trader.operation_count) + " 笔操作";
      return '<button class="m-dragon-card" type="button" data-dragon-trader="' + escapeHtml(trader.id) + '">' +
        '<span class="m-dragon-card-rank">' + String(index + 1).padStart(2, "0") + "</span>" +
        '<span class="m-dragon-card-mono">' + escapeHtml(trader.name.slice(0, 2)) + "</span>" +
        '<span class="m-dragon-card-copy"><strong>' + escapeHtml(trader.name) + "</strong>" +
        "<small>" + escapeHtml(desc) + "</small></span>" +
        '<span class="m-dragon-card-stats"><small>' + number(trader.stock_count) + " 股 · " + number(trader.operation_count) + " 笔</small>" +
        '<b class="' + changeClass(trader.net_buy_million) + '">' + formatMoneyMillion(trader.net_buy_million) + "</b></span></button>";
    }).join("");

    scroll.innerHTML = modeTabs +
      '<div class="m-sum-strip">' + metrics + "</div>" +
      '<div class="m-section-title">上榜游资</div>' +
      '<div class="m-dragon-list">' + (cards || emptyHtml()) + "</div>" +
      (unclassified.length ? '<div class="m-dragon-unclassified">另有 ' + number(unclassified.length) + " 个待归类席位</div>" : "");
  }

  function renderDragonProfiles() {
    const scroll = document.getElementById("m-scroll");
    if (!scroll) return;
    const payload = state.dragonProfiles;
    if (!payload) return;
    const profiles = payload.profiles || [];
    const summary = payload.summary || {};
    const modeTabs = '<div class="m-source-tabs" role="tablist" aria-label="龙虎榜视图">' +
      '<button class="m-source-tab" type="button" data-action="dragon-mode" data-mode="daily">当日</button>' +
      '<button class="m-source-tab active" type="button" data-action="dragon-mode" data-mode="profiles">游资档案</button>' +
      "</div>";

    const metrics = [
      ["收录游资", number(summary.profile_count), ""],
      ["已有简介", number(summary.described_count), ""],
      ["关联席位", number(summary.organization_count), ""],
    ].map(function (m) {
      return '<div class="m-sum-metric"><span>' + escapeHtml(m[0]) + "</span><strong>" + m[1] + "</strong></div>";
    }).join("");

    const list = profiles.length ? '<div class="m-profile-list">' + profiles.map(function (profile, index) {
      return '<button class="m-profile-row" type="button" data-profile-id="' + escapeHtml(profile.id) + '">' +
        '<span class="m-profile-mono">' + escapeHtml(profile.name.slice(0, 2)) + "</span>" +
        '<span class="m-profile-copy"><strong>' + escapeHtml(profile.name) + "</strong>" +
        "<small>" + escapeHtml(profile.description || "暂未收录简介") + "</small></span>" +
        '<span class="m-profile-seat">' + number(profile.organization_count) + " 席</span></button>";
    }).join("") + "</div>" : emptyHtml();

    scroll.innerHTML = modeTabs + '<div class="m-sum-strip">' + metrics + "</div>" +
      '<div class="m-section-title">游资名录（收录 ' + number(summary.profile_count) + " 位）</div>" + list;
  }

  function openDragonTraderSheet(id) {
    const payload = state.dragon || {};
    const traders = dragonTraders();
    const trader = traders.find(function (t) { return t.id === id; });
    if (!trader) return;
    const ops = trader.operations || [];
    openSheet(
      '<div class="m-sheet-head m-detail-head"><div class="m-detail-head-left">' +
      '<span class="m-detail-head-name">' + escapeHtml(trader.name) + "</span>" +
      '<span class="m-detail-head-code">' + number(trader.stock_count) + " 股 · " + number(trader.operation_count) + " 笔</span></div>" +
      '<button class="m-sheet-close" type="button" data-sheet-close aria-label="关闭">' + icon("close", 20) + "</button></div>" +
      '<div class="m-sheet-body" id="m-dragon-ops-body">' +
      '<div class="m-dragon-trader-totals">' +
      totRow("买入", trader.buy_million, "up") +
      totRow("卖出", trader.sell_million, "down") +
      totRow("净额", trader.net_buy_million, changeClass(trader.net_buy_million)) +
      "</div>" +
      '<div class="m-table-wrap" id="m-dragon-ops-table"></div>' +
      "</div>",
      { detail: true }
    );
    mountSortableTable("m-dragon-ops-table", "market/dragon/operations", function () { return ops; }, {});
  }

  function totRow(label, value, cls) {
    return '<div class="m-total"><span>' + escapeHtml(label) + "</span><b class='" + cls + "'>" + formatMoneyMillion(value) + "</b></div>";
  }

  function openProfileSheet(id) {
    const profiles = (state.dragonProfiles && state.dragonProfiles.profiles) || [];
    const profile = profiles.find(function (p) { return p.id === id; });
    if (!profile) return;
    const orgs = profile.organizations || [];
    openSheet(
      '<div class="m-sheet-head"><h2>游资档案</h2>' +
      '<button class="m-sheet-close" type="button" data-sheet-close aria-label="关闭">' + icon("close", 20) + "</button></div>" +
      '<div class="m-sheet-body">' +
      '<div class="m-profile-detail-head"><span class="m-profile-mono">' + escapeHtml(profile.name.slice(0, 2)) + "</span>" +
      "<div><strong>" + escapeHtml(profile.name) + "</strong><small>" + (orgs.length ? "关联 " + orgs.length + " 个公开席位" : "暂无关联席位") + "</small></div></div>" +
      '<div class="m-section-title">人物简介</div>' +
      '<p class="m-profile-desc">' + escapeHtml(profile.description || "名录暂未收录该游资的公开简介。") + "</p>" +
      (orgs.length ? '<div class="m-section-title">关联营业部</div><div class="m-profile-orgs">' +
        orgs.map(function (o) { return "<span>" + escapeHtml(o) + "</span>"; }).join("") + "</div>" : "") +
      (payloadNotice(state.dragonProfiles) || "") +
      "</div>",
      { detail: false }
    );
  }

  function payloadNotice(payload) {
    const meta = payload && payload.meta;
    return meta && meta.notice ? '<p class="m-profile-notice">' + escapeHtml(meta.notice) + "</p>" : "";
  }

  /* ---------------------------------------------------------------- helpers shared by new pages */

  function bindConfirmAction(onConfirm) {
    const ok = document.querySelector("[data-confirm-ok]");
    if (ok && typeof onConfirm === "function") {
      ok.addEventListener("click", function () {
        closeSheet();
        onConfirm();
      });
    }
  }

  function openCenteredDialog(content) {
    const root = ensureSheetRoot();
    sheetToken += 1;
    root.classList.add("is-dialog");
    root.innerHTML =
      '<div class="m-sheet-backdrop" data-sheet-backdrop></div>' +
      '<div class="m-dialog" role="dialog" aria-modal="true">' + content + "</div>";
    global.requestAnimationFrame(function () { root.classList.add("is-open"); });
  }

  function openConfirmSheet(title, body, options) {
    const opts = options || {};
    const confirmLabel = opts.confirmLabel || "确定";
    const cancelLabel = opts.cancelLabel || "取消";
    const danger = Boolean(opts.danger);
    if (opts.centered) {
      openCenteredDialog(
        "<h2>" + escapeHtml(title) + "</h2>" +
        (body ? "<p>" + escapeHtml(body) + "</p>" : "") +
        '<div class="m-dialog-actions">' +
        '<button class="m-btn-outline" type="button" data-sheet-close>' + escapeHtml(cancelLabel) + "</button>" +
        '<button class="' + (danger ? "m-btn-outline-danger" : "m-btn-primary") + '" type="button" data-confirm-ok>' + escapeHtml(confirmLabel) + "</button>" +
        "</div>"
      );
      bindConfirmAction(opts.onConfirm);
      return;
    }
    openSheet(
      '<div class="m-sheet-head"><h2>' + escapeHtml(title) + '</h2>' +
      '<button class="m-sheet-close" type="button" data-sheet-close aria-label="关闭">' + icon("close", 20) + '</button></div>' +
      '<div class="m-sheet-body m-confirm-body">' +
      (body ? '<p class="m-confirm-text">' + escapeHtml(body) + '</p>' : '') +
      '<div class="m-confirm-actions">' +
      '<button class="m-btn-primary m-btn-cancel" type="button" data-sheet-close>' + escapeHtml(cancelLabel) + '</button>' +
      '<button class="m-btn-primary ' + (danger ? 'm-btn-danger' : '') + '" type="button" data-confirm-ok>' + escapeHtml(confirmLabel) + '</button>' +
      '</div></div>',
      { detail: false }
    );
    bindConfirmAction(opts.onConfirm);
  }

  function nextSeq() {
    state.seq += 1;
    return state.seq;
  }

  function renderStrip5(cells) {
    // cells: [{label, value, tone?}] — 5 cells for 跟踪/复盘汇总条；窄屏自适应
    return '<div class="m-strip m-strip--5">' +
      cells.map(function (cell) {
        const tone = cell.tone ? ' m-strip-' + cell.tone : '';
        return '<div class="m-strip-cell' + tone + '">' +
          '<span class="m-strip-label">' + escapeHtml(cell.label) + '</span>' +
          '<span class="m-strip-value">' + (cell.value || '--') + '</span>' +
          '</div>';
      }).join('') + '</div>';
  }

  /* ============================================================== 智能选股 + 策略跟踪 ============================================================== */

  const SCREENER_VIEW_LABELS = {
    latest: "最新候选",
    active: "持续有效",
    history: "入选历史"
  };

  function screenerCurrentStrategy(payload) {
    return findScreenerStrategy(payload, state.screener.strategyId);
  }

  function screenerCurrentLibrary(payload) {
    if (!state.screener.strategyId) return "smart";
    const s = findScreenerStrategy(payload, state.screener.strategyId);
    if (!s) return "smart";
    return (s.formula && s.formula.meta && s.formula.meta.library) || "smart";
  }

  function screenerLibraryMode(library) {
    if (library === "curated") return "curated";
    if (library === "quant" || library === "custom") return "quant";
    return "smart";
  }

  // 按当前策略名在 recent_results / latest_results 中匹配最新一次运行（对齐电脑端 activeScreenerResultEntry）
  function screenerRunForCurrentStrategy(payload) {
    if (!payload) return null;
    const strategy = screenerCurrentStrategy(payload);
    if (!strategy || !strategy.name) return null;
    const name = strategy.name;
    const recent = (payload.recent_results || []).slice().reverse();
    for (let i = 0; i < recent.length; i += 1) {
      const run = recent[i];
      if (run && run.meta && run.meta.strategy_name === name) return run;
    }
    const latest = (payload.latest_results || {})[screenerLibraryMode(screenerCurrentLibrary(payload))];
    if (latest && latest.meta && latest.meta.strategy_name === name) return latest;
    return null;
  }

  // 持续有效 / 入选历史：生产 /api/screener/setup 只返回 recent_results / latest_results，
  // 从 recent_results 里筛出当前策略（meta.strategy_name 匹配，与 screenerRunForCurrentStrategy 同口径）的运行，逐候选组装。
  function screenerArchiveRows(payload, view) {
    if (!payload) return [];
    const strategy = screenerCurrentStrategy(payload);
    const strategyName = strategy ? strategy.name : "";
    if (!strategyName) return [];
    const rows = [];
    (payload.recent_results || []).forEach(function (run) {
      if (!run || !run.meta || run.meta.strategy_name !== strategyName) return;
      const meta = run.meta;
      const active = screenerSignalActive(run, payload);
      if (view === "active" && !active) return;
      (run.candidates || []).forEach(function (c) {
        const code = String(c.code || "");
        if (!code) return;
        rows.push({
          code: code,
          name: c.name || "",
          sector: c.sector || "",
          pct_chg: c.pct_chg,
          return_5d: c.return_5d,
          volume_ratio_5d: c.volume_ratio_5d,
          sector_strength: c.sector_strength,
          historical_probability: c.historical_probability,
          score_display: c.score_display,
          selection_date: displayCompactDate(String(meta.trade_date || ""))
        });
      });
    });
    rows.sort(function (a, b) {
      return String(b.selection_date || "").localeCompare(String(a.selection_date || ""));
    });
    return rows;
  }

  // 信号是否仍在有效期：阶段/精选策略按「阶段不变即有效」（meta.regime == 当前阶段）；
  // 量化策略按策略频率对应的交易日数，以 meta.trade_date 与数据日比较（无交易日历时按自然日近似）。
  function screenerSignalActive(run, payload) {
    const meta = (run && run.meta) || {};
    const mode = String(meta.mode || "smart");
    if (mode === "smart" || mode === "curated") {
      const regime = payload && payload.regime ? String(payload.regime.id || "") : "";
      return regime ? String(meta.regime || "") === regime : true;
    }
    const freq = String((((run && run.formula) || {}).meta || {}).frequency || "每日");
    const days = { "每周": 5, "双周": 10, "月度": 20, "事件驱动": 5 }[freq] || 1;
    const selected = String(meta.trade_date || "").replaceAll("-", "");
    const asOf = String((payload && payload.trade_date) || "").replaceAll("-", "");
    if (!selected || !asOf || selected === asOf) return true;
    const start = parseLocalDate(displayCompactDate(selected));
    const end = parseLocalDate(displayCompactDate(asOf));
    const diff = Math.round((end.getTime() - start.getTime()) / 86400000);
    return diff >= 0 && diff < days;
  }

  function screenerViewKey(payload, view) {
    if (!payload) return [];
    if (view === "active") return screenerArchiveRows(payload, "active");
    if (view === "history") return screenerArchiveRows(payload, "history");
    const run = screenerRunForCurrentStrategy(payload);
    if (!run) return [];
    return run.candidates || [];
  }

  function screenerCurrentRun(payload) {
    if (!payload) return null;
    if (state.screener.view === "active") return null;
    if (state.screener.view === "history") return null;
    return screenerRunForCurrentStrategy(payload);
  }

  function screenerCurrentRunId() {
    const payload = state.screener.data;
    if (!payload) return 0;
    const run = screenerCurrentRun(payload);
    return run ? Number(run.run_id || 0) : 0;
  }

  function screenerFollowedCodes() {
    const tracking = state.tracking.data;
    if (!tracking || !tracking.batches) return {};
    const map = {};
    tracking.batches.forEach(function (batch) {
      (batch.items || []).forEach(function (item) {
        if (item && item.code) map[String(item.code)] = item;
      });
    });
    return map;
  }

  function findScreenerStrategy(payload, strategyId) {
    const list = (payload && payload.strategies) || [];
    if (!strategyId) return null;
    for (let i = 0; i < list.length; i += 1) {
      if (String(list[i].id) === String(strategyId)) return list[i];
    }
    return null;
  }

  function findScreenerStrategyByName(payload, name) {
    const list = (payload && payload.strategies) || [];
    if (!name) return null;
    for (let i = 0; i < list.length; i += 1) {
      if (list[i].name === name) return list[i];
    }
    return null;
  }

  function pickInitialScreenerStrategy(payload) {
    const list = (payload && payload.strategies) || [];
    // 默认选 smart 阶段策略
    const stage = list.find(function (s) {
      const lib = s.formula && s.formula.meta && s.formula.meta.library;
      return lib === "smart";
    });
    if (stage) return stage;
    if (list.length) return list[0];
    return null;
  }

  function setupScreenerPage(key) {
    document.getElementById("m-view").classList.add("m-view-feature");
    state.screener = {
      data: null,
      date: todayString(),
      view: "latest",
      strategyId: "",
      strategyName: "",
      loading: false,
    };
    updateHeader("智能选股", displayCompactDate(state.screener.date));
    document.getElementById("m-actions").innerHTML =
      '<button class="m-header-btn m-date-btn" type="button" data-screener-date aria-label="选择日期">' +
      icon("calendar", 20) + '<span class="m-date-btn-label">' + escapeHtml(displayCompactDate(state.screener.date)) + '</span></button>';
    document.getElementById("m-view").innerHTML = complexFrame(key, complexScroll(skeletonHtml(8)));
  }

  function loadScreener() {
    state.screener.loading = true;
    const seq = nextSeq();
    const date = state.screener.date || todayString();
    const url = "/api/screener/setup?trade_date=" + encodeURIComponent(date);
    global.MobileAPI.request(url).then(function (payload) {
      if (seq !== state.seq) return;
      state.screener.data = payload;
      state.screener.date = payload.requested_trade_date || payload.trade_date || date;
      // 自动选择默认策略（首次进入时）
      if (!state.screener.strategyId) {
        const def = pickInitialScreenerStrategy(payload);
        if (def) {
          state.screener.strategyId = def.id;
          state.screener.strategyName = def.name;
        }
      }
      updateHeader("智能选股", displayCompactDate(state.screener.date));
      renderScreener();
    }).catch(function (error) {
      if (seq !== state.seq) return;
      renderError(error && error.message ? error.message : "选股数据加载失败");
    }).then(function () {
      state.screener.loading = false;
    });
  }

  function renderScreener() {
    const scroll = document.getElementById("m-scroll");
    if (!scroll) return;
    const payload = state.screener.data;
    if (!payload) { scroll.innerHTML = skeletonHtml(6); return; }
    const regime = payload.regime || {};
    const stageLabel = regime.label || regime.id || "--";
    const confidence = regime.confidence != null ? Math.round(Number(regime.confidence)) : null;
    const reason = regime.reason || regime.description || "";
    const updatedAt = formatScreenerUpdatedAt(regime.updated_at);
    const factors = (payload.factor_data && payload.factor_data.date_count) || 0;

    const stageCard =
      '<div class="m-card m-screener-stage">' +
      '<div class="m-screener-stage-row">' +
      '<span class="m-screener-stage-label">当前阶段</span>' +
      '<span class="m-screener-stage-time">' + escapeHtml(updatedAt) + '</span>' +
      '</div>' +
      '<div class="m-screener-stage-row">' +
      '<strong class="m-screener-stage-name">' + escapeHtml(stageLabel) + '</strong>' +
      (confidence != null ? '<span class="m-screener-stage-conf">置信度 ' + confidence + '%</span>' : '') +
      '</div>' +
      (reason ? '<p class="m-screener-stage-reason">' + escapeHtml(reason) + '</p>' : '') +
      '</div>';

    // 视图切换 pill 行
    const tabs = Object.keys(SCREENER_VIEW_LABELS).map(function (key) {
      const active = state.screener.view === key;
      return '<button class="m-source-tab' + (active ? " active" : "") + '" type="button" role="tab" aria-selected="' + (active ? "true" : "false") + '" data-screener-view="' + key + '">' + escapeHtml(SCREENER_VIEW_LABELS[key]) + '</button>';
    }).join("");
    const viewTabs = '<div class="m-source-tabs m-screener-views" role="tablist" aria-label="选股视图">' + tabs + '</div>';

    // 当前策略胶囊行
    const strategyName = state.screener.strategyName || "选择策略";
    const statusText = screenerStrategyStatusText(payload);
    const strategyRow =
      '<button class="m-card m-screener-strategy" type="button" data-screener-open-drawer>' +
      '<span class="m-screener-strategy-icon">' + icon("filter", 16) + '</span>' +
      '<span class="m-screener-strategy-name">' + escapeHtml(strategyName) + '</span>' +
      (statusText ? '<span class="m-screener-strategy-status">' + escapeHtml(statusText) + '</span>' : '') +
      '<span class="m-screener-strategy-chevron">' + icon("chevron-down", 16) + '</span>' +
      '</button>';

    // 表格容器
    const tableWrap = '<div class="m-table-wrap" id="m-screener-table"></div>';
    const foot = '<p class="m-screener-foot">结果由盘后自动更新 · 自定义策略请在电脑端新建/编译</p>';

    scroll.innerHTML = stageCard + viewTabs + strategyRow + tableWrap + foot;
    renderScreenerTable();
  }

  function screenerStrategyStatusText(payload) {
    if (!state.screener.strategyId) return "选择策略";
    const strategy = findScreenerStrategy(payload, state.screener.strategyId);
    if (strategy && strategy.published_run) {
      const pub = strategy.published_run;
      const detail = pub.detail || "";
      if (pub.status === "ready") return detail;
      if (pub.status === "missing_data") return "数据不足";
      if (pub.status === "not_run") return "等待盘后";
    }
    if (strategy && strategy.missing_data && strategy.missing_data.length) return "数据不足";
    if (state.screener.view === "active") return "查看持续有效";
    if (state.screener.view === "history") return "查看入选历史";
    const rows = screenerViewKey(payload, state.screener.view);
    if (rows.length) return rows.length + " 只候选";
    return "暂无信号";
  }

  function screenerEmptyText(payload) {
    const status = screenerStrategyStatusText(payload);
    if (status === "数据不足") return "该策略数据不足，暂无法出候选";
    if (status === "等待盘后") return "该策略等待盘后更新";
    if (status === "选择策略") return "请先选择一个策略查看结果";
    if (state.screener.view === "active") return "该策略暂无持续有效信号";
    if (state.screener.view === "history") return "该策略暂无入选历史";
    return "该策略当日暂无信号";
  }

  function formatScreenerUpdatedAt(value) {
    if (!value) return "盘后已更新";
    const text = String(value);
    if (text.length >= 5 && text.indexOf(":") > 0) {
      return "盘后 " + text + " 已更新";
    }
    if (text.length >= 10) return displayCompactDate(text) + " 已更新";
    return "盘后已更新";
  }

  function renderScreenerTable() {
    const wrap = document.getElementById("m-screener-table");
    if (!wrap) return;
    const payload = state.screener.data;
    if (!payload) { wrap.innerHTML = ""; return; }
    const rows = screenerViewKey(payload, state.screener.view);
    if (!rows.length) {
      wrap.innerHTML = '<div class="m-state m-motion-rise-in">' +
        '<span class="m-state-icon">' + icon("inbox", 26) + '</span>' +
        '<p>' + escapeHtml(screenerEmptyText(payload)) + '</p>' +
        '<small>可切换其他策略查看结果</small>' +
        '<button class="m-btn-primary m-btn-retry" type="button" data-screener-open-drawer style="margin-top:16px;">切换策略</button>' +
        '</div>';
      return;
    }
    // 跟踪状态映射（加入跟踪后置灰）
    const followed = screenerFollowedCodes();
    rows.forEach(function (r) { r.tracked = followed[String(r.code)] ? true : false; });

    const primaryColumns = [];
    if (state.screener.view === "history") {
      primaryColumns.push({ key: "selection_date", label: "入选日", type: "text", width: 76 });
    }
    primaryColumns.push(
      { key: "score_display", label: "综合分", type: "score" },
      { key: "pct_chg", label: "涨幅%", type: "change" },
      { key: "sector", label: "板块", type: "text" }
    );
    const cols = {
      frozenColumns: [
        { key: "stock", label: "股票", type: "stock", width: 96 }
      ],
      primaryColumns: primaryColumns,
      scrollColumns: [
        { key: "historical_probability", label: "历史胜率%", type: "rate" },
        { key: "return_5d", label: "5日%", type: "change" },
        { key: "volume_ratio_5d", label: "量比", type: "rate" },
        { key: "sector_strength", label: "板块强度", type: "rate" }
      ]
    };
    state.sortTable = { cols: cols, reapply: renderScreenerTable };
    const head = orderedColumns(cols).map(function (col, i) {
      const isFrozen = i < cols.frozenColumns.length;
      const sortable = sortableColumn(col);
      const active = sortable && state.sort.key === col.key && Boolean(state.sort.dir);
      const indicator = sortable ? sortIndicatorHtml(active) : "";
      const extraCls = sortable ? " m-sortable" + (active ? " is-sorted" : "") : "";
      const extraAttrs = sortable ? ' data-sort-key="' + escapeHtml(col.key) + '" aria-sort="' + (active ? (state.sort.dir === "asc" ? "ascending" : "descending") : "none") + '"' : '';
      return '<th class="m-th ' + colAlign(col) + (isFrozen ? " m-frozen" : "") + extraCls + '" style="min-width:' + columnWidth(col) + 'px' + (isFrozen ? ";width:" + columnWidth(col) + "px;left:0" : "") + '"' + extraAttrs + '>' +
        '<span class="m-th-inner">' + escapeHtml(col.label) + indicator + '</span></th>';
    }).join("");

    const sorted = sortedRows(rows, cols);
    const body = sorted.map(function (row, rIndex) {
      const cells = orderedColumns(cols).map(function (col, i) {
        const isFrozen = i < cols.frozenColumns.length;
        const width = columnWidth(col);
        const cls = "m-td " + colAlign(col) + (isFrozen ? " m-frozen" : "");
        const style = "min-width:" + width + "px" + (isFrozen ? ";width:" + width + "px;left:0" : "");
        let html = cellHtml(col, row, rIndex);
        if (col.key === "score_display") {
          const n = Number(row.score_display);
          if (Number.isFinite(n) && n >= 80) {
            html = '<span class="m-num m-score-up">' + formatNumber(n, 1) + '</span>';
          }
        }
        return '<td class="' + cls + '" style="' + style + '">' + html + '</td>';
      }).join("");
      const code = /^\d{6}$/.test(String(row.code || "")) ? ' data-code="' + escapeHtml(row.code) + '"' : "";
      return '<tr' + code + '>' + cells + '</tr>';
    }).join("");

    wrap.innerHTML = '<table class="m-table"><thead><tr>' + head + '</tr></thead><tbody>' + body + '</tbody></table>';
  }

  function openScreenerStrategyDrawer() {
    const payload = state.screener.data;
    if (!payload) return;
    const list = (payload.strategies || []).map(function (s) {
      const meta = (s.formula && s.formula.meta) || {};
      return {
        name: s.name,
        library: meta.library || "",
        category: meta.category || "",
        id: s.id,
        regimes: s.regimes || [],
        published_run: s.published_run || null,
        missing_data: s.missing_data || []
      };
    });
    if (!list.length) {
      list.push({ id: "smart", name: "智能阶段", library: "smart", category: "周期策略", published_run: null, missing_data: [] });
    }
    openSheet(
      '<div class="m-sheet-head"><h2>选择策略</h2>' +
      '<button class="m-sheet-close" type="button" data-sheet-close aria-label="关闭">' + icon("close", 20) + '</button></div>' +
      '<div class="m-sheet-body" id="m-screener-drawer-body"></div>',
      { detail: false }
    );
    const body = document.getElementById("m-screener-drawer-body");
    body.innerHTML = screenerDrawerContent(list);
    const input = body.querySelector("[data-screener-drawer-search]");
    input.addEventListener("input", function () {
      body.innerHTML = screenerDrawerContent(list, input.value, body.dataset.cat);
    });
    body.addEventListener("click", function (event) {
      const pill = event.target.closest("[data-screener-cat]");
      if (pill) {
        body.dataset.cat = pill.dataset.screenerCat;
        body.innerHTML = screenerDrawerContent(list, input.value, body.dataset.cat);
        return;
      }
      const row = event.target.closest("[data-screener-row]");
      if (row) {
        const id = row.dataset.screenerRow;
        const strat = list.find(function (s) { return String(s.id) === String(id); });
        if (strat) {
          state.screener.strategyId = strat.id;
          state.screener.strategyName = strat.name;
        }
        closeSheet();
        renderScreener();
        return;
      }
    });
  }

  function screenerStrategyRegimeText(s) {
    const regimes = (s && s.regimes) || [];
    if (!regimes.length) return "";
    const all = (state.screener.data && state.screener.data.regimes) || [];
    const labels = [];
    regimes.forEach(function (id) {
      const r = all.find(function (x) { return x.id === id; });
      labels.push(r ? r.label : id);
    });
    if (!labels.length) return "";
    if (all.length > 0 && labels.length >= all.length) return "全阶段";
    return labels.join(" / ");
  }

  function screenerStrategyStatusTextFor(s, payload) {
    if (s.published_run) {
      const pub = s.published_run;
      if (pub.status === "ready") return pub.detail || "数据完整";
      if (pub.status === "missing_data") return "数据不足";
      if (pub.status === "not_run") return "等待盘后";
    }
    if (s.missing_data && s.missing_data.length) return "数据不足";
    return "暂无信号";
  }

  function screenerDrawerContent(list, filter, cat) {
    const kw = (filter || "").trim().toLowerCase();
    const activeCat = cat || "all";
    const cats = [{ key: "all", label: "全部" }, { key: "基本面", label: "基本面" }, { key: "趋势", label: "趋势" }, { key: "短线", label: "短线" }, { key: "动量", label: "动量" }, { key: "量化", label: "量化" }, { key: "事件", label: "事件" }, { key: "资金", label: "资金" }];
    const catPills = cats.map(function (c) {
      const active = activeCat === c.key;
      return '<button class="m-source-tab' + (active ? " active" : "") + '" type="button" data-screener-cat="' + escapeHtml(c.key) + '">' + escapeHtml(c.label) + '</button>';
    }).join("");
    const filtered = list.filter(function (s) {
      if (kw && String(s.name).toLowerCase().indexOf(kw) < 0) return false;
      if (activeCat !== "all") {
        const cat = s.category || s.library || "";
        if (String(cat) !== activeCat) return false;
      }
      return true;
    });
    const rows = filtered.map(function (s) {
      const isCurrent = String(s.id) === String(state.screener.strategyId);
      const status = screenerStrategyStatusTextFor(s, state.screener.data);
      const statusCls = status === "数据不足" ? 'm-strategy-status--warn' : (status === "暂无信号" || status === "等待盘后" ? 'm-strategy-status--muted' : '');
      const regimeText = screenerStrategyRegimeText(s);
      return '<button class="m-screener-drawer-row' + (isCurrent ? ' is-current' : '') + '" type="button" data-screener-row="' + escapeHtml(String(s.id)) + '">' +
        '<span class="m-screener-drawer-main">' +
        '<span class="m-screener-drawer-name">' + escapeHtml(s.name) + (isCurrent ? ' <span class="m-strategy-check">' + icon("check", 16) + '</span>' : '') + '</span>' +
        (regimeText ? '<small class="m-screener-drawer-sub">' + escapeHtml(regimeText) + '</small>' : '') +
        '</span>' +
        '<span class="m-strategy-status ' + statusCls + '">' + escapeHtml(status) + '</span>' +
        '</button>';
    }).join("") || '<div class="m-state m-state--error"><p>无匹配策略</p></div>';
    return '<div class="m-form-field m-screener-drawer-search"><span class="m-screener-drawer-search-icon">' + icon("search", 16) + '</span><input type="search" placeholder="搜索策略" data-screener-drawer-search></div>' +
      '<div class="m-source-tabs m-screener-drawer-cats" role="tablist">' + catPills + '</div>' +
      '<div class="m-screener-drawer-section">' +
      '<span class="m-screener-drawer-section-title">选择策略</span>' +
      '<span class="m-screener-drawer-section-count">' + filtered.length + ' 套</span>' +
      '</div>' +
      '<div class="m-screener-drawer-list">' + rows + '</div>';
  }

  function setupTrackingPage(key) {
    document.getElementById("m-view").classList.add("m-view-feature");
    state.tracking = { data: null, loading: false, refreshing: false };
    updateHeader("策略跟踪", "");
    document.getElementById("m-actions").innerHTML =
      '<button class="m-header-btn" type="button" data-tracking-refresh aria-label="更新跟踪">' +
      icon("refresh-cw", 20) + '</button>';
    document.getElementById("m-view").innerHTML = complexFrame(key, complexScroll(skeletonHtml(6)));
  }

  function loadTracking() {
    state.tracking.loading = true;
    const seq = nextSeq();
    global.MobileAPI.request("/api/screener/tracking?limit=12").then(function (payload) {
      if (seq !== state.seq) return;
      state.tracking.data = payload;
      renderTracking();
    }).catch(function (error) {
      if (seq !== state.seq) return;
      renderError(error && error.message ? error.message : "跟踪数据加载失败");
    }).then(function () {
      state.tracking.loading = false;
    });
  }

  function renderTracking() {
    const scroll = document.getElementById("m-scroll");
    if (!scroll) return;
    const data = state.tracking.data || {};
    const summary = data.summary || {};
    const batches = data.batches || [];
    const items = [];
    batches.forEach(function (b) {
      (b.items || []).forEach(function (it) { items.push(Object.assign({ batch: b }, it)); });
    });
    items.sort(function (a, b) {
      return String(b.batch.selection_date || "").localeCompare(String(a.batch.selection_date || ""));
    });

    if (!items.length) {
      scroll.innerHTML = '<div class="m-state m-motion-rise-in">' +
        '<span class="m-state-icon">' + icon("target", 26) + '</span>' +
        '<p>暂无跟踪记录</p>' +
        '<small>在智能选股页将候选加入跟踪，5 个交易日内会在这里自动汇总表现</small>' +
        '<button class="m-btn-primary m-btn-retry" type="button" data-route="#/feature/tools/screener" style="margin-top:16px;">去智能选股加跟踪</button>' +
        '</div>';
      return;
    }

    const stripHtml = renderStrip5([
      { label: "跟踪标的", value: String(summary.total != null ? summary.total : items.length) },
      { label: "已观察", value: String(summary.observed != null ? summary.observed : 0) },
      { label: "T+1胜率", value: summary.t1_win_rate != null ? summary.t1_win_rate + "%" : "--" },
      { label: "T+5胜率", value: summary.t5_win_rate != null ? summary.t5_win_rate + "%" : "--" },
      { label: "T+5平均", value: summary.average_t5 != null ? (summary.average_t5 > 0 ? "+" : "") + summary.average_t5 + "%" : "--", tone: summary.average_t5 > 0 ? "up" : (summary.average_t5 < 0 ? "down" : "") }
    ]);

    const cols = {
      frozenColumns: [
        { key: "stock", label: "股票", type: "stock", width: 96 }
      ],
      primaryColumns: [
        { key: "status_text", label: "状态", type: "text" },
        { key: "max_gain", label: "最大涨幅", type: "change" }
      ],
      scrollColumns: [
        { key: "selection_date", label: "入选日", type: "text" },
        { key: "strategy_name", label: "策略", type: "text", wide: true },
        { key: "entry_price", label: "入场价", type: "price" },
        { key: "t1_close", label: "T+1收", type: "change" },
        { key: "t3_close", label: "T+3", type: "change" },
        { key: "t5_close", label: "T+5", type: "change" },
        { key: "max_drawdown", label: "最大回撤", type: "change" }
      ]
    };
    const rows = items.map(function (it) {
      return {
        code: it.code,
        name: it.name,
        sector: it.sector,
        status: it.status,
        status_text: it.status || "--",
        max_gain: it.max_gain,
        selection_date: displayCompactDate(it.batch.selection_date || ""),
        strategy_name: it.batch.strategy_name || "--",
        entry_price: it.entry_price,
        t1_close: it.t1_close,
        t3_close: it.t3_close,
        t5_close: it.t5_close,
        max_drawdown: it.max_drawdown,
        track_id: it.id
      };
    });

    // 表头
    const head = orderedColumns(cols).map(function (col, i) {
      const isFrozen = i < cols.frozenColumns.length;
      const sortable = sortableColumn(col) && col.key !== "status_text";
      const active = sortable && state.sort.key === col.key && Boolean(state.sort.dir);
      const indicator = sortable ? sortIndicatorHtml(active) : "";
      const extraCls = sortable ? " m-sortable" + (active ? " is-sorted" : "") : "";
      const extraAttrs = sortable ? ' data-sort-key="' + escapeHtml(col.key) + '" aria-sort="' + (active ? (state.sort.dir === "asc" ? "ascending" : "descending") : "none") + '"' : '';
      return '<th class="m-th ' + colAlign(col) + (isFrozen ? " m-frozen" : "") + extraCls + '" style="min-width:' + columnWidth(col) + 'px' + (isFrozen ? ";width:" + columnWidth(col) + "px;left:0" : "") + '"' + extraAttrs + '>' +
        '<span class="m-th-inner">' + escapeHtml(col.label) + indicator + '</span></th>';
    }).join("");

    state.sortTable = { cols: cols, reapply: renderTracking };
    const sorted = sortedRows(rows, cols);
    const body = sorted.map(function (row, rIndex) {
      const cells = orderedColumns(cols).map(function (col, i) {
        const isFrozen = i < cols.frozenColumns.length;
        const width = columnWidth(col);
        const cls = "m-td " + colAlign(col) + (isFrozen ? " m-frozen" : "");
        const style = "min-width:" + width + "px" + (isFrozen ? ";width:" + width + "px;left:0" : "");
        let html;
        if (col.key === "status_text") {
          html = trackingStatusBadge(row.status);
        } else if (col.key === "max_gain") {
          const v = row.max_gain;
          if (v == null) html = '<span class="m-num">--</span>';
          else html = '<span class="m-num ' + (Number(v) >= 0 ? "up" : "down") + '">' + (Number(v) > 0 ? "+" : "") + formatNumber(Number(v), 2) + '</span>';
        } else {
          html = cellHtml(col, row, rIndex);
        }
        return '<td class="' + cls + '" style="' + style + '">' + html + '</td>';
      }).join("");
      const code = /^\d{6}$/.test(String(row.code || "")) ? ' data-code="' + escapeHtml(row.code) + '" data-track-id="' + escapeHtml(String(row.track_id || "")) + '"' : '';
      return '<tr' + code + '>' + cells + '</tr>';
    }).join("");

    const tableHtml = '<div class="m-table-wrap" id="m-tracking-table"><table class="m-table"><thead><tr>' + head + '</tr></thead><tbody>' + body + '</tbody></table></div>';

    scroll.innerHTML = stripHtml + tableHtml;
  }

  function trackingStatusBadge(status) {
    const text = status || "--";
    if (text.indexOf("已完成") === 0) return '<span class="m-tag m-tag--down">' + escapeHtml(text) + '</span>';
    if (text.indexOf("等待") === 0) return '<span class="m-tag m-tag--fail">' + escapeHtml(text) + '</span>';
    if (text.indexOf("跟踪中") === 0) return '<span class="m-tag m-tag--dual">' + escapeHtml(text) + '</span>';
    return '<span class="m-tag m-tag--fail">' + escapeHtml(text) + '</span>';
  }

  /* ---------------------------------------------------------------- screener/tracking detail sheets (composed on top of existing openDetailSheet) */

  function appendDetailGroup(sheet, title, rows) {
    return '<section class="m-detail-group"><h3 class="m-detail-group-title">' + escapeHtml(title) + '</h3>' +
      rows.map(function (r) {
        return '<div class="m-detail-group-row"><span class="m-detail-group-label">' + escapeHtml(r.label) + '</span>' +
          '<span class="m-detail-group-value ' + (r.cls || "") + '">' + r.value + '</span></div>';
      }).join("") + '</section>';
  }

  function openScreenerDetailSheet(code) {
    openDetailSheet(code);
    // 等详情体渲染完成后追加「选股信息」分组 + 「加入跟踪」按钮
    const attach = function () {
      const body = document.getElementById("m-detail-sheet-body");
      if (!body) return;
      // 找第一个空骨架或图表加载完成
      if (body.querySelector(".m-skeleton")) {
        return global.setTimeout(attach, 80);
      }
      const rows = screenerViewKey(state.screener.data, state.screener.view);
      const row = rows.find(function (r) { return String(r.code) === String(code); });
      if (!row) return;
      const runId = screenerCurrentRunId();
      const followed = screenerFollowedCodes()[String(code)];
      const isTracked = Boolean(followed);
      const contributions = Array.isArray(row.contributions) ? row.contributions : [];
      const riskFlags = Array.isArray(row.risk_flags) ? row.risk_flags : [];
      const contributionGroup = (row.reason || contributions.length)
        ? appendDetailGroup("主要贡献", contributions.length
          ? contributions.map(function (c) {
              return {
                label: c.label || "",
                value: (c.value != null && c.value !== "" ? escapeHtml(String(c.value)) : "--") + (c.points != null ? "（+" + formatNumber(Number(c.points), 1) + "分）" : "")
              };
            })
          : [{ label: "", value: '<span style="white-space:normal;">' + escapeHtml(row.reason || "") + '</span>' }])
        : "";
      const riskGroup = riskFlags.length
        ? appendDetailGroup("风险标记", riskFlags.map(function (f) {
            return { label: "", value: '<span style="white-space:normal;">' + escapeHtml(f) + '</span>' };
          }))
        : "";
      const html =
        appendDetailGroup("选股信息", [
          { label: "综合分", value: formatNumber(Number(row.score_display || 0), 1), cls: Number(row.score_display || 0) >= 80 ? "up" : "" },
          { label: "当日涨幅", value: row.pct_chg != null ? (Number(row.pct_chg) > 0 ? "+" : "") + formatNumber(Number(row.pct_chg), 2) + "%" : "--", cls: Number(row.pct_chg || 0) > 0 ? "up" : "down" },
          { label: "历史胜率", value: row.historical_probability != null ? formatNumber(Number(row.historical_probability), 1) + "%" : "--" },
          { label: "板块", value: escapeHtml(row.sector || "--") }
        ]) +
        contributionGroup +
        riskGroup +
        '<div class="m-screener-detail-actions">' +
        (isTracked
          ? '<button class="m-btn-primary m-btn-disabled" type="button" disabled>已加入跟踪</button>'
          : '<button class="m-btn-primary" type="button" data-screener-track data-run-id="' + runId + '" data-code="' + escapeHtml(code) + '">加入跟踪</button>') +
        '</div>';
      const foot = document.createElement("div");
      foot.className = "m-detail-foot";
      foot.innerHTML = html;
      body.appendChild(foot);
      const btn = body.querySelector("[data-screener-track]");
      if (btn) {
        btn.addEventListener("click", function () {
          addScreenerTrackingFromSheet(btn.dataset.runId, btn.dataset.code);
        });
      }
    };
    attach();
  }

  function addScreenerTrackingFromSheet(runId, code) {
    if (!runId || runId === "0" || !code) {
      showToast("当前结果不支持加入跟踪");
      return;
    }
    global.MobileAPI.request("/api/screener/tracking", "POST", { run_id: Number(runId), code: code }).then(function () {
      showToast("已加入跟踪");
      // 刷新 tracking 缓存
      return global.MobileAPI.request("/api/screener/tracking?limit=12");
    }).then(function (payload) {
      if (payload) state.tracking.data = payload;
      // 重渲染选股表中跟踪态
      renderScreenerTable();
      // 更新当前 sheet 中的按钮
      const body = document.getElementById("m-detail-sheet-body");
      if (body) {
        const actions = body.querySelector(".m-screener-detail-actions");
        if (actions) {
          actions.innerHTML = '<button class="m-btn-primary m-btn-disabled" type="button" disabled>已加入跟踪</button>';
        }
      }
    }).catch(function (err) {
      showToast((err && err.message) || "加入跟踪失败");
    });
  }

  function openTrackingDetailSheet(code, trackId) {
    openDetailSheet(code);
    const attach = function () {
      const body = document.getElementById("m-detail-sheet-body");
      if (!body) return;
      if (body.querySelector(".m-skeleton")) {
        return global.setTimeout(attach, 80);
      }
      const data = state.tracking.data || {};
      let item = null;
      let batch = null;
      (data.batches || []).forEach(function (b) {
        (b.items || []).forEach(function (it) {
          if (String(it.id) === String(trackId) || String(it.code) === String(code)) {
            item = it;
            batch = b;
          }
        });
      });
      if (!item) return;
      const html =
        appendDetailGroup("跟踪信息", [
          { label: "入选日", value: escapeHtml(displayCompactDate(batch.selection_date || "")) },
          { label: "策略", value: escapeHtml(batch.strategy_name || "--") },
          { label: "入场价", value: item.entry_price != null ? formatNumber(Number(item.entry_price), 2) : "--" },
          { label: "T+1开", value: item.t1_open != null ? (Number(item.t1_open) > 0 ? "+" : "") + formatNumber(Number(item.t1_open), 2) + "%" : "--" },
          { label: "T+1收", value: item.t1_close != null ? (Number(item.t1_close) > 0 ? "+" : "") + formatNumber(Number(item.t1_close), 2) + "%" : "--" },
          { label: "T+3收", value: item.t3_close != null ? (Number(item.t3_close) > 0 ? "+" : "") + formatNumber(Number(item.t3_close), 2) + "%" : "--" },
          { label: "T+5收", value: item.t5_close != null ? (Number(item.t5_close) > 0 ? "+" : "") + formatNumber(Number(item.t5_close), 2) + "%" : "--" },
          { label: "最大涨幅", value: item.max_gain != null ? (Number(item.max_gain) > 0 ? "+" : "") + formatNumber(Number(item.max_gain), 2) + "%" : "--", cls: "up" },
          { label: "最大回撤", value: item.max_drawdown != null ? formatNumber(Number(item.max_drawdown), 2) + "%" : "--", cls: "down" }
        ]) +
        '<div class="m-tracking-detail-actions">' +
        '<button class="m-btn-primary m-btn-danger" type="button" data-tracking-remove data-track-id="' + escapeHtml(String(trackId)) + '">移除跟踪</button>' +
        '</div>';
      const foot = document.createElement("div");
      foot.className = "m-detail-foot";
      foot.innerHTML = html;
      body.appendChild(foot);
      const btn = body.querySelector("[data-tracking-remove]");
      if (btn) {
        btn.addEventListener("click", function () {
          openConfirmSheet("移除跟踪", "移除后该股将不再自动更新表现，但历史成交已记录保留。", {
            danger: true,
            confirmLabel: "移除",
            onConfirm: function () { removeTracking(btn.dataset.trackId); }
          });
        });
      }
    };
    attach();
  }

  function removeTracking(trackId) {
    if (!trackId) return;
    global.MobileAPI.request("/api/screener/tracking/" + encodeURIComponent(trackId), "DELETE").then(function () {
      showToast("已移除跟踪");
      closeSheet();
      return global.MobileAPI.request("/api/screener/tracking?limit=12");
    }).then(function (payload) {
      if (payload) {
        state.tracking.data = payload;
        renderTracking();
      }
    }).catch(function (err) {
      showToast((err && err.message) || "移除失败");
    });
  }

  function refreshTracking() {
    if (state.tracking.refreshing) return;
    state.tracking.refreshing = true;
    global.MobileAPI.request("/api/screener/tracking/refresh", "POST", {}).then(function (payload) {
      showToast(payload && payload.notice ? payload.notice : "已更新");
      if (payload && payload.tracking) {
        state.tracking.data = payload.tracking;
        renderTracking();
      } else {
        return global.MobileAPI.request("/api/screener/tracking?limit=12");
      }
    }).then(function (payload) {
      if (payload) {
        state.tracking.data = payload;
        renderTracking();
      }
    }).catch(function (err) {
      showToast((err && err.message) || "刷新失败");
    }).then(function () {
      state.tracking.refreshing = false;
      const icon = document.querySelector("[data-tracking-refresh] svg");
      if (icon) icon.classList.remove("is-spinning");
    });
  }

  /* ============================================================== 复盘助手 + 问师 聊天工作台 ============================================================== */

  const ASSISTANT_PRESET_QUESTIONS = [
    "市场位置",
    "市场主线",
    "交易复盘",
    "明日清单"
  ];

  function setupChatPage(key) {
    document.getElementById("m-view").classList.add("m-view-feature");
    state.chat = {
      page: key,
      messages: [],
      mentorId: "",
      mentorName: "",
      mentorTagline: "",
      mentorGrade: "",
      mentorFocus: [],
      mentorSetup: null,
      followUps: [],
      streaming: false,
      streamController: null,
      streamBuffer: "",
      streamBubble: null,
      loading: false,
      historyLoaded: false,
      aborter: null
    };
    const isAssistant = key === "assistant/chat";
    // 复盘助手是底栏直达（无 back）；问师从智能工具图标页进入（有 back）；m-actions 由本函数自己控制
    global.MobileRouter.updateHeader({ title: isAssistant ? "复盘助手" : "问师", back: !isAssistant, actions: "" });
    document.getElementById("m-view").innerHTML = buildChatShell(key);
    const actions = [];
    if (!isAssistant) {
      actions.push('<button class="m-header-btn" type="button" data-chat-mentors aria-label="导师目录">' + icon("bot", 22) + '</button>');
    }
    actions.push('<button class="m-header-btn" type="button" data-chat-clear aria-label="清空对话">' + icon("trash", 22) + '</button>');
    document.getElementById("m-actions").innerHTML = actions.join("");
    bindChatShell(key);
  }

  function buildChatShell(key) {
    const isAssistant = key === "assistant/chat";
    return '<div class="m-page m-chat-page" data-page="' + escapeHtml(key) + '">' +
      (isAssistant ? '' : '<div class="m-chat-mentor-bar" id="m-chat-mentor-bar" style="display:none"></div>') +
      '<div class="m-chat-stream" id="m-chat-stream">' + chatSkeletonHtml() + '</div>' +
      '<div class="m-chat-input-bar" id="m-chat-input-bar">' +
        '<div class="m-chat-input-wrap">' +
          '<textarea id="m-chat-input" class="m-chat-input" rows="1" placeholder="问点什么…" maxlength="2000" enterkeyhint="send"></textarea>' +
        '</div>' +
        '<button class="m-chat-send" type="button" data-chat-send aria-label="发送">' + icon("send", 20) + '</button>' +
      '</div>' +
    '</div>';
  }

  function chatSkeletonHtml() {
    return '<div class="m-chat-skeleton">' +
      '<div class="m-chat-bubble-skel m-chat-bubble-skel--ai" style="width:70%"></div>' +
      '<div class="m-chat-bubble-skel m-chat-bubble-skel--user" style="width:55%;align-self:flex-end"></div>' +
      '<div class="m-chat-bubble-skel m-chat-bubble-skel--ai" style="width:80%"></div>' +
      '</div>';
  }

  function bindChatShell(key) {
    const isAssistant = key === "assistant/chat";
    const input = document.getElementById("m-chat-input");
    if (input) {
      // 避免重复绑定：每次 setupChatPage 都新建 input 元素
      input.addEventListener("input", function () {
        autoSizeChatInput();
        syncChatSendEnabled();
      });
      input.addEventListener("keydown", function (event) {
        if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
          event.preventDefault();
          if (!state.chat.streaming) submitChatMessage();
        }
      });
    }
    syncChatSendEnabled();
    autoSizeChatInput();
  }

  function autoSizeChatInput() {
    const input = document.getElementById("m-chat-input");
    if (!input) return;
    input.style.height = "auto";
    const max = 116; // ~4 lines
    const next = Math.min(max, Math.max(44, input.scrollHeight));
    input.style.height = next + "px";
  }

  function syncChatSendEnabled() {
    const input = document.getElementById("m-chat-input");
    const btn = document.querySelector("[data-chat-send]");
    if (!btn) return;
    if (state.chat.streaming) {
      btn.classList.add("m-chat-send--stop");
      btn.setAttribute("aria-label", "停止");
      btn.innerHTML = icon("square", 14);
      btn.disabled = false;
      return;
    }
    btn.classList.remove("m-chat-send--stop");
    btn.setAttribute("aria-label", "发送");
    btn.innerHTML = icon("send", 20);
    const text = input ? input.value.trim() : "";
    btn.disabled = !text;
  }

  function loadChat() {
    if (state.chat.page === "assistant/chat") {
      loadAssistantHistory();
    } else {
      loadMentorSetup();
    }
  }

  function loadAssistantHistory() {
    state.chat.loading = true;
    const seq = nextSeq();
    global.MobileAPI.request("/api/assistant/messages").then(function (payload) {
      if (seq !== state.seq) return;
      const items = (payload && payload.items) || [];
      state.chat.messages = items.map(function (it) {
        return { role: it.role, content: String(it.content || ""), created_at: it.created_at || "" };
      });
      state.chat.historyLoaded = true;
      renderChatStream();
    }).catch(function (error) {
      if (seq !== state.seq) return;
      renderChatStreamError(error && error.message ? error.message : "对话历史加载失败");
    }).then(function () {
      state.chat.loading = false;
    });
  }

  function loadMentorSetup() {
    state.chat.loading = true;
    const seq = nextSeq();
    global.MobileAPI.request("/api/mentors/setup?trade_date=" + encodeURIComponent(todayString())).then(function (payload) {
      if (seq !== state.seq) return;
      state.chat.mentorSetup = payload;
      // 无已选导师时保持空态（spec §1.5）：不自动选中导师，等用户点「选择导师」或顶栏 bot 开抽屉
      if (!state.chat.mentorId) {
        // 空态，等待选择
      } else {
        // 已选导师，更新名称/标签
        const cur = ((payload && payload.mentors) || []).find(function (m) { return m.id === state.chat.mentorId; });
        if (cur) {
          state.chat.mentorName = cur.name || state.chat.mentorName;
          state.chat.mentorTagline = cur.tagline || state.chat.mentorTagline;
          state.chat.mentorGrade = (cur.evidence && cur.evidence.grade) || state.chat.mentorGrade;
          state.chat.mentorFocus = cur.focus || state.chat.mentorFocus;
        }
        loadMentorHistory();
      }
      renderChatMentorBar();
      renderChatStream();
    }).catch(function (error) {
      if (seq !== state.seq) return;
      renderChatStreamError(error && error.message ? error.message : "导师目录加载失败");
    }).then(function () {
      state.chat.loading = false;
    });
  }

  function loadMentorHistory() {
    if (!state.chat.mentorId) return;
    const seq = nextSeq();
    const url = "/api/mentors/messages?mentor_id=" + encodeURIComponent(state.chat.mentorId) + "&trade_date=" + encodeURIComponent(todayString());
    global.MobileAPI.request(url).then(function (payload) {
      if (seq !== state.seq) return;
      const items = (payload && payload.items) || [];
      state.chat.messages = items.map(function (it) {
        return { role: it.role, content: String(it.content || ""), created_at: it.created_at || "" };
      });
      state.chat.historyLoaded = true;
      renderChatStream();
    }).catch(function (error) {
      if (seq !== state.seq) return;
      renderChatStreamError(error && error.message ? error.message : "对话历史加载失败");
    });
  }

  function selectMentor(id, opts) {
    const payload = state.chat.mentorSetup;
    if (!payload) return;
    const mentors = payload.mentors || [];
    const m = mentors.find(function (x) { return x.id === id; });
    if (!m) return;
    state.chat.mentorId = m.id;
    state.chat.mentorName = m.name;
    state.chat.mentorTagline = m.tagline || "";
    state.chat.mentorGrade = (m.evidence && m.evidence.grade) || "";
    state.chat.mentorFocus = m.focus || [];
    state.chat.messages = [];
    state.chat.followUps = [];
    if (!opts || !opts.silent) {
      showToast("已切换到「" + state.chat.mentorName + "」");
    }
    if (typeof closeSheet === "function") closeSheet();
    renderChatMentorBar();
    renderChatStream();
    loadMentorHistory();
  }

  function clearAssistantMessages() {
    global.MobileAPI.request("/api/assistant/messages", "DELETE").then(function () {
      state.chat.messages = [];
      state.chat.followUps = [];
      renderChatStream();
      showToast("已清空对话");
    }).catch(function (err) {
      showToast((err && err.message) || "清空失败");
    });
  }

  function clearMentorMessages(mentorId, tradeDate) {
    const url = "/api/mentors/messages?mentor_id=" + encodeURIComponent(mentorId) + "&trade_date=" + encodeURIComponent(tradeDate);
    global.MobileAPI.request(url, "DELETE").then(function () {
      state.chat.messages = [];
      state.chat.followUps = [];
      renderChatStream();
      showToast("已清空当日对话");
    }).catch(function (err) {
      showToast((err && err.message) || "清空失败");
    });
  }

  function renderChatStream() {
    const stream = document.getElementById("m-chat-stream");
    if (!stream) return;
    const isAssistant = state.chat.page === "assistant/chat";
    const messages = state.chat.messages || [];
    if (state.chat.streaming) {
      // 渲染中只更新最新气泡文本
      const bubble = stream.querySelector("[data-stream-bubble]");
      if (bubble) {
        bubble.innerHTML = renderChatMarkdown(state.chat.streamBuffer);
        scrollChatToBottom();
      }
      return;
    }
    let html = "";
    if (!messages.length) {
      if (isAssistant) {
        html =
          chatBubbleHtml("ai", "我是你的复盘助手，可以问我市场位置、主线、交易复盘、明日清单。") +
          '<div class="m-chat-empty-hint">从下面挑一个开始，或直接输入你的问题</div>' +
          chatPresetPillsHtml();
      } else if (state.chat.mentorId) {
        // 显示导师 tagline 原文（spec §1.5）
        const tagline = state.chat.mentorTagline || "今天想问点什么？";
        html = chatBubbleHtml("ai", tagline) +
          '<div class="m-chat-empty-hint">基于今日市场数据回答；引用数据时已带日期</div>';
      } else {
        html = '<div class="m-state m-state--empty"><p>请先选择一位导师</p>' +
          '<button class="m-btn-primary m-btn-retry" type="button" data-chat-mentors>选择导师</button></div>';
      }
    } else {
      html = messages.map(function (msg, idx) {
        return chatBubbleHtmlWithTimestamp(msg, idx, messages);
      }).join("");
    }
    stream.innerHTML = html;
    scrollChatToBottom();
    renderChatFollowUps();
  }

  function renderChatStreamError(message) {
    const stream = document.getElementById("m-chat-stream");
    if (!stream) return;
    stream.innerHTML = '<div class="m-state m-state--error m-motion-fade-in"><p>' + escapeHtml(message) + '</p>' +
      '<button class="m-btn-primary m-btn-retry" type="button" data-chat-retry>重试</button></div>';
    const btn = stream.querySelector("[data-chat-retry]");
    if (btn) btn.addEventListener("click", loadChat);
  }

  function chatBubbleHtml(role, content) {
    const isUser = role === "user";
    return '<div class="m-chat-msg m-chat-msg--' + (isUser ? "user" : "ai") + ' m-motion-rise-in">' +
      '<div class="m-chat-bubble">' + (isUser ? escapeHtml(content) : renderChatMarkdown(content)) + '</div>' +
      '</div>';
  }

  function chatBubbleHtmlWithTimestamp(msg, idx, all) {
    const isUser = msg.role === "user";
    let stamp = "";
    if (msg.created_at && idx > 0) {
      const prev = all[idx - 1];
      if (prev && prev.created_at && shouldInsertTimestamp(prev.created_at, msg.created_at)) {
        stamp = '<div class="m-chat-stamp">' + escapeHtml(chatTimestampLabel(msg.created_at)) + '</div>';
      }
    }
    return stamp + chatBubbleHtml(isUser ? "user" : "ai", msg.content);
  }

  function shouldInsertTimestamp(prevIso, currIso) {
    try {
      const a = new Date(prevIso).getTime();
      const b = new Date(currIso).getTime();
      return (b - a) > 5 * 60 * 1000;
    } catch (_error) { return false; }
  }

  function chatTimestampLabel(iso) {
    try {
      const d = new Date(iso);
      const now = new Date();
      const sameDay = d.toDateString() === now.toDateString();
      if (sameDay) return "今天 " + String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
      const yest = new Date(now.getTime() - 24 * 3600 * 1000);
      if (d.toDateString() === yest.toDateString()) return "昨天 " + String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
      return displayCompactDate(iso) + " " + String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
    } catch (_error) { return displayCompactDate(iso) || ""; }
  }

  // 极简 markdown：加粗 / 列表 / 引用 / 标题（与电脑端限制保持一致；不渲染图片/表格/代码）
  function renderChatMarkdown(text) {
    if (!text) return "";
    const lines = String(text).split("\n");
    const out = [];
    let inList = false;
    let inQuote = false;
    for (let i = 0; i < lines.length; i += 1) {
      const raw = lines[i];
      const line = escapeHtml(raw);
      if (/^\s*[-*]\s+/.test(raw)) {
        if (!inList) { out.push("<ul>"); inList = true; }
        if (inQuote) { out.push("</blockquote>"); inQuote = false; }
        out.push("<li>" + line.replace(/^\s*[-*]\s+/, "").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>") + "</li>");
        continue;
      }
      if (inList) { out.push("</ul>"); inList = false; }
      if (/^>\s?/.test(raw)) {
        if (!inQuote) { out.push("<blockquote>"); inQuote = true; }
        out.push(line.replace(/^>\s?/, "").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>"));
        continue;
      }
      if (inQuote) { out.push("</blockquote>"); inQuote = false; }
      if (/^###\s+/.test(raw)) {
        out.push("<h4>" + line.replace(/^###\s+/, "").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>") + "</h4>");
        continue;
      }
      if (!line.trim()) { out.push(""); continue; }
      out.push("<p>" + line.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>") + "</p>");
    }
    if (inList) out.push("</ul>");
    if (inQuote) out.push("</blockquote>");
    return out.join("");
  }

  function renderChatMentorBar() {
    const bar = document.getElementById("m-chat-mentor-bar");
    if (!bar) return;
    if (state.chat.page === "assistant/chat") { bar.innerHTML = ""; bar.style.display = "none"; return; }
    if (!state.chat.mentorId) { bar.innerHTML = ""; bar.style.display = "none"; return; }
    bar.style.display = "flex";
    const grade = state.chat.mentorGrade || "";
    const gradeBadge = grade ? '<span class="m-mentor-grade m-mentor-grade--' + escapeHtml(grade.toLowerCase()) + '">' + escapeHtml(grade) + '</span>' : "";
    bar.innerHTML = '<button class="m-mentor-pill" type="button" data-chat-mentors>' +
      '<span class="m-mentor-pill-name">' + escapeHtml(state.chat.mentorName || "") + gradeBadge + '</span>' +
      '<span class="m-mentor-pill-close" data-chat-remove-mentor aria-label="清除导师">×</span>' +
      '</button>';
  }

  function chatPresetPillsHtml() {
    return '<div class="m-chat-presets">' +
      '<div class="m-chat-preset-list">' +
      ASSISTANT_PRESET_QUESTIONS.map(function (q) {
        return '<button class="m-source-tab" type="button" data-chat-preset="' + escapeHtml(q) + '">' + escapeHtml(q) + '</button>';
      }).join("") + '</div></div>';
  }

  function renderChatFollowUps() {
    const stream = document.getElementById("m-chat-stream");
    if (!stream) return;
    const old = stream.querySelector("[data-chat-followup-row]");
    if (old) old.remove();
    if (state.chat.streaming || !state.chat.followUps || !state.chat.followUps.length) return;
    const html = '<div class="m-chat-followup-row" data-chat-followup-row>' +
      state.chat.followUps.slice(0, 3).map(function (q) {
        return '<button class="m-source-tab" type="button" data-chat-followup="' + escapeHtml(q) + '">' + escapeHtml(q) + '</button>';
      }).join("") + '</div>';
    stream.insertAdjacentHTML("beforeend", html);
    scrollChatToBottom();
  }

  function scrollChatToBottom() {
    const stream = document.getElementById("m-chat-stream");
    if (!stream) return;
    global.requestAnimationFrame(function () {
      stream.scrollTop = stream.scrollHeight;
    });
  }

  function submitChatMessage() {
    const input = document.getElementById("m-chat-input");
    if (!input) return;
    const text = input.value.trim();
    if (!text) return;
    sendChatMessage(text);
  }

  function sendChatMessage(text) {
    if (state.chat.streaming) return;
    if (state.chat.page === "tools/mentor" && !state.chat.mentorId) {
      showToast("请先选择导师");
      return;
    }
    const userMsg = { role: "user", content: text, created_at: new Date().toISOString() };
    state.chat.messages = state.chat.messages.concat([userMsg]);
    state.chat.followUps = [];
    const input = document.getElementById("m-chat-input");
    if (input) { input.value = ""; autoSizeChatInput(); }
    renderChatStream();
    startChatStream(text);
  }

  function startChatStream(question) {
    if (state.chat.streaming) return;
    state.chat.streaming = true;
    state.chat.streamBuffer = "";
    state.chat.streamController = new AbortController();
    syncChatSendEnabled();
    // 插入等待气泡占位
    const stream = document.getElementById("m-chat-stream");
    if (stream) {
      stream.insertAdjacentHTML("beforeend",
        '<div class="m-chat-msg m-chat-msg--ai m-chat-msg--stream" data-stream-msg>' +
        '<div class="m-chat-bubble" data-stream-bubble>' +
        '<span class="m-chat-typing" aria-hidden="true"><i></i><i></i><i></i></span>' +
        '</div></div>');
      scrollChatToBottom();
    }
    renderChatFollowUps();

    const url = state.chat.page === "assistant/chat" ? "/api/assistant/chat" : "/api/mentors/chat";
    const body = state.chat.page === "assistant/chat"
      ? { question: question, trade_date: todayString() }
      : { mentor_id: state.chat.mentorId, question: question, trade_date: todayString() };

    const onEvent = function (event) {
      if (!event || !event.type) return;
      if (event.type === "delta" && typeof event.content === "string") {
        state.chat.streamBuffer += event.content;
        const bubble = document.querySelector("[data-stream-bubble]");
        if (bubble) {
          bubble.innerHTML = renderChatMarkdown(state.chat.streamBuffer) + '<span class="m-chat-cursor" aria-hidden="true"></span>';
          scrollChatToBottom();
        }
      } else if (event.type === "meta") {
        if (Array.isArray(event.follow_ups) && event.follow_ups.length) {
          state.chat.followUps = event.follow_ups.slice(0, 3);
        }
        renderChatFollowUps();
      }
    };

    global.MobileAPI.streamNdjson(url, {
      method: "POST",
      body: body,
      signal: state.chat.streamController.signal,
      onEvent: onEvent,
      errorMessage: state.chat.page === "assistant/chat" ? "智能解读失败" : "问师回复失败"
    }).then(function () {
      finalizeChatStream();
    }).catch(function (err) {
      const aborted = err && (err.name === "AbortError" || err.message === "请求已取消");
      handleChatStreamError(err, aborted);
    });
  }

  function finalizeChatStream() {
    if (!state.chat.streaming) return;
    const aiText = state.chat.streamBuffer;
    if (aiText) {
      const aiMsg = { role: "assistant", content: aiText, created_at: new Date().toISOString() };
      state.chat.messages = state.chat.messages.concat([aiMsg]);
    }
    state.chat.streaming = false;
    state.chat.streamController = null;
    state.chat.streamBuffer = "";
    state.chat.streamBubble = null;
    syncChatSendEnabled();
    renderChatStream();
  }

  function handleChatStreamError(err, aborted) {
    if (aborted) {
      // 用户主动停止
      if (state.chat.streamBuffer) {
        const aiMsg = { role: "assistant", content: state.chat.streamBuffer, created_at: new Date().toISOString() };
        state.chat.messages = state.chat.messages.concat([aiMsg]);
      }
      state.chat.streaming = false;
      state.chat.streamController = null;
      state.chat.streamBuffer = "";
      syncChatSendEnabled();
      renderChatStream();
      return;
    }
    state.chat.streaming = false;
    state.chat.streamController = null;
    state.chat.streamBuffer = "";
    syncChatSendEnabled();
    // 渲染错误气泡 + 重试
    const stream = document.getElementById("m-chat-stream");
    if (stream) {
      const placeholder = stream.querySelector("[data-stream-msg]");
      if (placeholder) placeholder.remove();
      stream.insertAdjacentHTML("beforeend",
        '<div class="m-chat-msg m-chat-msg--ai m-chat-msg--error">' +
        '<div class="m-chat-bubble m-chat-bubble--error">' +
        '<span class="m-chat-error-text">' + escapeHtml((err && err.message) || "网络异常，请重试") + '</span>' +
        '<button class="m-chat-retry" type="button" data-chat-retry>重试</button>' +
        '</div></div>');
      const btn = stream.querySelector("[data-chat-retry]");
      if (btn) btn.addEventListener("click", function () {
        // 找到上条用户消息并重试
        const last = [...state.chat.messages].reverse().find(function (m) { return m.role === "user"; });
        if (last) {
          state.chat.messages = state.chat.messages.slice(0, state.chat.messages.length - 1);
          renderChatStream();
          sendChatMessage(last.content);
        }
      });
      scrollChatToBottom();
    }
  }

  function stopChatStream() {
    if (!state.chat.streaming || !state.chat.streamController) return;
    try { state.chat.streamController.abort(); } catch (_error) { /* noop */ }
  }

  function openMentorDrawer() {
    const payload = state.chat.mentorSetup;
    if (!payload) {
      showToast("导师目录暂未加载");
      return;
    }
    const mentors = payload.mentors || [];
    const listHtml = mentors.map(function (m) {
      const grade = (m.evidence && m.evidence.grade) || "";
      const isCurrent = m.id === state.chat.mentorId;
      return '<button class="m-mentor-row' + (isCurrent ? " is-current" : "") + '" type="button" data-mentor-row="' + escapeHtml(m.id) + '">' +
        '<span class="m-mentor-row-main">' +
        '<strong class="m-mentor-row-name">' + escapeHtml(m.name) + '</strong>' +
        '<small class="m-mentor-row-tag">' + escapeHtml(m.tagline || "") + '</small>' +
        '</span>' +
        '<span class="m-mentor-row-grade m-mentor-grade m-mentor-grade--' + escapeHtml(String(grade).toLowerCase()) + '">' + escapeHtml(grade || "—") + '</span>' +
        (isCurrent ? '<span class="m-mentor-row-check">' + icon("check", 16) + '</span>' : '') +
        '</button>';
    }).join("") || '<div class="m-state m-state--error"><p>导师目录为空</p></div>';
    openSheet(
      '<div class="m-sheet-head"><h2>选择导师</h2>' +
      '<button class="m-sheet-close" type="button" data-sheet-close aria-label="关闭">' + icon("close", 20) + '</button></div>' +
      '<div class="m-sheet-body m-mentor-drawer-body">' +
      '<div class="m-form-field m-mentor-drawer-search"><span class="m-mentor-drawer-search-icon">' + icon("search", 16) + '</span><input type="search" placeholder="搜索导师" data-mentor-drawer-search></div>' +
      '<div class="m-mentor-drawer-list">' + listHtml + '</div>' +
      '</div>',
      { detail: false }
    );
    const sheetBody = document.querySelector(".m-sheet-body");
    if (!sheetBody) return;
    const input = sheetBody.querySelector("[data-mentor-drawer-search]");
    const list = sheetBody.querySelector(".m-mentor-drawer-list");
    if (input && list) {
      input.addEventListener("input", function () {
        const kw = input.value.trim().toLowerCase();
        const rows = list.querySelectorAll(".m-mentor-row");
        rows.forEach(function (row) {
          const name = (row.querySelector(".m-mentor-row-name") || {}).textContent || "";
          const tag = (row.querySelector(".m-mentor-row-tag") || {}).textContent || "";
          const hit = !kw || name.toLowerCase().indexOf(kw) >= 0 || tag.toLowerCase().indexOf(kw) >= 0;
          row.style.display = hit ? "" : "none";
        });
      });
    }
    // 导师行点击在 sheet 内绑定（m-view 上的全局 handler 抓不到 sheet 内点击）
    if (list) {
      list.addEventListener("click", function (event) {
        const row = event.target.closest("[data-mentor-row]");
        if (!row) return;
        const id = row.dataset.mentorRow;
        if (id) selectMentor(id);
      });
    }
  }

  /* ----- complex page registry ----- */

  function setupComplexPage(key) {
    resetComplexState(key);
    document.getElementById("m-view").classList.add("m-view-feature");
    updateHeader(findLabel(key) || key, state.requestedDate);
    document.getElementById("m-view").innerHTML = complexFrame(key, complexScroll(skeletonHtml(8)));
  }

  const COMPLEX_PAGES = {
    "market/sentiment": setupComplexPage,
    "market/ladder": setupComplexPage,
    "market/rotation": setupComplexPage,
    "market/auction": setupComplexPage,
    "market/themes": setupComplexPage,
    "market/dragon": setupComplexPage,
    "tools/screener": setupScreenerPage,
    "tools/tracking": setupTrackingPage,
    "tools/mentor": setupChatPage,
    "assistant/chat": setupChatPage,
    "review/watchlist": setupReviewPage,
    "review/trades": setupReviewPage,
    "review/daily": setupReviewPage,
    "review/notes": setupReviewPage,
    "review/alerts": setupReviewPage,
    "system/profile": setupSystemPage,
    "system/password": setupSystemPage,
    "system/membership": setupSystemPage,
    "system/admin": setupSystemPage,
    "system/members": setupSystemPage,
  };

  const COMPLEX_LOADERS = {
    "market/sentiment": loadSentiment,
    "market/ladder": loadLadder,
    "market/rotation": loadRotation,
    "market/auction": loadAuction,
    "market/themes": loadThemes,
    "market/dragon": loadDragon,
    "tools/screener": loadScreener,
    "tools/tracking": loadTracking,
    "tools/mentor": loadChat,
    "assistant/chat": loadChat,
    "review/watchlist": loadReview,
    "review/trades": loadReview,
    "review/daily": loadReview,
    "review/notes": loadReview,
    "review/alerts": loadReview,
    "system/profile": loadSystem,
    "system/password": loadSystem,
    "system/membership": loadSystem,
    "system/admin": loadSystem,
    "system/members": loadSystem,
  };

  function isComplexPage(key) {
    return Boolean(COMPLEX_PAGES[key]);
  }

  /* ---------------------------------------------------------------- sheets */

  function ensureSheetRoot() {
    let root = document.getElementById("m-sheet-root");
    if (!root) {
      root = document.createElement("div");
      root.id = "m-sheet-root";
      root.className = "m-sheet-root";
      document.getElementById("m-app").appendChild(root);
    }
    return root;
  }

  function openSheet(content, opts) {
    const root = ensureSheetRoot();
    sheetToken += 1;
    root.classList.remove("is-dialog");
    root.innerHTML =
      '<div class="m-sheet-backdrop" data-sheet-backdrop></div>' +
      '<div class="m-sheet' + (opts && opts.detail ? " m-sheet--detail" : "") + '" role="dialog" aria-modal="true">' +
      '<div class="m-sheet-handle" aria-hidden="true"></div>' +
      content +
      "</div>";
    global.requestAnimationFrame(function () { root.classList.add("is-open"); });
    bindSheetDrag(root);
  }

  function closeSheet() {
    const root = document.getElementById("m-sheet-root");
    if (!root) return;
    const token = sheetToken;
    root.classList.remove("is-open");
    root.classList.remove("is-dialog");
    global.setTimeout(function () {
      if (sheetToken === token && !root.classList.contains("is-open")) {
        root.innerHTML = "";
      }
    }, 340);
  }

  function bindSheetDrag(root) {
    if (root.classList.contains("is-dialog")) return;
    const sheet = root.querySelector(".m-sheet");
    const backdrop = root.querySelector(".m-sheet-backdrop");
    const handle = root.querySelector(".m-sheet-handle");
    const head = root.querySelector(".m-sheet-head");
    if (!sheet) return;
    let startY = 0;
    let startT = 0;
    let dragging = false;

    function begin(event) {
      if (event.target && event.target.closest(".m-sheet-close")) return;
      dragging = true;
      startY = event.clientY;
      startT = Date.now();
      sheet.style.transition = "none";
      if (backdrop) backdrop.style.transition = "none";
      if (event.currentTarget && event.currentTarget.setPointerCapture) {
        try { event.currentTarget.setPointerCapture(event.pointerId); } catch (e) { /* noop */ }
      }
    }

    function move(event) {
      if (!dragging) return;
      const dy = event.clientY - startY;
      if (dy > 0) {
        sheet.style.transform = "translateY(" + dy + "px)";
        if (backdrop) {
          const ratio = Math.min(1, dy / sheet.offsetHeight);
          backdrop.style.opacity = String(Math.max(0, 1 - ratio));
        }
      }
    }

    function end(event) {
      if (!dragging) return;
      dragging = false;
      sheet.style.transition = "";
      if (backdrop) { backdrop.style.opacity = ""; backdrop.style.transition = ""; }
      const dy = event.clientY - startY;
      const dt = Date.now() - startT;
      const velocity = dt > 0 ? dy / dt : 0;
      if (dy >= sheet.offsetHeight * 0.25 || (velocity >= 0.5 && dy >= 40)) {
        closeSheet();
      } else {
        sheet.style.transform = "";
      }
    }

    [handle, head].forEach(function (grip) {
      if (!grip) return;
      grip.style.touchAction = "none";
      grip.addEventListener("pointerdown", begin);
      grip.addEventListener("pointermove", move);
      grip.addEventListener("pointerup", end);
      grip.addEventListener("pointercancel", end);
    });
  }

  function selectDate(dateStr) {
    if (!dateStr) return;
    state.requestedDate = dateStr;
    if (state.key === "tools/screener") {
      state.screener.date = dateStr;
      state.screener.strategyId = "";
      state.screener.strategyName = "";
      const label = document.querySelector("[data-screener-date] .m-date-btn-label");
      if (label) label.textContent = displayCompactDate(dateStr);
    }
    closeSheet();
    reloadCurrent();
  }

  function openDateSheet() {
    const baseDate = state.key === "tools/screener" && state.screener.date
      ? state.screener.date
      : state.requestedDate;
    state.calCursor = {
      year: parseLocalDate(baseDate).getFullYear(),
      month: parseLocalDate(baseDate).getMonth(),
    };
    openSheet(
      '<div class="m-sheet-head">' +
      '<h2>选择日期</h2>' +
      '<button class="m-sheet-close" type="button" data-sheet-close aria-label="关闭">' + icon("close", 20) + "</button>" +
      "</div>" +
      '<div class="m-sheet-body" id="m-date-sheet-body"></div>',
      { detail: false }
    );
    renderDateSheetBody();
  }

  function renderDateSheetBody() {
    const body = document.getElementById("m-date-sheet-body");
    if (!body || !state.calCursor) return;
    const year = state.calCursor.year;
    const month = state.calCursor.month;
    const today = todayString();
    const now = parseLocalDate(today);
    const currentMonth = now.getFullYear() * 12 + now.getMonth();
    const cursorMonth = year * 12 + month;
    const prevDisabled = cursorMonth <= currentMonth - 12;
    const nextDisabled = cursorMonth >= currentMonth;

    const quick = [
      { label: "今天", date: todayString() },
      { label: "昨天", date: addDays(today, -1) },
      { label: "前一交易日", date: previousTradeDate() },
    ];

    const quickHtml = quick.map(function (item) {
      return '<button class="m-quick" type="button" data-quick-date="' + escapeHtml(item.date) + '">' + escapeHtml(item.label) + "</button>";
    }).join("");

    body.innerHTML =
      '<div class="m-quick-row">' + quickHtml + "</div>" +
      '<div class="m-cal-head">' +
      '<button class="m-cal-nav" type="button" data-cal-nav="prev"' + (prevDisabled ? " disabled" : "") + " aria-label=\"上个月\">" + icon("chevron-left", 18) + "</button>" +
      '<span class="m-cal-title">' + year + " 年 " + (month + 1) + " 月</span>" +
      '<button class="m-cal-nav" type="button" data-cal-nav="next"' + (nextDisabled ? " disabled" : "") + " aria-label=\"下个月\">" + icon("chevron-right", 18) + "</button>" +
      "</div>" +
      '<div class="m-cal-week">' + ["一", "二", "三", "四", "五", "六", "日"].map(function (d) { return "<span>" + d + "</span>"; }).join("") + "</div>" +
      '<div class="m-cal-grid">' + calendarCells(year, month) + "</div>";
  }

  function calendarCells(year, month) {
    const first = new Date(year, month, 1);
    const startWeekday = (first.getDay() + 6) % 7;
    const daysInMonth = new Date(year, month + 1, 0).getDate();
    const today = todayString();
    const cells = [];
    for (let i = 0; i < startWeekday; i += 1) cells.push('<span class="m-cal-cell is-empty"></span>');
    for (let d = 1; d <= daysInMonth; d += 1) {
      const date = new Date(year, month, d);
      const dateStr = localDateString(date);
      const dow = date.getDay();
      const disabled = dow === 0 || dow === 6 || dateStr > today;
      const selected = dateStr === state.requestedDate;
      cells.push(
        '<button class="m-cal-cell' + (selected ? " is-selected" : "") + '" type="button" data-date="' + dateStr + '"' + (disabled ? " disabled" : "") + ">" + d + "</button>"
      );
    }
    return cells.join("");
  }

  function previousTradeDate() {
    const dash = state.dashboard || {};
    if (dash.meta && dash.meta.previous_trade_date) return displayCompactDate(dash.meta.previous_trade_date);
    if (state.popularity && state.popularity.meta && state.popularity.meta.previous_trade_date) {
      return displayCompactDate(state.popularity.meta.previous_trade_date);
    }
    return previousWeekday(todayString());
  }

  function openDetailSheet(code) {
    openSheet(
      '<div class="m-sheet-head m-detail-head">' +
      '<div class="m-detail-head-left">' +
      '<span class="m-detail-head-name">--</span>' +
      '<span class="m-detail-head-code">' + escapeHtml(code) + "</span>" +
      "</div>" +
      '<div class="m-detail-head-right">' +
      '<button class="m-detail-star" type="button" data-action="watch" aria-label="加入自选" disabled>' + starIcon(false) + "</button>" +
      '<button class="m-sheet-close" type="button" data-sheet-close aria-label="关闭">' + icon("close", 20) + "</button>" +
      "</div></div>" +
      '<div class="m-sheet-body" id="m-detail-sheet-body">' +
      '<div class="m-skeleton">' +
      '<div class="m-skeleton-row"><span class="m-skeleton-bar"></span></div>' +
      '<div class="m-skeleton-row"><span class="m-skeleton-bar"></span></div>' +
      '<div class="m-skeleton-row"><span class="m-skeleton-bar"></span></div>' +
      "</div>" +
      "</div>",
      { detail: true }
    );
    loadDetail(code);
  }

  function loadDetail(code) {
    const token = sheetToken;
    global.MobileAPI.request("/api/stock/" + encodeURIComponent(code) + "/preview").then(function (payload) {
      if (token !== sheetToken) return;
      renderDetail(payload);
    }).catch(function (error) {
      if (token !== sheetToken) return;
      const body = document.getElementById("m-detail-sheet-body");
      if (body) body.innerHTML = errorHtml(error && error.message ? error.message : "行情预览加载失败");
    });
  }

  function chartCaptionHtml(tab) {
    const detail = state.detail;
    const payload = detail && detail.payload ? detail.payload : {};
    const meta = payload.meta || {};
    if (tab === "daily") {
      const bars = (payload.prices || []).slice(-48);
      const last = bars.length ? bars[bars.length - 1].trade_date : "";
      return "日线 · 近48根 · 至 " + (displayCompactDate(last) || "--");
    }
    const d = displayCompactDate(meta.intraday_trade_date) || displayCompactDate(meta.trade_date);
    return "分时 · " + (d || "--");
  }

  function switchChartTab(tab) {
    const detail = state.detail;
    if (!detail || !detail.payload) return;
    detail.tab = tab;
    const payload = detail.payload;
    const meta = payload.meta || {};
    const chart = document.getElementById("m-detail-chart");
    if (chart) {
      if (tab === "daily") {
        chart.innerHTML = dailyChart(payload);
      } else if (meta.intraday_status === "available") {
        chart.innerHTML = intradayChart(payload);
      } else {
        chart.innerHTML = emptyChart(meta.intraday_notice || "分时数据暂不可用");
      }
    }
    const caption = document.getElementById("m-detail-caption");
    if (caption) caption.textContent = chartCaptionHtml(tab);
    document.querySelectorAll(".m-detail-tab").forEach(function (btn) {
      const active = btn.dataset.chartTab === tab;
      btn.classList.toggle("active", active);
      btn.setAttribute("aria-selected", String(active));
    });
  }

  function updateStarButton() {
    const btn = document.querySelector(".m-detail-star");
    if (!btn) return;
    const watched = Boolean(state.detail && state.detail.watchlist);
    btn.classList.toggle("is-added", watched);
    btn.setAttribute("aria-label", watched ? "移出自选" : "加入自选");
    btn.setAttribute("aria-pressed", String(watched));
    btn.innerHTML = starIcon(watched);
    btn.disabled = false;
  }

  function toggleWatch() {
    const detail = state.detail;
    if (!detail || !detail.code) return;
    const btn = document.querySelector(".m-detail-star");
    const adding = !detail.watchlist;
    const code = detail.code;
    const url = adding ? "/api/watchlist" : "/api/watchlist/" + encodeURIComponent(code);
    const method = adding ? "POST" : "DELETE";
    const body = adding ? { code: code, name: detail.name || code, sector: detail.sector || "" } : null;
    if (btn) btn.disabled = true;
    global.MobileAPI.request(url, method, body).then(function () {
      detail.watchlist = adding;
      updateStarButton();
      showToast(adding ? "已加入自选" : "已移出自选");
      // 自选股列表页在背后：同步移除该行，避免关 Sheet 后仍显示已移除标的
      if (!adding && state.review && state.review.watchlist) {
        state.review.watchlist.items = (state.review.watchlist.items || []).filter(function (it) {
          return String(it.code) !== String(code);
        });
        renderReviewWatchlist();
      }
    }).catch(function () {
      updateStarButton();
      showToast("操作失败，请重试");
    });
  }

  function openRemarkEditSheet() {
    const detail = state.detail;
    if (!detail || !detail.code) return;
    const item = detail.watchItem || {};
    const current = item.remark || "";
    openSheet(
      '<div class="m-sheet-head"><h2>编辑跟踪备注</h2>' +
      '<button class="m-sheet-close" type="button" data-sheet-close aria-label="关闭">' + icon("close", 20) + "</button></div>" +
      '<div class="m-sheet-body">' +
      '<label class="m-form-field"><span>跟踪备注</span>' +
      '<div class="m-field-textarea">' +
      '<textarea id="m-remark-input" maxlength="240" rows="4" data-maxlength="240">' + escapeHtml(current) + "</textarea>" +
      '<span class="m-field-counter" data-counter-for="m-remark-input">' + String(current).length + "/240</span>" +
      "</div></label>" +
      '<div class="m-confirm-actions"><button class="m-btn-primary" type="button" data-remark-save>保存</button></div>' +
      "</div>",
      { detail: false }
    );
    const input = document.getElementById("m-remark-input");
    if (input) {
      input.addEventListener("input", function () {
        const counter = document.querySelector('[data-counter-for="m-remark-input"]');
        if (counter) counter.textContent = input.value.length + "/240";
      });
    }
  }

  function saveRemark() {
    const detail = state.detail;
    if (!detail || !detail.code) return;
    const item = detail.watchItem || {};
    const code = detail.code;
    const remark = document.getElementById("m-remark-input").value.trim();
    const payload = {
      code: code,
      name: item.name || detail.name || code,
      sector: item.sector || detail.sector || "其他",
      color: item.color || "red",
      remark: remark
    };
    global.MobileAPI.request("/api/watchlist", "POST", payload).then(function () {
      const refresh = state.review && state.review.watchlist
        ? global.MobileAPI.request("/api/watchlist?trade_date=" + encodeURIComponent(todayString())).then(function (res) {
          state.review.watchlist = res || state.review.watchlist;
          renderReviewWatchlist();
        })
        : Promise.resolve();
      return refresh;
    }).then(function () {
      closeSheet();
      openDetailSheet(code);
      showToast("备注已保存");
    }).catch(function (error) {
      showToast(error && error.message ? error.message : "保存失败");
    });
  }

  function showToast(message) {
    const root = ensureSheetRoot();
    if (!root) return;
    let toast = document.getElementById("m-toast");
    if (!toast) {
      toast = document.createElement("div");
      toast.id = "m-toast";
      toast.className = "m-toast";
      root.appendChild(toast);
    }
    toast.textContent = message;
    toast.classList.remove("is-visible");
    void toast.offsetWidth;
    toast.classList.add("is-visible");
    global.clearTimeout(toast._timer);
    toast._timer = global.setTimeout(function () {
      toast.classList.remove("is-visible");
    }, 1500);
  }

  function renderDetail(payload) {
    const body = document.getElementById("m-detail-sheet-body");
    if (!body) return;
    const stock = payload.stock || {};
    const meta = payload.meta || {};
    const name = stock.name && stock.name !== "--" ? stock.name : "--";
    const price = number(stock.price);
    const change = number(stock.change);
    const industry = stock.industry && stock.industry !== "其他" ? stock.industry : (stock.sector || "其他");

    state.detail = {
      code: stock.code || "",
      name: name === "--" ? "" : name,
      sector: industry === "其他" ? "" : industry,
      watchlist: Boolean(stock.watchlist),
      watchItem: stock.watchlist || null,
      tab: "intraday",
      payload: payload,
    };

    const headName = document.querySelector(".m-detail-head-name");
    const headCode = document.querySelector(".m-detail-head-code");
    if (headName) headName.textContent = name;
    if (headCode) headCode.textContent = stock.code || "";
    updateStarButton();

    const intradayOk = meta.intraday_status === "available";
    const chartHtml = intradayOk ? intradayChart(payload) : emptyChart(meta.intraday_notice || "分时数据暂不可用");

    const watchItem = stock.watchlist || null;
    const remarkRow = watchItem
      ? '<div class="m-remark-row">' +
        '<span class="m-remark-label">跟踪备注</span>' +
        '<span class="m-remark-text">' + escapeHtml(watchItem.remark || "未填写") + "</span>" +
        '<button class="m-remark-edit" type="button" data-remark-edit>编辑</button>' +
        "</div>"
      : "";

    body.innerHTML =
      '<div class="m-detail-top">' +
      '<div class="m-detail-quote">' +
      '<strong class="m-detail-price ' + changeClass(change) + '">' + (price ? formatNumber(price, 2) : "--") + "</strong>" +
      '<span class="m-detail-change ' + changeClass(change) + '">' + (change > 0 ? "+" : "") + formatNumber(change, 2) + "%</span>" +
      "</div>" +
      '<div class="m-detail-meta">' + escapeHtml(industry) + " · " + escapeHtml(displayCompactDate(meta.trade_date) || "--") + " 收盘</div>" +
      "</div>" +
      '<div class="m-detail-tabs" role="tablist" aria-label="行情图类型">' +
      '<button class="m-detail-tab active" type="button" role="tab" aria-selected="true" data-chart-tab="intraday">分时</button>' +
      '<button class="m-detail-tab" type="button" role="tab" aria-selected="false" data-chart-tab="daily">日线</button>' +
      '<span class="m-detail-chart-caption" id="m-detail-caption">' + escapeHtml(chartCaptionHtml("intraday")) + "</span>" +
      "</div>" +
      '<div class="m-detail-chart" id="m-detail-chart">' + chartHtml + "</div>" +
      remarkRow +
      '<div class="m-detail-note">点按图面可读数值 · 数据来源：本地行情</div>';
  }

  /* ---------------------------------------------------------------- charts */

  function emptyChart(message) {
    return '<div class="m-chart-empty">' + escapeHtml(message || "数据暂不可用") + "</div>";
  }

  function svgAxisText(x, y, anchor, cls, content) {
    return '<text x="' + x + '" y="' + y + '" text-anchor="' + anchor + '" class="m-chart-axis ' + cls + '">' + escapeHtml(content) + "</text>";
  }

  function dateMMDD(value) {
    const s = String(value || "");
    return s.length >= 10 ? s.slice(5, 10) : s;
  }

  function movingAverage(values, period) {
    const out = [];
    for (let i = 0; i < values.length; i += 1) {
      if (i < period - 1) { out.push(null); continue; }
      let sum = 0;
      for (let j = i - period + 1; j <= i; j += 1) sum += values[j];
      out.push(sum / period);
    }
    return out;
  }

  function maPath(ma, xf, yf, cls) {
    const pts = [];
    for (let i = 0; i < ma.length; i += 1) {
      if (ma[i] == null) continue;
      pts.push(xf(i).toFixed(1) + " " + yf(ma[i]).toFixed(1));
    }
    return pts.length > 1 ? '<polyline class="m-chart-ma ' + cls + '" points="' + pts.join(" ") + '"/>' : "";
  }

  function intradayChart(payload) {
    const W = 360, H = 240, padL = 8, padR = 52, padT = 10, padB = 22;
    const pw = W - padL - padR;
    const ph = H - padT - padB;
    const points = payload.intraday || [];
    const meta = payload.meta || {};
    const prevClose = number(meta.intraday_previous_close) || (points.length ? number(points[0].close) : 0);

    if (!points.length || prevClose <= 0) {
      return emptyChart(meta.intraday_notice || "分时数据暂不可用");
    }

    let maxPct = 2;
    points.forEach(function (p) {
      const cp = number(p.close);
      const ap = number(p.average);
      if (cp > 0) maxPct = Math.max(maxPct, Math.abs((cp - prevClose) / prevClose * 100));
      if (ap > 0) maxPct = Math.max(maxPct, Math.abs((ap - prevClose) / prevClose * 100));
    });
    maxPct = Math.max(0.01, Math.ceil(maxPct * 100) / 100);

    function x(i) {
      return padL + (points.length <= 1 ? pw / 2 : i / (points.length - 1) * pw);
    }
    function y(close) {
      const pct = (close - prevClose) / prevClose * 100;
      return padT + (maxPct - pct) / (2 * maxPct) * ph;
    }

    const pricePath = points.map(function (p, i) {
      return (i ? "L" : "M") + x(i).toFixed(1) + " " + y(number(p.close)).toFixed(1);
    }).join(" ");

    const avgPts = [];
    points.forEach(function (p, i) {
      const a = number(p.average);
      if (a <= 0) return;
      avgPts.push((avgPts.length ? "L" : "M") + x(i).toFixed(1) + " " + y(a).toFixed(1));
    });
    const avgPath = avgPts.join(" ");

    const y0 = y(prevClose).toFixed(1);
    const right = W - 4;

    return '<svg viewBox="0 0 ' + W + " " + H + '" preserveAspectRatio="none" aria-hidden="true">' +
      '<line class="m-chart-prev-close" x1="' + padL + '" y1="' + y0 + '" x2="' + (W - padR) + '" y2="' + y0 + '"/>' +
      '<path class="m-chart-line m-chart-avg" d="' + avgPath + '"/>' +
      '<path class="m-chart-line m-chart-price" d="' + pricePath + '"/>' +
      svgAxisText(right, padT + 8, "end", "is-up", "+" + maxPct.toFixed(2) + "%") +
      svgAxisText(right, padT + ph / 2 + 3, "end", "", "0.00%") +
      svgAxisText(right, padT + ph, "end", "is-down", "-" + maxPct.toFixed(2) + "%") +
      svgAxisText(padL, H - 6, "start", "", "09:30") +
      svgAxisText(padL + pw / 2, H - 6, "middle", "", "11:30/13:00") +
      svgAxisText(padL + pw, H - 6, "end", "", "15:00") +
      "</svg>";
  }

  function dailyChart(payload) {
    const W = 360, H = 240, padL = 8, padR = 52, padT = 10, padB = 22;
    const pw = W - padL - padR;
    const ph = H - padT - padB;
    const prices = (payload.prices || []).slice(-48);

    if (prices.length < 2) return emptyChart("日线数据暂不可用");

    const closes = prices.map(function (b) { return number(b.close); });
    const highs = prices.map(function (b) { return number(b.high); });
    const lows = prices.map(function (b) { return number(b.low); });
    let max = Math.max.apply(null, highs);
    let min = Math.min.apply(null, lows);
    if (max - min <= 0) { max += 1; min -= 1; }
    const padRange = (max - min) * 0.08;
    max += padRange;
    min -= padRange;

    function x(i) { return padL + (i + 0.5) / prices.length * pw; }
    function y(v) { return padT + (max - v) / (max - min) * ph; }

    const candleW = Math.max(2, Math.min(7, pw / prices.length * 0.7));
    const candles = prices.map(function (b, i) {
      const up = number(b.close) >= number(b.open);
      const cls = "m-chart-candle" + (up ? " is-up" : " is-down");
      const cx = x(i).toFixed(1);
      const bodyTop = y(Math.max(number(b.open), number(b.close))).toFixed(1);
      const bodyH = Math.max(1, Math.abs(y(number(b.open)) - y(number(b.close)))).toFixed(1);
      const bx = (x(i) - candleW / 2).toFixed(1);
      return '<line class="' + cls + '" x1="' + cx + '" y1="' + y(number(b.high)).toFixed(1) + '" x2="' + cx + '" y2="' + y(number(b.low)).toFixed(1) + '"/>' +
        '<rect class="' + cls + '" x="' + bx + '" y="' + bodyTop + '" width="' + candleW.toFixed(1) + '" height="' + bodyH + '"/>';
    }).join("");

    const ma5 = movingAverage(closes, 5);
    const ma10 = movingAverage(closes, 10);

    const firstDate = prices[0].trade_date;
    const midDate = prices[Math.floor(prices.length / 2)].trade_date;
    const lastDate = prices[prices.length - 1].trade_date;
    const right = W - 4;

    return '<div class="m-chart-legend" aria-hidden="true">' +
      '<span class="m-chart-legend-ma5">MA5</span>' +
      '<span class="m-chart-legend-ma10">MA10</span>' +
      "</div>" +
      '<svg viewBox="0 0 ' + W + " " + H + '" preserveAspectRatio="none" aria-hidden="true">' +
      candles +
      maPath(ma5, x, y, "is-ma5") +
      maPath(ma10, x, y, "is-ma10") +
      svgAxisText(right, padT + 8, "end", "is-up", formatNumber(max, 2)) +
      svgAxisText(right, padT + ph, "end", "is-down", formatNumber(min, 2)) +
      svgAxisText(padL, H - 6, "start", "", dateMMDD(firstDate)) +
      svgAxisText(padL + pw / 2, H - 6, "middle", "", dateMMDD(midDate)) +
      svgAxisText(padL + pw, H - 6, "end", "", dateMMDD(lastDate)) +
      "</svg>";
  }

  /* ============================================================== 我的复盘（P3b） ============================================================== */

  const TRADE_ACTION_OPTIONS = [
    { key: "buy", label: "买入", tone: "up" },
    { key: "add", label: "加仓", tone: "up" },
    { key: "sell", label: "卖出", tone: "down" },
    { key: "trim", label: "减仓", tone: "down" },
    { key: "watch", label: "观察", tone: "flat" }
  ];
  const TRADE_EMOTION_OPTIONS = [
    { key: "calm", label: "平静" },
    { key: "confident", label: "笃定" },
    { key: "hesitant", label: "犹豫" },
    { key: "anxious", label: "焦虑" },
    { key: "impulsive", label: "冲动" }
  ];

  function reviewSubKey(key) {
    const parts = String(key || "").split("/");
    return parts[1] || "";
  }

  function reviewTitle(key) {
    const map = {
      watchlist: "自选股",
      trades: "交易日志",
      daily: "每日复盘",
      notes: "个股笔记",
      alerts: "提醒中心"
    };
    return map[reviewSubKey(key)] || "我的复盘";
  }

  function headerActionButton(dataAttr, iconName, ariaLabel) {
    return '<button class="m-header-btn" type="button" data-' + dataAttr + ' aria-label="' + escapeHtml(ariaLabel) + '">' + icon(iconName, 20) + "</button>";
  }

  function setupReviewPage(key) {
    const subKey = reviewSubKey(key);
    state.key = key;
    state.requestedDate = todayString();
    state.sort = { key: "", dir: null };
    state.sortTable = { cols: null, reapply: null };
    state.detail = null;
    state.review.subKey = subKey;
    state.review.form = null;
    state.review.dirty = false;
    state.review.watchlist = null;
    state.review.trades = null;
    state.review.notes = null;
    state.review.dailyItems = null;
    state.review.alerts = null;
    state.review.alertStatus = "all";

    document.getElementById("m-view").classList.add("m-view-feature");
    const title = reviewTitle(key);
    let actions = "";
    if (subKey === "trades") {
      actions = headerActionButton("review-add-trade", "plus", "新增交易");
    } else if (subKey === "notes") {
      actions = headerActionButton("review-add-note", "plus", "新增笔记");
    } else if (subKey === "alerts") {
      actions =
        headerActionButton("review-add-alert", "plus", "新增提醒") +
        '<button class="m-header-btn" type="button" data-review-read-all aria-label="全部已读" hidden>' + icon("check-check", 20) + "</button>";
    }
    if (subKey === "daily") {
      updateHeader(title, state.requestedDate);
    } else {
      global.MobileRouter.updateHeader({ title: title, back: true, actions: actions });
    }
    document.getElementById("m-view").innerHTML = complexFrame(key, complexScroll(skeletonHtml(6)));
  }

  function loadReview() {
    const subKey = state.review.subKey;
    if (subKey === "watchlist") loadReviewWatchlist();
    else if (subKey === "trades") loadReviewTrades();
    else if (subKey === "daily") loadReviewDaily();
    else if (subKey === "notes") loadReviewNotes();
    else if (subKey === "alerts") loadReviewAlerts();
  }

  function reviewState(kind) {
    return '<div class="m-state m-state--error m-motion-fade-in"><p>' + escapeHtml(kind) + "加载失败</p>" +
      '<button class="m-btn-primary m-btn-retry" type="button" data-action="retry">重试</button></div>';
  }

  /* ----- 自选股 ----- */

  function loadReviewWatchlist() {
    const seq = nextSeq();
    global.MobileAPI.request("/api/watchlist?trade_date=" + encodeURIComponent(todayString())).then(function (payload) {
      if (seq !== state.seq) return;
      state.review.watchlist = payload || { items: [] };
      renderReviewWatchlist();
    }).catch(function (error) {
      if (seq !== state.seq) return;
      const scroll = document.getElementById("m-scroll");
      if (scroll) scroll.innerHTML = reviewState("自选股");
    });
  }

  function watchDotColor(color) {
    return { red: true, blue: true, green: true, amber: true }[color] ? color : "red";
  }

  function watchStockCell(row) {
    const color = watchDotColor(row.color);
    return '<span class="m-stock m-stock--watch">' +
      '<span class="m-watch-dot m-watch-dot--' + color + '" aria-hidden="true"></span>' +
      '<span class="m-stock-copy">' +
      '<span class="m-stock-name">' + escapeHtml(row.name || "--") + "</span>" +
      '<span class="m-stock-code">' + escapeHtml(row.code || "") + "</span>" +
      "</span></span>";
  }

  function renderReviewWatchlist() {
    const scroll = document.getElementById("m-scroll");
    if (!scroll) return;
    const data = state.review.watchlist || { items: [] };
    const rows = data.items || [];
    if (!rows.length) {
      scroll.innerHTML = '<div class="m-state m-motion-rise-in">' +
        '<span class="m-state-icon">' + icon("star", 26) + "</span>" +
        "<p>还没有自选股</p>" +
        "<small>在任意股票详情点星形加入</small>" +
        "</div>";
      return;
    }
    const cols = {
      frozenColumns: [{ key: "stock", label: "股票", type: "stock", width: 112 }],
      primaryColumns: [
        { key: "change", label: "当日涨幅", type: "change" },
        { key: "attention_score", label: "关注分", type: "score" }
      ],
      scrollColumns: [
        { key: "sector", label: "板块", type: "text" },
        { key: "return_5d", label: "5日涨幅", type: "change" },
        { key: "remark", label: "跟踪备注", type: "text" }
      ]
    };
    state.sortTable = { cols: cols, reapply: renderReviewWatchlist };
    const sorted = sortedRows(rows, cols);
    scroll.innerHTML = '<div class="m-table-wrap">' + buildReviewTable(cols, sorted, {
      noSort: true,
      cellRenderer: function (col, row) {
        if (col.key === "stock") return watchStockCell(row);
        if (col.key === "attention_score") {
          if (row.attention_score == null || row.attention_score === "") return '<span class="m-num"></span>';
          return '<span class="m-num m-num-strong">' + formatNumber(number(row.attention_score), 1) + "</span>";
        }
        if (col.key === "remark") {
          const text = String(row.remark == null ? "" : row.remark);
          if (!text) return '<span class="m-cell-text m-cell-text--empty">--</span>';
          return '<span class="m-cell-text" title="' + escapeHtml(text) + '">' + escapeHtml(text) + "</span>";
        }
        return null;
      }
    }) + "</div>";
  }

  /* ----- 交易日志 ----- */

  function loadReviewTrades() {
    const seq = nextSeq();
    global.MobileAPI.request("/api/trades").then(function (payload) {
      if (seq !== state.seq) return;
      state.review.trades = payload || { items: [], summary: {} };
      renderReviewTrades();
    }).catch(function (error) {
      if (seq !== state.seq) return;
      const scroll = document.getElementById("m-scroll");
      if (scroll) scroll.innerHTML = reviewState("交易日志");
    });
  }

  function tradeActionBadge(action) {
    const opt = TRADE_ACTION_OPTIONS.find(function (o) { return o.key === action; });
    const label = opt ? opt.label : (action || "--");
    const tone = opt ? opt.tone : "flat";
    return '<span class="m-trade-badge m-trade-badge--' + tone + '">' + escapeHtml(label) + "</span>";
  }

  function renderReviewTrades() {
    const scroll = document.getElementById("m-scroll");
    if (!scroll) return;
    const data = state.review.trades || { items: [], summary: {} };
    const items = data.items || [];
    const summary = data.summary || {};
    if (!items.length) {
      scroll.innerHTML = '<div class="m-state m-motion-rise-in">' +
        '<span class="m-state-icon">' + icon("scroll-text", 26) + "</span>" +
        "<p>还没有交易记录</p>" +
        "<small>记下每一次买卖，回头复盘更有据</small>" +
        '<button class="m-btn-primary m-btn-retry" type="button" data-review-add-trade style="margin-top:16px;">记一笔</button>' +
        "</div>";
      return;
    }
    const pnlAmount = summary.pnl_amount;
    const avgPos = summary.average_position;
    const strip = renderStrip5([
      { label: "记录", value: String(summary.total != null ? summary.total : items.length) },
      { label: "已实现", value: String(summary.realized != null ? summary.realized : 0) },
      { label: "胜率", value: summary.win_rate != null ? summary.win_rate + "%" : "--" },
      { label: "累计盈亏", value: pnlAmount == null ? "--" : (pnlAmount > 0 ? "+" : "") + formatNumber(pnlAmount, 2), tone: pnlAmount == null ? "" : (pnlAmount > 0 ? "up" : (pnlAmount < 0 ? "down" : "")) },
      { label: "平均仓位", value: avgPos == null ? "--" : avgPos + "%" }
    ]);

    const cols = {
      frozenColumns: [{ key: "stock", label: "股票", type: "stock", width: 96 }],
      primaryColumns: [
        { key: "action_label", label: "动作", type: "text" },
        { key: "pnl_pct", label: "盈亏%", type: "change" }
      ],
      scrollColumns: [
        { key: "trade_date", label: "日期", type: "text" },
        { key: "position_pct", label: "仓位%", type: "rate" },
        { key: "pnl_amount", label: "盈亏额", type: "change" },
        { key: "emotion_label", label: "情绪", type: "text" }
      ]
    };
    const rows = items.map(function (it) {
      return {
        code: it.code,
        name: it.name,
        action: it.action,
        action_label: it.action_label,
        pnl_pct: it.pnl_pct,
        trade_date: displayCompactDate(it.trade_date),
        position_pct: it.position_pct,
        pnl_amount: it.pnl_amount,
        emotion_label: it.emotion_label,
        id: it.id,
        _raw: it
      };
    });
    state.sortTable = { cols: cols, reapply: renderReviewTrades };
    const sorted = sortedRows(rows, cols);
    const table = buildReviewTable(cols, sorted, {
      cellRenderer: function (col, row) {
        if (col.key === "action_label") return tradeActionBadge(row.action);
        if (col.key === "pnl_pct") {
          if (row.pnl_pct == null || row.pnl_pct === "") return '<span class="m-num">--</span>';
          return '<span class="m-num ' + changeClass(row.pnl_pct) + '">' + (number(row.pnl_pct) > 0 ? "+" : "") + formatNumber(number(row.pnl_pct), 2) + "</span>";
        }
        if (col.key === "pnl_amount") {
          if (row.pnl_amount == null || row.pnl_amount === "") return '<span class="m-num">--</span>';
          return '<span class="m-num ' + changeClass(row.pnl_amount) + '">' + (number(row.pnl_amount) > 0 ? "+" : "") + formatNumber(number(row.pnl_amount), 2) + "</span>";
        }
        if (col.key === "position_pct") {
          if (row.position_pct == null || row.position_pct === "") return '<span class="m-num">--</span>';
          return '<span class="m-num">' + formatNumber(number(row.position_pct), 1) + "</span>";
        }
        return null;
      },
      rowAttrs: function (row) {
        return ' data-trade-id="' + escapeHtml(String(row.id || "")) + '"';
      }
    });
    scroll.innerHTML = strip + '<div class="m-table-wrap">' + table + "</div>";
  }

  function openTradeDetail(tradeId) {
    const data = state.review.trades || { items: [] };
    const item = (data.items || []).find(function (it) { return String(it.id) === String(tradeId); });
    if (!item) return;
    openSheet(
      '<div class="m-sheet-head"><h2>交易详情</h2>' +
      '<button class="m-sheet-close" type="button" data-sheet-close aria-label="关闭">' + icon("close", 20) + "</button></div>" +
      '<div class="m-sheet-body" id="m-trade-detail-body">' + tradeDetailBody(item) + "</div>",
      { detail: false }
    );
  }

  function tradeDetailBody(item) {
    const rows = [
      ["股票", (item.name || "--") + " " + (item.code || "")],
      ["日期", displayCompactDate(item.trade_date)],
      ["动作", item.action_label || "--"],
      ["价格", item.price != null ? formatNumber(number(item.price), 2) : "--"],
      ["数量", item.quantity != null ? intOrDash(item.quantity) : "--"],
      ["仓位", item.position_pct != null ? formatNumber(number(item.position_pct), 1) + "%" : "--"],
      ["盈亏额", item.pnl_amount == null ? "--" : (number(item.pnl_amount) > 0 ? "+" : "") + formatNumber(number(item.pnl_amount), 2)],
      ["盈亏%", item.pnl_pct == null ? "--" : (number(item.pnl_pct) > 0 ? "+" : "") + formatNumber(number(item.pnl_pct), 2) + "%"],
      ["情绪", item.emotion_label || "--"],
      ["标签", (item.tags && item.tags.length) ? item.tags.join("、") : "--"]
    ];
    const toneRow = item.pnl_amount == null ? "" : (number(item.pnl_amount) > 0 ? "up" : number(item.pnl_amount) < 0 ? "down" : "");
    const grid = rows.map(function (pair) {
      const isPnl = pair[0] === "盈亏额";
      return '<div class="m-trade-detail-row"><span class="m-trade-detail-label">' + escapeHtml(pair[0]) + "</span>" +
        '<span class="m-trade-detail-value' + (isPnl ? " " + toneRow : "") + '">' + escapeHtml(pair[1]) + "</span></div>";
    }).join("");
    const block = function (title, text) {
      return '<div class="m-detail-group"><div class="m-detail-group-title">' + escapeHtml(title) + "</div>" +
        '<div class="m-detail-text">' + escapeHtml(text || "尚未填写") + "</div></div>";
    };
    return '<div class="m-trade-detail">' + grid + "</div>" +
      '<div class="m-detail-foot">' +
      block("交易逻辑", item.thesis) +
      block("执行复核", item.execution) +
      '<div class="m-confirm-actions">' +
      '<button class="m-btn-primary" type="button" data-review-edit-trade data-trade-id="' + escapeHtml(String(item.id)) + '">编辑</button>' +
      '<button class="m-btn-primary m-btn-danger" type="button" data-review-delete-trade data-trade-id="' + escapeHtml(String(item.id)) + '">删除</button>' +
      "</div></div>";
  }

  function deleteTrade(tradeId) {
    openConfirmSheet("删除交易记录", "删除后无法恢复，确定删除这条交易记录吗？", {
      danger: true,
      confirmLabel: "删除",
      onConfirm: function () {
        global.MobileAPI.request("/api/trades/" + encodeURIComponent(tradeId), "DELETE").then(function (payload) {
          state.review.trades = { items: payload.items || [], summary: payload.summary || {} };
          closeSheet();
          renderReviewTrades();
          showToast("交易记录已删除");
        }).catch(function (error) {
          showToast(error && error.message ? error.message : "删除失败");
        });
      }
    });
  }

  /* ----- 每日复盘 ----- */

  function loadReviewDaily() {
    const seq = nextSeq();
    global.MobileAPI.request("/api/notes?scope=daily").then(function (payload) {
      if (seq !== state.seq) return;
      state.review.dailyItems = (payload && payload.items) || [];
      updateHeader("每日复盘", state.requestedDate);
      renderReviewDaily();
    }).catch(function (error) {
      if (seq !== state.seq) return;
      const scroll = document.getElementById("m-scroll");
      if (scroll) scroll.innerHTML = reviewState("每日复盘");
    });
  }

  function dailyNoteFor(dateStr) {
    const items = state.review.dailyItems || [];
    const compact = String(dateStr || "").replaceAll("-", "");
    return items.find(function (it) { return String(it.trade_date || "").replaceAll("-", "") === compact; }) || null;
  }

  function renderReviewDaily() {
    const scroll = document.getElementById("m-scroll");
    if (!scroll) return;
    const dateStr = state.requestedDate || todayString();
    const note = dailyNoteFor(dateStr);
    const items = state.review.dailyItems || [];

    const dateLine = '<div class="m-daily-head">' +
      '<span class="m-daily-date">' + escapeHtml(displayCompactDate(dateStr)) + "</span>" +
      '<button class="m-daily-edit" type="button" data-review-edit-daily>' + (note ? "编辑" : "写复盘") + "</button>" +
      "</div>";

    if (!note) {
      const history = dailyHistoryBlock(items, dateStr);
      scroll.innerHTML = '<div class="m-state m-motion-rise-in" style="margin:24px 0;">' +
        '<span class="m-state-icon">' + icon("calendar-check", 26) + "</span>" +
        "<p>今日还未复盘</p>" +
        '<small>花几分钟回顾今天的盘面与操作</small>' +
        '<button class="m-btn-primary m-btn-retry" type="button" data-review-edit-daily style="margin-top:16px;">写复盘</button>' +
        "</div>" + history;
      return;
    }

    const card = function (title, text) {
      return '<div class="m-card m-daily-card">' +
        '<div class="m-daily-card-title">' + escapeHtml(title) + "</div>" +
        '<p class="m-daily-card-body">' + escapeHtml(text || "尚未填写") + "</p>" +
        "</div>";
    };

    const history = dailyHistoryBlock(items, dateStr);
    scroll.innerHTML = dateLine +
      card("今日盘面", note.summary) +
      card("做对做错", note.content) +
      card("明日计划", note.plan) +
      '<button class="m-btn-primary m-btn-danger m-daily-delete" type="button" data-review-delete-daily data-note-id="' + escapeHtml(String(note.id)) + '">删除此日复盘</button>' +
      history;
  }

  function dailyHistoryBlock(items, currentDate) {
    const sorted = (items || []).slice().sort(function (a, b) {
      return String(b.trade_date || "").localeCompare(String(a.trade_date || ""));
    });
    if (!sorted.length) return "";
    const rows = sorted.map(function (it) {
      const active = String(it.trade_date || "").replaceAll("-", "") === String(currentDate || "").replaceAll("-", "");
      const oneLine = String(it.summary || "").trim() || "（无盘面摘要）";
      return '<button class="m-daily-history-row' + (active ? " is-active" : "") + '" type="button" data-daily-date="' + escapeHtml(displayCompactDate(it.trade_date)) + '">' +
        '<span class="m-daily-history-date">' + escapeHtml(displayCompactDate(it.trade_date)) + "</span>" +
        '<span class="m-daily-history-line">' + escapeHtml(oneLine) + "</span>" +
        "</button>";
    }).join("");
    return '<div class="m-daily-history">' +
      '<div class="m-section-title">历史复盘</div>' +
      '<div class="m-daily-history-list">' + rows + "</div>" +
      "</div>";
  }

  function deleteDailyNote(noteId) {
    openConfirmSheet("删除此日复盘", "删除后无法恢复，确定删除这一天的复盘吗？", {
      danger: true,
      confirmLabel: "删除",
      onConfirm: function () {
        global.MobileAPI.request("/api/notes/" + encodeURIComponent(noteId), "DELETE").then(function () {
          state.review.dailyItems = (state.review.dailyItems || []).filter(function (it) { return String(it.id) !== String(noteId); });
          closeSheet();
          renderReviewDaily();
          showToast("复盘已删除");
        }).catch(function (error) {
          showToast(error && error.message ? error.message : "删除失败");
        });
      }
    });
  }

  /* ----- 个股笔记 ----- */

  function loadReviewNotes() {
    const seq = nextSeq();
    global.MobileAPI.request("/api/notes?scope=stock").then(function (payload) {
      if (seq !== state.seq) return;
      state.review.notes = (payload && payload.items) || [];
      renderReviewNotes();
    }).catch(function (error) {
      if (seq !== state.seq) return;
      const scroll = document.getElementById("m-scroll");
      if (scroll) scroll.innerHTML = reviewState("个股笔记");
    });
  }

  function groupNotesByStock(items) {
    const groups = {};
    (items || []).forEach(function (it) {
      const key = it.code || "_";
      if (!groups[key]) groups[key] = { code: it.code, name: it.stock_name || it.code || "--", notes: [] };
      groups[key].notes.push(it);
    });
    return Object.keys(groups).map(function (k) { return groups[k]; });
  }

  function renderReviewNotes() {
    const scroll = document.getElementById("m-scroll");
    if (!scroll) return;
    const items = state.review.notes || [];
    if (!items.length) {
      scroll.innerHTML = '<div class="m-state m-motion-rise-in">' +
        '<span class="m-state-icon">' + icon("sticky-note", 26) + "</span>" +
        "<p>还没有个股笔记</p>" +
        "<small>把对个股的观察与计划记下来</small>" +
        '<button class="m-btn-primary m-btn-retry" type="button" data-review-add-note style="margin-top:16px;">记一条笔记</button>' +
        "</div>";
      return;
    }
    const groups = groupNotesByStock(items);
    scroll.innerHTML = groups.map(function (g) {
      const cards = g.notes.map(function (it) {
        const plan = it.plan ? '<span class="m-note-card-plan">计划：' + escapeHtml(it.plan) + "</span>" : "";
        return '<button class="m-card m-note-card" type="button" data-note-id="' + escapeHtml(String(it.id)) + '">' +
          '<span class="m-note-card-date">' + escapeHtml(displayCompactDate(it.trade_date)) + "</span>" +
          '<span class="m-note-card-body">' + escapeHtml(it.content || "（无内容）") + "</span>" +
          plan +
          "</button>";
      }).join("");
      return '<div class="m-note-group">' +
        '<div class="m-note-group-head">' +
        '<span class="m-note-group-name">' + escapeHtml(g.name) + "</span>" +
        '<span class="m-note-group-code">' + escapeHtml(g.code || "") + "</span>" +
        '<span class="m-note-group-count">' + g.notes.length + " 条</span>" +
        "</div>" +
        '<div class="m-note-group-list">' + cards + "</div>" +
        "</div>";
    }).join("");
  }

  function openNoteDetail(noteId) {
    const item = (state.review.notes || []).find(function (it) { return String(it.id) === String(noteId); });
    if (!item) return;
    openSheet(
      '<div class="m-sheet-head"><h2>' + escapeHtml((item.stock_name || item.code || "笔记") + " · " + displayCompactDate(item.trade_date)) + "</h2>" +
      '<button class="m-sheet-close" type="button" data-sheet-close aria-label="关闭">' + icon("close", 20) + "</button></div>" +
      '<div class="m-sheet-body" id="m-note-detail-body">' +
      '<div class="m-note-detail-block"><div class="m-detail-group-title">笔记</div><div class="m-detail-text">' + escapeHtml(item.content || "尚未填写") + "</div></div>" +
      '<div class="m-note-detail-block"><div class="m-detail-group-title">计划</div><div class="m-detail-text">' + escapeHtml(item.plan || "尚未填写") + "</div></div>" +
      '<div class="m-confirm-actions">' +
      '<button class="m-btn-primary m-btn-danger" type="button" data-review-delete-note data-note-id="' + escapeHtml(String(item.id)) + '">删除</button>' +
      "</div></div>",
      { detail: false }
    );
  }

  function deleteNote(noteId) {
    openConfirmSheet("删除笔记", "删除后无法恢复，确定删除这条笔记吗？", {
      danger: true,
      confirmLabel: "删除",
      onConfirm: function () {
        global.MobileAPI.request("/api/notes/" + encodeURIComponent(noteId), "DELETE").then(function () {
          state.review.notes = (state.review.notes || []).filter(function (it) { return String(it.id) !== String(noteId); });
          closeSheet();
          renderReviewNotes();
          showToast("笔记已删除");
        }).catch(function (error) {
          showToast(error && error.message ? error.message : "删除失败");
        });
      }
    });
  }

  /* ----- 提醒中心 ----- */

  function loadReviewAlerts() {
    const seq = nextSeq();
    const status = state.review.alertStatus;
    global.MobileAPI.request("/api/alerts?status=" + status + "&as_of=" + encodeURIComponent(todayString())).then(function (payload) {
      if (seq !== state.seq) return;
      state.review.alerts = payload || { items: [], unread_count: 0 };
      renderReviewAlerts();
    }).catch(function (error) {
      if (seq !== state.seq) return;
      const scroll = document.getElementById("m-scroll");
      if (scroll) scroll.innerHTML = reviewState("提醒中心");
    });
  }

  function alertKindLabel(kind) {
    if (kind === "manual") return "自定提醒";
    if (kind === "strategy_t5") return "跟踪完成";
    return "策略反馈";
  }

  function alertKindBadge(kind) {
    const tone = kind === "manual" ? "action" : kind === "strategy_t5" ? "down" : "warn";
    return '<span class="m-alert-kind m-alert-kind--' + tone + '">' + escapeHtml(alertKindLabel(kind)) + "</span>";
  }

  function renderReviewAlerts() {
    const scroll = document.getElementById("m-scroll");
    if (!scroll) return;
    const data = state.review.alerts || { items: [], unread_count: 0 };
    const items = data.items || [];
    const unreadCount = number(data.unread_count);

    const readAllBtn = document.querySelector("[data-review-read-all]");
    if (readAllBtn) readAllBtn.hidden = unreadCount <= 0;

    const filterTabs = ["all", "unread"].map(function (f) {
      const active = state.review.alertStatus === f;
      const label = f === "all" ? "全部" : "未读";
      const dot = f === "unread" && unreadCount > 0 ? '<span class="m-alert-dot" aria-hidden="true"></span>' : "";
      return '<button class="m-source-tab' + (active ? " active" : "") + '" type="button" role="tab" aria-selected="' + (active ? "true" : "false") + '" data-review-alert-filter="' + f + '">' + label + dot + "</button>";
    }).join("");
    const tabs = '<div class="m-source-tabs m-alert-tabs" role="tablist" aria-label="提醒筛选">' + filterTabs + "</div>";

    if (!items.length) {
      const emptyText = state.review.alertStatus === "unread" ? "没有未读提醒" : "暂无提醒";
      scroll.innerHTML = tabs + '<div class="m-state m-motion-rise-in" style="margin:24px 0;">' +
        '<span class="m-state-icon">' + icon("bell", 26) + "</span>" +
        "<p>" + escapeHtml(emptyText) + "</p>" +
        (state.review.alertStatus === "all" ? '<small>重要节点给自己留个备忘</small><button class="m-btn-primary m-btn-retry" type="button" data-review-add-alert style="margin-top:16px;">新增提醒</button>' : "") +
        "</div>";
      return;
    }

    const rows = items.map(function (it) {
      const upcoming = !it.due;
      const unread = !it.is_read;
      const dot = unread && it.due ? '<span class="m-alert-unread-dot" aria-hidden="true"></span>' : "";
      const dateText = displayCompactDate(it.available_date);
      return '<button class="m-alert-row' + (unread ? " is-unread" : " is-read") + '" type="button" data-alert-id="' + escapeHtml(String(it.id)) + '">' +
        '<span class="m-alert-row-main">' +
        '<span class="m-alert-row-top">' + alertKindBadge(it.kind) + '<span class="m-alert-row-date">' + escapeHtml(dateText) + (upcoming ? "（未到期）" : "") + "</span></span>" +
        '<span class="m-alert-row-title">' + dot + escapeHtml(it.title) + "</span>" +
        (it.content ? '<span class="m-alert-row-summary">' + escapeHtml(it.content) + "</span>" : "") +
        "</span>" +
        '<span class="m-alert-row-chevron">' + icon("chevron-right", 16) + "</span>" +
        "</button>";
    }).join("");

    scroll.innerHTML = tabs + '<div class="m-alert-list">' + rows + "</div>";
  }

  function openAlertDetail(alertId) {
    const item = (state.review.alerts && state.review.alerts.items || []).find(function (it) { return String(it.id) === String(alertId); });
    if (!item) return;
    const upcoming = !item.due;
    const canRead = !item.is_read && !upcoming;
    openSheet(
      '<div class="m-sheet-head"><h2>提醒详情</h2>' +
      '<button class="m-sheet-close" type="button" data-sheet-close aria-label="关闭">' + icon("close", 20) + "</button></div>" +
      '<div class="m-sheet-body">' +
      '<div class="m-alert-detail">' +
      alertKindBadge(item.kind) +
      '<h3 class="m-alert-detail-title">' + escapeHtml(item.title) + "</h3>" +
      '<div class="m-alert-detail-meta">' + escapeHtml(displayCompactDate(item.available_date)) + (upcoming ? "（未到期）" : "") + "</div>" +
      (item.content ? '<div class="m-detail-text">' + escapeHtml(item.content) + "</div>" : "") +
      (item.code ? '<button class="m-alert-stock" type="button" data-alert-stock data-code="' + escapeHtml(item.code) + '">' + escapeHtml(item.code) + "</button>" : "") +
      "</div>" +
      '<div class="m-confirm-actions">' +
      (canRead ? '<button class="m-btn-primary" type="button" data-review-alert-read data-alert-id="' + escapeHtml(String(item.id)) + '">标为已读</button>' : "") +
      '<button class="m-btn-primary m-btn-danger" type="button" data-review-alert-delete data-alert-id="' + escapeHtml(String(item.id)) + '">删除</button>' +
      "</div></div>",
      { detail: false }
    );
  }

  function markAlertRead(alertId) {
    global.MobileAPI.request("/api/alerts/" + encodeURIComponent(alertId) + "/read", "POST", {}).then(function (payload) {
      state.review.alerts = payload || state.review.alerts;
      closeSheet();
      renderReviewAlerts();
      showToast("已标为已读");
    }).catch(function (error) {
      showToast(error && error.message ? error.message : "操作失败");
    });
  }

  function deleteAlert(alertId) {
    openConfirmSheet("删除提醒", "删除后无法恢复，确定删除这条提醒吗？", {
      danger: true,
      confirmLabel: "删除",
      onConfirm: function () {
        global.MobileAPI.request("/api/alerts/" + encodeURIComponent(alertId), "DELETE").then(function (payload) {
          state.review.alerts = payload || state.review.alerts;
          closeSheet();
          renderReviewAlerts();
          showToast("提醒已删除");
        }).catch(function (error) {
          showToast(error && error.message ? error.message : "删除失败");
        });
      }
    });
  }

  function markAllAlertsRead() {
    global.MobileAPI.request("/api/alerts/read-all", "POST", { as_of: todayString() }).then(function (payload) {
      state.review.alerts = payload || state.review.alerts;
      renderReviewAlerts();
      showToast("全部已读");
    }).catch(function (error) {
      showToast(error && error.message ? error.message : "操作失败");
    });
  }

  /* ----- 全屏表单页 ----- */

  function openForm(kind, opts) {
    const t = (opts && opts.trade) || {};
    state.review.form = { kind: kind, opts: opts || {}, action: t.action || "buy", emotion: t.emotion || "calm", stock: null };
    state.review.dirty = false;
    const title = kind === "trade" ? ((opts && opts.trade) ? "编辑交易" : "新增交易") : kind === "daily" ? "写复盘" : "新增笔记";
    global.MobileRouter.updateHeader({ title: title, back: true, actions: "" });
    document.getElementById("m-view").classList.add("m-view-feature");
    document.getElementById("m-view").innerHTML = renderForm();
    bindForm();
  }

  function closeForm() {
    if (state.review.dirty) {
      openConfirmSheet("放弃本次编辑？", "返回后已填写的内容将不会保存。", {
        confirmLabel: "放弃",
        onConfirm: function () { exitForm(); }
      });
      return;
    }
    exitForm();
  }

  function exitForm() {
    const key = state.key;
    const wasDaily = state.review.subKey === "daily";
    const keepDate = state.requestedDate;
    state.review.form = null;
    state.review.dirty = false;
    setupReviewPage(key);
    if (wasDaily) state.requestedDate = keepDate;
    loadReview();
  }

  function formFieldHtml(label, controlHtml, required, hintHtml) {
    return '<label class="m-form-field">' +
      '<span>' + escapeHtml(label) + (required ? '<em class="m-field-req">*</em>' : "") + "</span>" +
      controlHtml +
      (hintHtml || "") +
      "</label>";
  }

  function textInputHtml(id, value, attrs) {
    return '<input id="' + id + '" type="text" value="' + escapeHtml(value == null ? "" : value) + '"' + (attrs || "") + ">";
  }

  function numberInputHtml(id, value, attrs) {
    return '<input id="' + id + '" type="text" inputmode="decimal" value="' + escapeHtml(value == null ? "" : value) + '"' + (attrs || "") + ">";
  }

  function textareaHtml(id, value, maxlength, rows) {
    return '<div class="m-field-textarea">' +
      '<textarea id="' + id + '" maxlength="' + maxlength + '" rows="' + (rows || 3) + '" data-maxlength="' + maxlength + '">' + escapeHtml(value == null ? "" : value) + "</textarea>" +
      '<span class="m-field-counter" data-counter-for="' + id + '">0/' + maxlength + "</span>" +
      "</div>";
  }

  function dateInputHtml(id, value) {
    return '<div class="m-input-date">' +
      '<input id="' + id + '" type="date" value="' + escapeHtml(value || todayString()) + '">' +
      '<span class="m-input-date-icon">' + icon("calendar", 16) + "</span>" +
      "</div>";
  }

  function segHtml(group, options, selected) {
    return '<div class="m-seg" role="radiogroup" data-seg-group="' + group + '">' +
      options.map(function (opt) {
        const active = opt.key === selected;
        return '<button class="m-seg-opt' + (active ? " active" : "") + '" type="button" role="radio" aria-checked="' + (active ? "true" : "false") + '" data-seg-key="' + escapeHtml(opt.key) + '">' + escapeHtml(opt.label) + "</button>";
      }).join("") +
      "</div>";
  }

  function stockPickerHtml(id, stock) {
    const chip = stock
      ? '<span class="m-stock-chip">' +
        '<span class="m-stock-chip-name">' + escapeHtml(stock.name) + "</span>" +
        '<span class="m-stock-chip-code">' + escapeHtml(stock.code) + "</span>" +
        '<button class="m-stock-chip-clear" type="button" data-stock-clear aria-label="清除">' + icon("close", 14) + "</button>" +
        "</span>"
      : "";
    return '<div class="m-stock-picker" data-stock-picker>' +
      (chip || '<input id="' + id + '" type="text" placeholder="搜索股票名称或代码" autocomplete="off">') +
      '<div class="m-stock-picker-results" data-stock-results hidden></div>' +
      "</div>";
  }

  function renderForm() {
    const form = state.review.form;
    if (form.kind === "trade") {
      return renderTradeForm(form);
    }
    if (form.kind === "daily") {
      return renderDailyForm(form);
    }
    return renderNoteForm(form);
  }

  function renderTradeForm(form) {
    const t = form.opts.trade || {};
    const tags = (t.tags || []).join(", ");
    const fields = [
      formFieldHtml("日期", dateInputHtml("m-trade-date", displayCompactDate(t.trade_date)), true),
      formFieldHtml("代码", textInputHtml("m-trade-code", t.code, 'inputmode="numeric" maxlength="6" placeholder="6 位代码"'), true),
      formFieldHtml("名称", textInputHtml("m-trade-name", t.name, 'placeholder="股票名称"'), true),
      formFieldHtml("动作", segHtml("action", TRADE_ACTION_OPTIONS, form.action || t.action || "buy"), true),
      formFieldHtml("价格", numberInputHtml("m-trade-price", t.price, 'placeholder="成交价格"'), true),
      formFieldHtml("数量", numberInputHtml("m-trade-quantity", t.quantity, 'inputmode="numeric" placeholder="成交数量"'), false),
      formFieldHtml("仓位%", '<div class="m-input-unit">' + numberInputHtml("m-trade-position", t.position_pct, 'placeholder="0"') + '<span class="m-input-unit-suffix">%</span></div>', false),
      formFieldHtml("盈亏额", numberInputHtml("m-trade-pnl-amount", t.pnl_amount, 'placeholder="可留空"'), false),
      formFieldHtml("盈亏%", '<div class="m-input-unit">' + numberInputHtml("m-trade-pnl-pct", t.pnl_pct, 'placeholder="可留空"') + '<span class="m-input-unit-suffix">%</span></div>', false),
      formFieldHtml("情绪", segHtml("emotion", TRADE_EMOTION_OPTIONS, form.emotion || t.emotion || "calm"), true),
      formFieldHtml("标签", textInputHtml("m-trade-tags", tags, 'placeholder="逗号分隔，如 打板、龙头"'), false),
      formFieldHtml("交易逻辑", textareaHtml("m-trade-thesis", t.thesis, 2000, 4), false),
      formFieldHtml("执行复核", textareaHtml("m-trade-execution", t.execution, 2000, 4), false)
    ];
    return '<div class="m-page m-form-page" data-page="' + escapeHtml(state.key) + '">' +
      '<div class="m-scroll" id="m-scroll">' +
      '<div class="m-form-body">' + fields.join("") + "</div>" +
      "</div>" +
      '<div class="m-form-bar"><button class="m-btn-primary" type="button" data-review-submit>保存</button></div>' +
      "</div>";
  }

  function renderDailyForm() {
    const note = state.review.form.opts.note || {};
    const fields = [
      formFieldHtml("日期", dateInputHtml("m-daily-date", state.requestedDate || todayString()), true),
      formFieldHtml("今日盘面", textareaHtml("m-daily-summary", note.summary, 500, 3), false),
      formFieldHtml("做对做错", textareaHtml("m-daily-content", note.content, 5000, 8), false),
      formFieldHtml("明日计划", textareaHtml("m-daily-plan", note.plan, 2000, 5), false)
    ];
    return '<div class="m-page m-form-page" data-page="' + escapeHtml(state.key) + '">' +
      '<div class="m-scroll" id="m-scroll">' +
      '<div class="m-form-body">' + fields.join("") + "</div>" +
      "</div>" +
      '<div class="m-form-bar"><button class="m-btn-primary" type="button" data-review-submit>保存</button></div>' +
      "</div>";
  }

  function renderNoteForm() {
    const fields = [
      formFieldHtml("股票", stockPickerHtml("m-note-stock", state.review.form.stock), true),
      formFieldHtml("笔记内容", textareaHtml("m-note-content", "", 5000, 6), false),
      formFieldHtml("计划", textareaHtml("m-note-plan", "", 2000, 4), false)
    ];
    return '<div class="m-page m-form-page" data-page="' + escapeHtml(state.key) + '">' +
      '<div class="m-scroll" id="m-scroll">' +
      '<div class="m-form-body">' + fields.join("") + "</div>" +
      "</div>" +
      '<div class="m-form-bar"><button class="m-btn-primary" type="button" data-review-submit>保存</button></div>' +
      "</div>";
  }

  function markDirty() {
    if (state.review.form) state.review.dirty = true;
  }

  function bindForm() {
    const scroll = document.getElementById("m-scroll");
    if (!scroll) return;

    scroll.addEventListener("input", function (event) {
      if (event.target.matches("input, textarea")) markDirty();
      if (event.target.dataset && event.target.dataset.maxlength) {
        const counter = document.querySelector('[data-counter-for="' + event.target.id + '"]');
        if (counter) counter.textContent = event.target.value.length + "/" + event.target.dataset.maxlength;
      }
    });

    scroll.querySelectorAll("textarea[data-maxlength]").forEach(function (ta) {
      const counter = document.querySelector('[data-counter-for="' + ta.id + '"]');
      if (counter) counter.textContent = ta.value.length + "/" + ta.dataset.maxlength;
    });

    scroll.addEventListener("click", function (event) {
      const seg = event.target.closest("[data-seg-key]");
      if (seg) {
        const group = seg.parentElement && seg.parentElement.dataset.segGroup;
        seg.parentElement.querySelectorAll("[data-seg-key]").forEach(function (b) {
          b.classList.toggle("active", b === seg);
          b.setAttribute("aria-checked", String(b === seg));
        });
        state.review.form[group] = seg.dataset.segKey;
        markDirty();
        return;
      }
    });

    const picker = scroll.querySelector("[data-stock-picker]");
    if (picker) bindStockPicker(picker, state.review.form);
  }

  function bindStockPicker(picker, target) {
    const t = target || { stock: null };
    let timer = null;
    let requestSeq = 0;

    function hideResults() {
      const results = picker.querySelector("[data-stock-results]");
      if (results) { results.hidden = true; results.innerHTML = ""; }
    }

    function renderPickerContent() {
      picker.innerHTML = stockPickerHtml("", t.stock);
    }

    function runStockSearch(q) {
      const seq = ++requestSeq;
      global.MobileAPI.request("/api/search?q=" + encodeURIComponent(q) + "&trade_date=" + encodeURIComponent(todayString())).then(function (payload) {
        if (seq !== requestSeq) return;
        const stocks = (payload && payload.groups && payload.groups.stocks) || [];
        const results = picker.querySelector("[data-stock-results]");
        if (!results) return;
        results.hidden = false;
        results.innerHTML = stocks.length
          ? stocks.map(function (s) {
            return '<button class="m-stock-picker-row" type="button" data-stock-pick="' + escapeHtml(s.code) + '" data-stock-name="' + escapeHtml(s.name) + '" data-stock-sector="' + escapeHtml(s.industry || "其他") + '">' +
              '<span class="m-stock-picker-row-name">' + escapeHtml(s.name) + "</span>" +
              '<span class="m-stock-picker-row-sub">' + escapeHtml(s.industry || "其他") + "</span>" +
              '<span class="m-stock-picker-row-code">' + escapeHtml(s.code) + "</span>" +
              "</button>";
          }).join("")
          : '<div class="m-stock-picker-empty">没有找到匹配的股票</div>';
      }).catch(function () {
        if (seq !== requestSeq) return;
        const results = picker.querySelector("[data-stock-results]");
        if (!results) return;
        results.hidden = false;
        results.innerHTML = '<div class="m-stock-picker-empty">搜索失败</div>';
      });
    }

    picker.addEventListener("input", function (event) {
      if (event.target.tagName !== "INPUT") return;
      markDirty();
      clearTimeout(timer);
      const q = event.target.value.trim();
      if (!q) { hideResults(); return; }
      timer = setTimeout(function () { runStockSearch(q); }, 180);
    });

    picker.addEventListener("focusin", function (event) {
      if (event.target.tagName !== "INPUT") return;
      const q = event.target.value.trim();
      if (q) runStockSearch(q);
    });

    picker.addEventListener("click", function (event) {
      const row = event.target.closest("[data-stock-pick]");
      if (row) {
        t.stock = { code: row.dataset.stockPick, name: row.dataset.stockName, sector: row.dataset.stockSector };
        hideResults();
        renderPickerContent();
        markDirty();
        return;
      }
      const clear = event.target.closest("[data-stock-clear]");
      if (clear) {
        t.stock = null;
        renderPickerContent();
        markDirty();
        return;
      }
    });

    document.addEventListener("click", function onDoc(event) {
      if (!picker.contains(event.target)) hideResults();
    });
  }

  function fieldError(id, message) {
    const input = document.getElementById(id);
    if (!input) return;
    const label = input.closest(".m-form-field");
    if (!label) return;
    label.classList.add("is-invalid");
    let err = label.querySelector(".m-field-error");
    if (!err) {
      err = document.createElement("span");
      err.className = "m-field-error";
      label.appendChild(err);
    }
    err.textContent = message;
  }

  function clearFieldError(id) {
    const input = document.getElementById(id);
    if (!input) return;
    const label = input.closest(".m-form-field");
    if (!label) return;
    label.classList.remove("is-invalid");
    const err = label.querySelector(".m-field-error");
    if (err) err.remove();
  }

  function numberValue(id) {
    const el = document.getElementById(id);
    const raw = el ? el.value.trim() : "";
    return raw === "" ? "" : number(raw);
  }

  function submitForm() {
    const form = state.review.form;
    if (!form) return;
    if (form.kind === "trade") submitTradeForm();
    else if (form.kind === "daily") submitDailyForm();
    else submitNoteForm();
  }

  function setSubmitting(btn, submitting) {
    if (!btn) return;
    btn.disabled = submitting;
    if (submitting) {
      btn.innerHTML = '<span class="m-btn-spinner" aria-hidden="true"></span><span>保存中…</span>';
    } else {
      btn.innerHTML = "保存";
    }
  }

  function submitTradeForm() {
    const form = state.review.form;
    const trade = form.opts.trade || {};
    const tradeDate = document.getElementById("m-trade-date").value;
    const code = document.getElementById("m-trade-code").value.trim();
    const name = document.getElementById("m-trade-name").value.trim();
    const price = numberValue("m-trade-price");
    const position = numberValue("m-trade-position");
    const pnlAmount = numberValue("m-trade-pnl-amount");
    const pnlPct = numberValue("m-trade-pnl-pct");
    const tags = document.getElementById("m-trade-tags").value.trim();

    clearFieldError("m-trade-code");
    clearFieldError("m-trade-name");
    clearFieldError("m-trade-price");
    let invalid = false;
    if (!code) { fieldError("m-trade-code", "请输入股票代码"); invalid = true; }
    if (!name) { fieldError("m-trade-name", "请输入股票名称"); invalid = true; }
    if (price === "") { fieldError("m-trade-price", "请输入成交价格"); invalid = true; }
    if (invalid) return;

    const payload = {
      trade_date: tradeDate || todayString(),
      code: code,
      name: name,
      action: form.action,
      price: price,
      quantity: number(document.getElementById("m-trade-quantity").value || 0),
      position_pct: position === "" ? 0 : position,
      pnl_amount: pnlAmount === "" ? null : pnlAmount,
      pnl_pct: pnlPct === "" ? null : pnlPct,
      emotion: form.emotion,
      tags: tags ? tags.split(/[,，]/).map(function (s) { return s.trim(); }).filter(Boolean) : [],
      thesis: document.getElementById("m-trade-thesis").value.trim(),
      execution: document.getElementById("m-trade-execution").value.trim()
    };
    if (trade.id) payload.id = trade.id;

    const btn = document.querySelector("[data-review-submit]");
    setSubmitting(btn, true);
    global.MobileAPI.request("/api/trades", "POST", payload).then(function () {
      state.review.dirty = false;
      exitForm();
      showToast("交易已保存");
    }).catch(function (error) {
      setSubmitting(btn, false);
      showToast(error && error.message ? error.message : "保存失败");
    });
  }

  function submitDailyForm() {
    const form = state.review.form;
    const note = form.opts.note || {};
    const summary = document.getElementById("m-daily-summary").value.trim();
    const content = document.getElementById("m-daily-content").value.trim();
    const plan = document.getElementById("m-daily-plan").value.trim();
    const tradeDate = document.getElementById("m-daily-date").value || todayString();
    if (!summary && !content && !plan) {
      showToast("复盘内容不能全部为空");
      return;
    }
    const payload = {
      trade_date: tradeDate,
      summary: summary,
      content: content,
      plan: plan
    };
    if (note.id) payload.id = note.id;

    const btn = document.querySelector("[data-review-submit]");
    setSubmitting(btn, true);
    global.MobileAPI.request("/api/notes", "POST", payload).then(function () {
      state.review.dirty = false;
      state.requestedDate = tradeDate;
      exitForm();
      showToast("已保存");
    }).catch(function (error) {
      setSubmitting(btn, false);
      showToast(error && error.message ? error.message : "保存失败");
    });
  }

  function submitNoteForm() {
    const form = state.review.form;
    const stock = form.stock;
    if (!stock || !stock.code) {
      showToast("请先选择股票");
      return;
    }
    const payload = {
      code: stock.code,
      stock_name: stock.name,
      trade_date: todayString(),
      content: document.getElementById("m-note-content").value.trim(),
      plan: document.getElementById("m-note-plan").value.trim()
    };
    if (!payload.content && !payload.plan) {
      showToast("笔记内容不能为空");
      return;
    }
    const btn = document.querySelector("[data-review-submit]");
    setSubmitting(btn, true);
    global.MobileAPI.request("/api/notes", "POST", payload).then(function () {
      state.review.dirty = false;
      exitForm();
      showToast("笔记已保存");
    }).catch(function (error) {
      setSubmitting(btn, false);
      showToast(error && error.message ? error.message : "保存失败");
    });
  }

  /* ----- 提醒新增抽屉 ----- */

  function openAlertAddDrawer() {
    state.review.alertDraft = { stock: null };
    openSheet(
      '<div class="m-sheet-head"><h2>新增提醒</h2>' +
      '<button class="m-sheet-close" type="button" data-sheet-close aria-label="关闭">' + icon("close", 20) + "</button></div>" +
      '<div class="m-sheet-body" id="m-alert-add-body">' + alertAddDrawerBody() + "</div>",
      { detail: false }
    );
    const body = document.getElementById("m-alert-add-body");
    const picker = body.querySelector("[data-stock-picker]");
    if (picker) bindStockPicker(picker, state.review.alertDraft);
  }

  function alertAddDrawerBody() {
    return '<label class="m-form-field"><span>标题<em class="m-field-req">*</em></span>' +
      '<input id="m-alert-title" type="text" maxlength="80" placeholder="提醒标题"></label>' +
      formFieldHtml("提醒日期", dateInputHtml("m-alert-date", todayString()), true) +
      formFieldHtml("关联股票", stockPickerHtml("m-alert-stock", state.review.alertDraft ? state.review.alertDraft.stock : null), false) +
      formFieldHtml("内容", textareaHtml("m-alert-content", "", 500, 3), false) +
      '<div class="m-confirm-actions"><button class="m-btn-primary" type="button" data-alert-save>保存</button></div>';
  }

  function submitAlertAdd() {
    const title = document.getElementById("m-alert-title").value.trim();
    if (!title) { showToast("请输入提醒标题"); return; }
    const stock = state.review.alertDraft && state.review.alertDraft.stock;
    const payload = {
      title: title,
      remind_date: document.getElementById("m-alert-date").value || todayString(),
      code: stock ? stock.code : "",
      content: document.getElementById("m-alert-content").value.trim()
    };
    global.MobileAPI.request("/api/alerts", "POST", payload).then(function (res) {
      state.review.alerts = res || state.review.alerts;
      closeSheet();
      renderReviewAlerts();
      showToast("已添加提醒");
    }).catch(function (error) {
      showToast(error && error.message ? error.message : "保存失败");
    });
  }

  function buildReviewTable(cols, rows, opts) {
    const o = opts || {};
    const frozen = cols.frozenColumns || [];
    const primary = cols.primaryColumns || [];
    const scroll = cols.scrollColumns || [];
    const ordered = frozen.concat(primary, scroll);
    let left = 0;
    const frozenLeft = frozen.map(function (col) { const off = left; left += columnWidth(col); return off; });

    function cellOpen(col, index, isHead, extraCls, extraAttrs) {
      const isFrozen = index < frozen.length;
      const width = columnWidth(col);
      const clamp = (col.type === "text" || col.type === "concepts") ? ";max-width:" + width + "px" : "";
      const style = "min-width:" + width + "px" + clamp + (isFrozen ? ";width:" + width + "px;left:" + frozenLeft[index] + "px" : "");
      const cls = (isHead ? "m-th" : "m-td") + " " + colAlign(col) + (isFrozen ? " m-frozen" : "") + (extraCls ? " " + extraCls : "");
      return '<' + (isHead ? "th" : "td") + ' class="' + cls + '" style="' + style + '"' + (extraAttrs || "") + '>';
    }

    const head = ordered.map(function (col, i) {
      const sortable = !o.noSort && sortableColumn(col);
      let extraCls = "";
      let extraAttrs = "";
      let indicator = "";
      if (sortable) {
        const active = state.sort.key === col.key && Boolean(state.sort.dir);
        extraCls = " m-sortable" + (active ? " is-sorted" : "");
        extraAttrs = ' data-sort-key="' + escapeHtml(col.key) + '" aria-sort="' + (active ? (state.sort.dir === "asc" ? "ascending" : "descending") : "none") + '"';
        indicator = sortIndicatorHtml(active);
      }
      return cellOpen(col, i, true, extraCls, extraAttrs) + '<span class="m-th-inner">' + escapeHtml(col.label) + indicator + "</span></th>";
    }).join("");

    const body = rows.map(function (row, rIndex) {
      const cells = ordered.map(function (col, i) {
        const custom = o.cellRenderer ? o.cellRenderer(col, row, rIndex) : null;
        const html = custom != null ? custom : cellHtml(col, row, rIndex);
        return cellOpen(col, i, false) + html + "</td>";
      }).join("");
      const linkable = !o.noLink && /^\d{6}$/.test(String(row.code || ""));
      const code = linkable ? ' data-code="' + escapeHtml(row.code) + '"' : "";
      const extra = o.rowAttrs ? o.rowAttrs(row) : "";
      return "<tr" + code + extra + ">" + cells + "</tr>";
    }).join("");

    return '<table class="m-table"><thead><tr>' + head + "</tr></thead><tbody>" + body + "</tbody></table>";
  }

  /* ---------------------------------------------------------------- 系统管理（HEL-238 按确认样图重排） */

  function isSystemPage(key) {
    return String(key || state.key || "").indexOf("system/") === 0;
  }

  function systemForbiddenHtml() {
    return '<div class="m-state m-motion-rise-in" data-system-page="forbidden">' +
      '<span class="m-state-icon">' + icon("lock", 26) + "</span>" +
      "<p>仅管理员可访问</p>" +
      "<small>系统设置和会员管理需要管理员权限。</small>" +
      "</div>";
  }

  function systemFill(html) {
    const scroll = document.getElementById("m-scroll");
    if (scroll) scroll.innerHTML = html;
  }

  function membershipDateLabel(value) {
    if (!value) return "";
    const parsed = new Date(value);
    if (Number.isNaN(parsed.getTime())) return String(value).slice(0, 10);
    return new Intl.DateTimeFormat("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit" }).format(parsed);
  }

  function currentUser() {
    return global.MobileSession && global.MobileSession.state ? global.MobileSession.state.user : null;
  }

  function currentUsername() {
    const user = currentUser();
    return user && user.username ? user.username : "当前账号";
  }

  function currentMembership() {
    const user = currentUser();
    return (user && user.membership) || {};
  }

  function setupSystemPage(key) {
    state.key = key;
    state.requestedDate = todayString();
    state.sort = { key: "", dir: null };
    state.sortTable = { cols: null, reapply: null };
    state.detail = null;
    if (key === "system/admin") state.system.adminTab = state.system.adminTab || "market";
    document.getElementById("m-view").classList.add("m-view-feature");
    global.MobileRouter.updateHeader({ title: findLabel(key) || key, back: true, actions: "" });
    document.getElementById("m-view").innerHTML = complexFrame(key, complexScroll(skeletonHtml(6)));
  }

  function loadSystem() {
    const key = state.key;
    if ((key === "system/admin" || key === "system/members") && !global.MobileSession.isAdmin()) {
      systemFill(systemForbiddenHtml());
      return;
    }
    if (key === "system/password") {
      renderSystemPassword();
      return;
    }
    const seq = nextSeq();
    const url = (key === "system/admin" || key === "system/members") ? "/api/admin/settings" : "/api/account/status";
    global.MobileAPI.request(url).then(function (payload) {
      if (seq !== state.seq || state.key !== key) return;
      if (key === "system/admin" || key === "system/members") {
        state.system.admin = payload || {};
        state.system.models = ((payload.llm && payload.llm.models) || []).map(function (item) {
          return Object.assign({}, item);
        });
        renderSystemAdmin(key);
      } else {
        state.system.account = payload || {};
        if (key === "system/profile") renderSystemProfile();
        else renderSystemMembership();
      }
    }).catch(function (error) {
      if (seq !== state.seq || state.key !== key) return;
      systemFill(errorHtml(error && error.message ? error.message : "加载失败"));
    });
  }

  function formatLastUsed(value) {
    if (!value) return "";
    const parsed = new Date(value);
    if (Number.isNaN(parsed.getTime())) return "";
    const now = new Date();
    const hh = String(parsed.getHours()).padStart(2, "0");
    const mm = String(parsed.getMinutes()).padStart(2, "0");
    const sameDay = parsed.toDateString() === now.toDateString();
    const yesterday = new Date(now);
    yesterday.setDate(now.getDate() - 1);
    if (sameDay) return "今天 " + hh + ":" + mm;
    if (parsed.toDateString() === yesterday.toDateString()) return "昨天 " + hh + ":" + mm;
    return membershipDateLabel(value) + " " + hh + ":" + mm;
  }

  function systemRowHtml(item) {
    return '<button class="m-sys-row" type="button" data-route="#/feature/' + item.key + '">' +
      '<span class="m-sys-row-icon">' + icon(item.icon, 18) + "</span>" +
      '<span class="m-sys-row-body"><strong>' + escapeHtml(item.label) + "</strong><small>" + escapeHtml(item.hint) + "</small></span>" +
      '<span class="m-sys-row-chevron">' + icon("chevron-right", 16) + "</span>" +
      "</button>";
  }

  function systemThemeRowHtml() {
    const dark = document.getElementById("m-app") && document.getElementById("m-app").dataset.theme === "dark";
    return '<div class="m-sys-row" data-theme-row>' +
      '<span class="m-sys-row-icon" data-theme-row-icon>' + icon(dark ? "moon" : "sun", 18) + "</span>" +
      '<span class="m-sys-row-body"><strong>外观主题</strong><small data-theme-row-label>' + (dark ? "当前：夜间模式" : "当前：日间模式") + "</small></span>" +
      '<button class="m-theme-switch" type="button" data-theme-toggle role="switch" aria-checked="' + (dark ? "true" : "false") + '" aria-label="切换日间/夜间模式"><span class="m-theme-switch-thumb"></span></button>' +
      "</div>";
  }

  function renderSystemHome() {
    state.key = "system";
    document.getElementById("m-view").classList.remove("m-view-feature");
    global.MobileRouter.updateHeader({ title: "系统管理", back: false, actions: "" });
    const view = document.getElementById("m-view");
    view.innerHTML = '<div class="m-sys-home" data-system-page="home">' + skeletonHtml(4) + "</div>";
    const seq = nextSeq();
    Promise.all([
      global.MobileAPI.request("/api/account/status").catch(function () { return {}; }),
      global.MobileSession.listAccounts().catch(function () { return { accounts: [] }; })
    ]).then(function (results) {
      if (seq !== state.seq || state.key !== "system") return;
      state.system.account = results[0] || {};
      state.system.accounts = (results[1] && results[1].accounts) || [];
      paintSystemHome();
    });
  }

  function paintSystemHome() {
    const user = currentUser() || {};
    const membership = currentMembership();
    const account = state.system.account || {};
    const access = account.llm_access || {};
    const status = access.membership || membership;
    const username = user.username || currentUsername();
    const avatar = String(username).slice(0, 1);
    const remembered = (state.system.accounts || []).some(function (item) {
      return String(item.user_id) === String(user.id);
    });
    const currentGrant = (state.system.accounts || []).find(function (item) {
      return String(item.user_id) === String(user.id);
    });
    const lastUsed = formatLastUsed(currentGrant && currentGrant.last_used_at);
    const metaParts = [];
    if (lastUsed) metaParts.push("上次登录：" + lastUsed);
    if (remembered) metaParts.push("本机已记住");
    const badges = [];
    if (global.MobileSession.isAdmin()) badges.push('<span class="m-sys-badge m-sys-badge--admin">管理员</span>');
    if (status.subscribed) badges.push('<span class="m-sys-badge m-sys-badge--ok">会员有效</span>');
    else if (!global.MobileSession.isAdmin()) badges.push('<span class="m-sys-badge">普通用户</span>');
    const accountRows = [
      { key: "system/profile", icon: "user", label: "账号资料", hint: "出生信息 · 加密保存" },
      { key: "system/password", icon: "lock", label: "修改密码", hint: "建议定期更换" },
      { key: "system/membership", icon: "gem", label: "会员状态", hint: "有效期与智能分析额度" }
    ];
    const adminRows = [
      { key: "system/admin", icon: "sliders-horizontal", label: "系统设置", hint: "行情数据 · 模型池" },
      { key: "system/members", icon: "users", label: "会员管理", hint: "开通 · 续期 · 额度" }
    ];
    const html =
      '<div class="m-sys-home" data-system-page="home">' +
      '<div class="m-card m-sys-profile-card"><span class="m-sys-avatar">' + escapeHtml(avatar) + "</span><div>" +
      "<strong>" + escapeHtml(username) + "</strong>" +
      (metaParts.length ? '<p class="m-sys-profile-meta">' + escapeHtml(metaParts.join(" · ")) + "</p>" : "") +
      (badges.length ? '<div class="m-sys-badges">' + badges.join("") + "</div>" : "") +
      "</div></div>" +
      '<h3 class="m-sys-group-title">账号</h3>' +
      '<div class="m-card m-sys-list">' + accountRows.map(systemRowHtml).join("") + "</div>" +
      '<h3 class="m-sys-group-title">偏好</h3>' +
      '<div class="m-card m-sys-list">' + systemThemeRowHtml() + "</div>" +
      (global.MobileSession.isAdmin()
        ? '<h3 class="m-sys-group-title">管理员专区</h3><div class="m-card m-sys-list">' + adminRows.map(systemRowHtml).join("") + "</div>"
        : "") +
      '<h3 class="m-sys-group-title">其他</h3>' +
      '<div class="m-card m-sys-list">' +
      '<button class="m-sys-row" type="button" data-system-switch>' +
      '<span class="m-sys-row-icon">' + icon("arrow-left-right", 18) + "</span>" +
      '<span class="m-sys-row-body"><strong>切换账号</strong><small>本机免密进入其他账号</small></span>' +
      '<span class="m-sys-row-chevron">' + icon("chevron-right", 16) + "</span></button>" +
      '<button class="m-sys-row m-sys-row--danger" type="button" data-system-logout>' +
      '<span class="m-sys-row-icon">' + icon("log-out", 18) + "</span>" +
      '<span class="m-sys-row-body"><strong>退出登录</strong><small>退出后需要重新登录</small></span>' +
      '<span class="m-sys-row-chevron">' + icon("chevron-right", 16) + "</span></button>" +
      "</div>" +
      '<p class="m-sys-foot">' + (global.MobileSession.isAdmin() ? "小白复盘 · 内网个人版" : "系统设置与会员管理仅管理员可见") + "</p>" +
      "</div>";
    document.getElementById("m-view").innerHTML = html;
  }

  function renderSystemProfile() {
    const payload = state.system.account || {};
    const birth = payload.birth_profile || {};
    let birthDate = "";
    let birthTime = "";
    if (birth.birth_datetime) {
      const parts = String(birth.birth_datetime).split("T");
      birthDate = parts[0] || "";
      birthTime = (parts[1] || "").slice(0, 5);
    }
    const gender = birth.gender || "unspecified";
    const configured = Boolean(payload.birth_profile_configured);
    const html =
      '<div class="m-sys-body" data-system-page="profile">' +
      '<div class="m-sys-notice"><p>出生信息仅对当前账号可见并加密保存。原始信息不会在观气页回显，智能解读只使用排盘后的派生结果。</p>' +
      '<div class="m-sys-badges"><span class="m-sys-badge ' + (configured ? "m-sys-badge--ok" : "") + '">' + (configured ? "已加密保存" : "尚未设置") + "</span></div></div>" +
      '<div class="m-card m-sys-section"><strong>命理资料</strong>' +
      formFieldHtml("出生日期", dateInputHtml("m-sys-birth-date", birthDate), true) +
      formFieldHtml("出生时间", '<input id="m-sys-birth-time" type="time" value="' + escapeHtml(birthTime) + '">', true) +
      formFieldHtml("性别", '<select id="m-sys-birth-gender">' +
        '<option value="unspecified"' + (gender === "unspecified" ? " selected" : "") + ">不指定</option>" +
        '<option value="male"' + (gender === "male" ? " selected" : "") + ">男</option>" +
        '<option value="female"' + (gender === "female" ? " selected" : "") + ">女</option>" +
        "</select>", false) +
      "</div>" +
      '<button class="m-btn-outline-danger" type="button" data-system-delete-birth' + (configured ? "" : " disabled") + ">删除命理资料</button>" +
      '<p class="m-sys-hint">删除后智能解读将无法使用出生信息，执行前会再次确认。</p>' +
      "</div>";
    const page = document.querySelector(".m-page");
    if (page) {
      page.innerHTML = '<div class="m-scroll" id="m-scroll">' + html + "</div>" +
        '<div class="m-form-bar"><button class="m-btn-primary" type="button" data-system-save-birth>保存资料</button></div>';
    }
  }

  function renderSystemPassword() {
    const html =
      '<div class="m-sys-body" data-system-page="password">' +
      '<div class="m-sys-notice"><p>仅修改当前账号的登录密码，密码不会保存在这台设备上。修改成功后下次登录需使用新密码。</p></div>' +
      '<div class="m-card m-sys-section"><strong>设置新密码</strong>' +
      formFieldHtml("当前密码", '<input id="m-sys-password-current" type="password" autocomplete="current-password" placeholder="输入现在的密码">', true, '<p class="m-field-error" hidden data-field-error="current"></p>') +
      formFieldHtml("新密码", '<input id="m-sys-password-new" type="password" minlength="8" maxlength="128" autocomplete="new-password" placeholder="8-128 位">', true, '<p class="m-sys-hint">建议字母与数字混合，不要与其他网站重复。</p><p class="m-field-error" hidden data-field-error="new"></p>') +
      formFieldHtml("确认新密码", '<input id="m-sys-password-confirm" type="password" minlength="8" maxlength="128" autocomplete="new-password" placeholder="再输入一次新密码">', true, '<p class="m-field-error" hidden data-field-error="confirm"></p>') +
      '<button class="m-btn-primary" type="button" data-system-save-password>更新密码</button>' +
      "</div></div>";
    const page = document.querySelector(".m-page");
    if (page) page.innerHTML = '<div class="m-scroll" id="m-scroll">' + html + "</div>";
  }

  function renderSystemMembership() {
    const payload = state.system.account || {};
    const access = payload.llm_access || {};
    const membership = access.membership || {};
    const badge = membership.subscribed ? "会员有效" : membership.is_admin ? "管理员权限" : "普通用户";
    const stateLabel = membership.subscribed ? "已开通" : membership.is_admin ? "管理员可用" : "未开通";
    const remaining = membership.subscribed && membership.expires_at
      ? number(membership.remaining_days) + " 天"
      : (membership.is_admin || membership.subscribed ? "长期有效" : "--");
    const detail = membership.subscribed
      ? ((membership.plan || "会员") + (membership.expires_at ? " · 有效至 " + membershipDateLabel(membership.expires_at) : " · 长期有效") + (membership.remaining_days != null ? "，剩余 " + number(membership.remaining_days) + " 天。" : "。"))
      : membership.is_admin
        ? "管理员拥有智能功能管理权限，但不会因此显示为已开通会员。"
        : "开通会员后可使用智能选股、问师、问天、复盘助手等智能功能。";
    const usedToday = membership.active ? (number(access.used_today) + " 次") : "--";
    const remainingCalls = membership.is_admin ? "不限" : membership.active ? (number(access.remaining_calls) + " 次") : "--";
    const html =
      '<div class="m-sys-body" data-system-page="membership">' +
      '<div class="m-card m-sys-hero"><strong>' + escapeHtml(badge) + '</strong>' +
      (membership.subscribed ? '<div class="m-sys-badges"><span class="m-sys-badge m-sys-badge--ok">已开通</span></div>' : "") +
      '<p class="m-sys-lead">' + escapeHtml(detail) + "</p></div>" +
      '<div class="m-card m-sys-section"><strong>今日智能分析</strong>' +
      '<div class="m-sys-grid">' +
      "<div><span>开通状态</span><strong>" + escapeHtml(stateLabel) + "</strong></div>" +
      "<div><span>剩余时长</span><strong>" + escapeHtml(remaining) + "</strong></div>" +
      "<div><span>今日已用</span><strong>" + escapeHtml(usedToday) + "</strong></div>" +
      "<div><span>今日剩余</span><strong>" + escapeHtml(remainingCalls) + "</strong></div>" +
      "</div>" +
      '<p class="m-sys-hint">会员每日智能分析额度 ' + number(access.daily_limit) + " 次，每日 0 点自动重置，由管理员统一设置。</p></div>" +
      '<div class="m-card m-sys-section"><strong>权益说明</strong>' +
      '<p class="m-sys-lead">全部用户可用：行情、搜索、自选股、交易日志与复盘。</p>' +
      '<p class="m-sys-lead">会员专属：智能选股、问师、问天、复盘助手等智能功能。</p></div>' +
      "</div>";
    systemFill(html);
  }

  function adminTabHtml() {
    const tab = state.system.adminTab === "models" ? "models" : "market";
    return '<div class="m-source-tabs" role="tablist" aria-label="系统设置分类">' +
      '<button class="m-source-tab' + (tab === "market" ? " active" : "") + '" type="button" role="tab" data-system-admin-tab="market">行情管理</button>' +
      '<button class="m-source-tab' + (tab === "models" ? " active" : "") + '" type="button" role="tab" data-system-admin-tab="models">模型池</button>' +
      "</div>";
  }

  function statusDot(ok) {
    return '<span class="m-sys-dot' + (ok ? " m-sys-dot--ok" : "") + '"></span>';
  }

  function renderSystemAdmin(key) {
    if (key === "system/members") {
      renderSystemMembers();
      return;
    }
    const payload = state.system.admin || {};
    const data = payload.data || {};
    const ifind = data.ifind || {};
    const llm = payload.llm || {};
    const tab = state.system.adminTab === "models" ? "models" : "market";
    const marketHtml =
      '<div class="m-sys-body" data-system-admin-panel="market">' +
      '<div class="m-card m-sys-section"><strong>数据源状态</strong>' +
      '<div class="m-sys-status-list">' +
      '<div class="m-sys-status-item"><span>Tushare</span><span>' + statusDot(data.configured) + (data.configured ? " 已配置" : " 未配置") + "</span></div>" +
      '<div class="m-sys-status-item"><span>iFinD</span><span>' + statusDot(ifind.configured) + (ifind.configured ? " 已配置" : " 未配置") + "</span></div>" +
      '<div class="m-sys-status-item"><span>行情快照</span><strong>' + number(data.snapshot_dates) + " 个交易日</strong></div>" +
      '<div class="m-sys-status-item"><span>后台刷新</span><span>' + statusDot(data.background_refresh_enabled) + (data.background_refresh_enabled ? " 已启用" : " 已暂停") + "</span></div>" +
      "</div></div>" +
      '<div class="m-card m-sys-section"><strong>数据源密钥</strong>' +
      formFieldHtml("Tushare Token", '<input id="m-sys-token" type="password" autocomplete="off" minlength="20" placeholder="留空则保留现有 Token">', false) +
      formFieldHtml("iFinD Refresh Token", '<input id="m-sys-ifind" type="password" autocomplete="off" maxlength="2048" placeholder="留空则保留现有 Token">', false) +
      '<button class="m-btn-primary" type="button" data-system-save-market>保存密钥</button></div>' +
      '<div class="m-card m-sys-section"><strong>后台刷新</strong>' +
      '<div class="m-sys-switch-row"><div><strong>交易时段自动刷新</strong><p class="m-sys-hint">开启后后台定时更新快照</p></div>' +
      '<button class="m-theme-switch" type="button" data-system-toggle-refresh role="switch" aria-checked="' + (data.background_refresh_enabled ? "true" : "false") + '" aria-label="交易时段自动刷新"><span class="m-theme-switch-thumb"></span></button></div>' +
      '<input id="m-sys-bg-refresh" type="checkbox"' + (data.background_refresh_enabled ? " checked" : "") + ' hidden>' +
      '<button class="m-btn-outline" type="button" data-system-refresh>立即刷新一次</button>' +
      '<p class="m-sys-hint">所有用户读取同一份快照，刷新不影响当前页面内容。</p></div>' +
      '<div class="m-card m-sys-section"><strong>历史数据回补</strong>' +
      formFieldHtml("开始日期", dateInputHtml("m-sys-backfill-start", ""), false) +
      formFieldHtml("结束日期", dateInputHtml("m-sys-backfill-end", ""), false) +
      '<button class="m-btn-outline" type="button" data-system-backfill>开始回补</button>' +
      '<p class="m-sys-hint">回补用于补齐缺失的历史行情，开始前会再次确认；回补期间页面可正常使用。</p></div>' +
      "</div>";
    const models = state.system.models || [];
    const modelsHtml =
      '<div class="m-sys-body" data-system-admin-panel="models">' +
      '<div class="m-card m-sys-section"><strong>模型分工</strong>' +
      formFieldHtml("主模型", '<select id="m-sys-primary-model"></select>', false) +
      formFieldHtml("辅助模型", '<select id="m-sys-fallback-model"></select>', false) +
      '<p class="m-sys-hint">主模型不可用时自动改用辅助模型</p>' +
      '<button class="m-btn-primary" type="button" data-system-save-models>保存分工</button></div>' +
      '<div class="m-card m-sys-section"><strong>模型池 · ' + models.length + " 个</strong>" +
      '<div id="m-sys-model-list">' + renderModelPoolHtml(models) + "</div>" +
      '<p class="m-sys-hint">点任意模型卡片进入编辑：改名称、地址、密钥、测试连接或删除</p>' +
      '<button class="m-btn-primary" type="button" data-system-add-model>+ 添加模型</button></div>' +
      "</div>";
    const page = document.querySelector(".m-page");
    if (!page) return;
    page.innerHTML = adminTabHtml() + '<div class="m-scroll" id="m-scroll">' + (tab === "models" ? modelsHtml : marketHtml) + "</div>";
    if (tab === "models") updateSystemModelRoleOptions(llm.primary_model_id || "", llm.fallback_model_id || "");
  }

  function hostOfUrl(url) {
    try {
      return new URL(url).host;
    } catch (error) {
      return String(url || "").replace(/^https?:\/\//, "").split("/")[0] || "--";
    }
  }

  function renderModelPoolHtml(models) {
    if (!models.length) {
      return '<div class="m-state"><p>模型池为空，请先添加模型</p></div>';
    }
    const llm = (state.system.admin && state.system.admin.llm) || {};
    return models.map(function (item) {
      const badges = [];
      if (item.id === llm.primary_model_id) badges.push('<span class="m-sys-badge m-sys-badge--admin">主模型</span>');
      if (item.id === llm.fallback_model_id) badges.push('<span class="m-sys-badge">辅助</span>');
      badges.push('<span class="m-sys-badge' + (item.configured ? " m-sys-badge--ok" : "") + '">' + (item.configured ? "已配置" : "待配置") + "</span>");
      return '<button class="m-sys-model-card" type="button" data-model-id="' + escapeHtml(item.id) + '" data-system-edit-model>' +
        "<div><strong>" + escapeHtml(item.name || "未命名模型") + "</strong>" +
        '<div class="m-sys-badges">' + badges.join("") + "</div>" +
        '<p class="m-sys-hint">' + escapeHtml(hostOfUrl(item.base_url) + " · " + (item.model || "未填写标识")) + "</p></div>" +
        '<span class="m-sys-row-chevron">' + icon("chevron-right", 16) + "</span></button>";
    }).join("");
  }

  function collectSystemModelPool() {
    return (state.system.models || []).map(function (item) {
      return {
        id: item.id,
        name: item.name || "",
        base_url: item.base_url || "",
        model: item.model || "",
        api_key: item.api_key || "",
        configured: Boolean(item.configured)
      };
    });
  }

  function updateSystemModelRoleOptions(primaryId, fallbackId) {
    const models = collectSystemModelPool();
    const options = models.map(function (item) {
      return '<option value="' + escapeHtml(item.id) + '">' + escapeHtml(item.name || item.model || "未命名模型") + "</option>";
    }).join("");
    const primary = document.getElementById("m-sys-primary-model");
    const fallback = document.getElementById("m-sys-fallback-model");
    if (!primary || !fallback) return;
    primary.innerHTML = models.length ? options : '<option value="">暂无模型</option>';
    fallback.innerHTML = '<option value="">不启用辅助模型</option>' + options;
    const keepPrimary = models.some(function (item) { return item.id === primaryId; }) ? primaryId : (models[0] && models[0].id) || "";
    primary.value = keepPrimary;
    fallback.value = models.some(function (item) { return item.id === fallbackId; }) && fallbackId !== keepPrimary ? fallbackId : "";
  }

  function openModelEditSheet(modelId) {
    const models = state.system.models || [];
    const item = models.find(function (row) { return row.id === modelId; }) || {};
    state.system.editingModelId = modelId;
    openSheet(
      '<div class="m-sheet-head"><h2>编辑模型</h2>' +
      '<button class="m-sheet-close" type="button" data-sheet-close aria-label="关闭">' + icon("close", 20) + "</button></div>" +
      '<div class="m-sheet-body" data-model-id="' + escapeHtml(modelId) + '">' +
      formFieldHtml("显示名称", '<input data-model-field="name" maxlength="50" value="' + escapeHtml(item.name || "") + '">', true) +
      formFieldHtml("API Base URL", '<input data-model-field="base_url" type="url" value="' + escapeHtml(item.base_url || "https://api.openai.com/v1") + '">', true) +
      formFieldHtml("模型标识", '<input data-model-field="model" maxlength="100" value="' + escapeHtml(item.model || "") + '">', true) +
      formFieldHtml("API Key", '<input data-model-field="api_key" type="password" autocomplete="off" maxlength="300" placeholder="' + (item.configured ? "留空则保留已保存的 Key" : "输入 API Key") + '">', !item.configured) +
      '<div class="m-sys-test-row"><button class="m-btn-outline" type="button" data-system-test-model>测试连接</button>' +
      '<span data-model-test-status>未测试</span></div>' +
      '<div class="m-sys-sheet-actions">' +
      '<button class="m-btn-outline-danger" type="button" data-system-delete-model>删除模型</button>' +
      '<button class="m-btn-primary" type="button" data-system-save-model>保存</button>' +
      "</div></div>"
    );
  }

  function readModelSheetFields(root) {
    function fieldValue(name) {
      const input = root.querySelector("[data-model-field='" + name + "']");
      return String(input && input.value != null ? input.value : "").trim();
    }
    return {
      name: fieldValue("name"),
      base_url: fieldValue("base_url"),
      model: fieldValue("model"),
      api_key: fieldValue("api_key")
    };
  }

  function renderSystemMembers() {
    const payload = state.system.admin || {};
    const membership = payload.membership || {};
    const users = payload.users || [];
    const userHtml = users.map(function (user) {
      const admin = user.role === "admin";
      const member = Boolean(user.membership_subscribed);
      const badges = [];
      if (admin) badges.push('<span class="m-sys-badge m-sys-badge--admin">管理员</span>');
      if (member) badges.push('<span class="m-sys-badge m-sys-badge--ok">会员</span>');
      else badges.push('<span class="m-sys-badge">普通用户</span>');
      const expiry = member
        ? (user.membership_expires_at ? "有效至 " + membershipDateLabel(user.membership_expires_at) : "永久有效")
        : user.membership_status === "suspended"
          ? "会员已停用"
          : "尚未开通";
      return '<div class="m-sys-user-row" data-admin-user="' + number(user.id) + '">' +
        '<div class="m-sys-row-body"><strong>' + escapeHtml(user.username) + "</strong>" +
        '<div class="m-sys-badges">' + badges.join("") + "</div>" +
        '<p class="m-sys-hint">' + escapeHtml(expiry) + " · 今日已用 " + number(user.used_today) + " 次</p></div>" +
        '<button class="m-btn-outline" type="button" data-system-open-member>管理</button></div>';
    }).join("") || '<div class="m-state"><p>暂无注册用户</p></div>';
    const html =
      '<div class="m-sys-body" data-system-page="members">' +
      '<div class="m-card m-sys-section"><strong>全局额度</strong>' +
      formFieldHtml("会员每日智能分析上限", '<input id="m-sys-member-limit" type="number" min="1" max="1000" value="' + (number(membership.member_daily_limit) || 50) + '">', false) +
      '<p class="m-sys-hint">对所有会员生效，每日 0 点自动重置。</p>' +
      '<button class="m-btn-primary" type="button" data-system-save-limit>保存额度</button></div>' +
      '<div class="m-card m-sys-section"><strong>会员账号 · ' + users.length + " 个</strong>" +
      userHtml +
      '<p class="m-sys-hint">点「管理」为对应账号开通、续期或停用会员。</p></div>' +
      "</div>";
    systemFill(html);
  }

  function openMemberManageSheet(userId) {
    const users = (state.system.admin && state.system.admin.users) || [];
    const user = users.find(function (item) { return String(item.id) === String(userId); });
    if (!user) return;
    state.system.editingUserId = String(userId);
    const member = Boolean(user.membership_subscribed);
    const expiry = member
      ? (user.membership_expires_at ? "有效至 " + membershipDateLabel(user.membership_expires_at) : "永久有效")
      : user.membership_status === "suspended" ? "会员已停用" : "尚未开通";
    openSheet(
      '<div class="m-sheet-head"><h2>管理会员 · ' + escapeHtml(user.username) + "</h2>" +
      '<button class="m-sheet-close" type="button" data-sheet-close aria-label="关闭">' + icon("close", 20) + "</button></div>" +
      '<div class="m-sheet-body" data-admin-user="' + number(user.id) + '">' +
      '<p class="m-sys-lead">当前状态：' + escapeHtml(expiry) + " · 今日已用 " + number(user.used_today) + " 次</p>" +
      formFieldHtml("会员状态", '<select data-member-status>' +
        '<option value="inactive"' + (user.membership_status === "inactive" ? " selected" : "") + ">未开通</option>" +
        '<option value="active"' + (user.membership_status === "active" ? " selected" : "") + ">有效</option>" +
        '<option value="suspended"' + (user.membership_status === "suspended" ? " selected" : "") + ">停用</option>" +
        "</select>", false) +
      formFieldHtml("开通 / 续期时长", '<select data-member-duration><option value="">选择时长</option>' +
        '<option value="1_month">1个月</option><option value="3_months">3个月</option>' +
        '<option value="12_months">12个月</option><option value="3_years">3年</option>' +
        '<option value="permanent">永久</option></select>', false) +
      '<p class="m-sys-hint">从当前时间开始顺延；已有会员则叠加续期。</p>' +
      '<div class="m-sys-sheet-actions">' +
      '<button class="m-btn-outline" type="button" data-sheet-close>取消</button>' +
      '<button class="m-btn-primary" type="button" data-system-save-member>应用</button>' +
      "</div></div>"
    );
  }

  function setFieldError(inputId, message) {
    const input = document.getElementById(inputId);
    const field = input && input.closest(".m-form-field");
    if (!field) return;
    field.classList.toggle("is-invalid", Boolean(message));
    const error = field.querySelector("[data-field-error]");
    if (error) {
      error.hidden = !message;
      error.textContent = message || "";
    }
  }

  function saveSystemBirth() {
    const birthDate = (document.getElementById("m-sys-birth-date") || {}).value;
    const birthTime = (document.getElementById("m-sys-birth-time") || {}).value;
    if (!birthDate || !birthTime) {
      showToast("请填写完整出生日期和时间");
      return;
    }
    const button = document.querySelector("[data-system-save-birth]");
    if (button) button.disabled = true;
    global.MobileAPI.request("/api/account/birth-profile", "POST", {
      birth_datetime: birthDate + "T" + birthTime,
      gender: (document.getElementById("m-sys-birth-gender") || {}).value || "unspecified",
      trade_date: todayString(),
    }).then(function () {
      showToast("个人命理资料已保存");
      loadSystem();
    }).catch(function (error) {
      showToast(error && error.message ? error.message : "个人命理资料保存失败");
    }).then(function () {
      if (button) button.disabled = false;
    });
  }

  function deleteSystemBirth() {
    openConfirmSheet("删除命理资料？", "删除后智能解读将无法使用你的出生信息，此操作不可恢复。", {
      danger: true,
      centered: true,
      confirmLabel: "删除",
      onConfirm: function () {
        global.MobileAPI.request("/api/account/birth-profile", "DELETE").then(function () {
          showToast("个人命理资料已删除");
          loadSystem();
        }).catch(function (error) {
          showToast(error && error.message ? error.message : "删除失败");
        });
      }
    });
  }

  function saveSystemPassword() {
    const current = (document.getElementById("m-sys-password-current") || {}).value || "";
    const next = (document.getElementById("m-sys-password-new") || {}).value || "";
    const confirm = (document.getElementById("m-sys-password-confirm") || {}).value || "";
    setFieldError("m-sys-password-current", "");
    setFieldError("m-sys-password-new", "");
    setFieldError("m-sys-password-confirm", "");
    let invalid = false;
    if (!current) {
      setFieldError("m-sys-password-current", "请输入当前密码");
      invalid = true;
    }
    if (next.length < 8 || next.length > 128) {
      setFieldError("m-sys-password-new", "新密码长度应为 8 至 128 位");
      invalid = true;
    }
    if (next !== confirm) {
      setFieldError("m-sys-password-confirm", "两次输入的密码不一致，请重新输入。");
      invalid = true;
    }
    if (invalid) return;
    const button = document.querySelector("[data-system-save-password]");
    if (button) button.disabled = true;
    global.MobileAPI.request("/api/account/password", "POST", {
      current_password: current,
      new_password: next,
      confirm_password: confirm,
    }).then(function () {
      ["m-sys-password-current", "m-sys-password-new", "m-sys-password-confirm"].forEach(function (id) {
        const input = document.getElementById(id);
        if (input) input.value = "";
      });
      showToast("密码已更新");
    }).catch(function (error) {
      showToast("更新失败：" + (error && error.message ? error.message : "密码更新失败"));
    }).then(function () {
      if (button) button.disabled = false;
    });
  }

  function switchSystemAccount() {
    global.location.assign("/login/");
  }

  function logoutSystemAccount() {
    openConfirmSheet("退出当前账号", "退出后需要重新登录。本机已记录的其他账号仍可直接切换。", {
      danger: true,
      centered: true,
      confirmLabel: "退出",
      onConfirm: function () {
        global.MobileSession.logout().then(function () {
          global.MobileRouter.replace("#/auth");
        }).catch(function (error) {
          showToast(error && error.message ? error.message : "退出失败");
        });
      }
    });
  }

  function saveSystemMarket() {
    const button = document.querySelector("[data-system-save-market]");
    if (button) button.disabled = true;
    global.MobileAPI.request("/api/admin/settings", "POST", {
      tushare_token: ((document.getElementById("m-sys-token") || {}).value || "").trim(),
      ifind_refresh_token: ((document.getElementById("m-sys-ifind") || {}).value || "").trim(),
    }).then(function () {
      showToast("行情密钥已保存");
      loadSystem();
    }).catch(function (error) {
      showToast(error && error.message ? error.message : "系统配置保存失败");
    }).then(function () {
      if (button) button.disabled = false;
    });
  }

  function toggleSystemRefresh() {
    const enabled = !Boolean((state.system.admin && state.system.admin.data && state.system.admin.data.background_refresh_enabled));
    global.MobileAPI.request("/api/admin/settings", "POST", {
      background_refresh_enabled: enabled,
    }).then(function () {
      showToast(enabled ? "后台刷新已启用" : "后台刷新已暂停");
      loadSystem();
    }).catch(function (error) {
      showToast(error && error.message ? error.message : "后台刷新设置失败");
    });
  }

  function saveSystemModels() {
    const button = document.querySelector("[data-system-save-models]");
    if (button) button.disabled = true;
    global.MobileAPI.request("/api/admin/settings", "POST", {
      primary_model_id: (document.getElementById("m-sys-primary-model") || {}).value || "",
      fallback_model_id: (document.getElementById("m-sys-fallback-model") || {}).value || "",
    }).then(function () {
      showToast("模型分工已保存");
      loadSystem();
    }).catch(function (error) {
      showToast(error && error.message ? error.message : "模型分工保存失败");
    }).then(function () {
      if (button) button.disabled = false;
    });
  }

  function persistSystemModels(models, message) {
    return global.MobileAPI.request("/api/admin/settings", "POST", { models: models }).then(function () {
      showToast(message || "模型池已保存");
      closeSheet();
      loadSystem();
    });
  }

  function saveEditedSystemModel() {
    const sheet = document.querySelector(".m-sheet-body[data-model-id]");
    if (!sheet) return;
    const id = sheet.dataset.modelId;
    const fields = readModelSheetFields(sheet);
    const models = collectSystemModelPool().map(function (item) {
      if (item.id !== id) return item;
      return Object.assign({}, item, fields);
    });
    persistSystemModels(models, "模型已保存").catch(function (error) {
      showToast(error && error.message ? error.message : "模型保存失败");
    });
  }

  function addSystemModel() {
    const models = collectSystemModelPool();
    const id = "model-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
    const created = { id: id, name: "模型 " + (models.length + 1), base_url: "https://api.openai.com/v1", model: "", api_key: "", configured: false };
    state.system.models = models.concat([created]);
    const list = document.getElementById("m-sys-model-list");
    if (list) list.innerHTML = renderModelPoolHtml(state.system.models);
    updateSystemModelRoleOptions(id, (document.getElementById("m-sys-fallback-model") || {}).value || "");
    openModelEditSheet(id);
  }

  function deleteSystemModel(row) {
    if (!row) return;
    const id = row.dataset.modelId;
    const primary = (document.getElementById("m-sys-primary-model") || {}).value;
    const fallback = (document.getElementById("m-sys-fallback-model") || {}).value;
    const llm = (state.system.admin && state.system.admin.llm) || {};
    if (id === primary || id === fallback || id === llm.primary_model_id || id === llm.fallback_model_id) {
      showToast("请先为主模型或辅助模型选择其他模型，再删除当前模型");
      return;
    }
    openConfirmSheet("删除模型？", "删除后该模型将从模型池移除，此操作不可恢复。", {
      danger: true,
      centered: true,
      confirmLabel: "删除",
      onConfirm: function () {
        const models = collectSystemModelPool().filter(function (item) { return item.id !== id; });
        persistSystemModels(models, "模型已删除").catch(function (error) {
          showToast(error && error.message ? error.message : "删除失败");
        });
      }
    });
  }

  function testSystemModel(row) {
    if (!row) return;
    const status = document.querySelector("[data-model-test-status]");
    const button = document.querySelector("[data-system-test-model]");
    const sheet = document.querySelector(".m-sheet-body[data-model-id]");
    const id = (sheet && sheet.dataset.modelId) || row.dataset.modelId;
    const fields = sheet ? readModelSheetFields(sheet) : {};
    const profile = Object.assign({}, collectSystemModelPool().find(function (item) { return item.id === id; }) || {}, fields, { id: id });
    if (button) button.disabled = true;
    if (status) status.textContent = "连接中";
    global.MobileAPI.request("/api/admin/settings/test", "POST", { model_id: id, profile: profile }).then(function (payload) {
      if (status) status.textContent = "上次测试：成功 · " + number(payload.result && payload.result.latency_ms) + "ms";
    }).catch(function (error) {
      if (status) status.textContent = error && error.message ? error.message : "测试失败";
    }).then(function () {
      if (button) button.disabled = false;
    });
  }

  function startSystemRefresh() {
    const button = document.querySelector("[data-system-refresh]");
    if (button) button.disabled = true;
    global.MobileAPI.request("/api/admin/refresh", "POST", { trade_date: todayString() }).then(function (payload) {
      showToast((payload && payload.message) || "后台刷新已提交");
    }).catch(function (error) {
      showToast(error && error.message ? error.message : "后台刷新启动失败");
    }).then(function () {
      if (button) button.disabled = false;
    });
  }

  function startSystemBackfill() {
    const startDate = (document.getElementById("m-sys-backfill-start") || {}).value;
    const endDate = (document.getElementById("m-sys-backfill-end") || {}).value;
    openConfirmSheet("开始历史回补？", "将按选定日期补齐缺失行情，回补期间页面仍可使用。", {
      confirmLabel: "开始回补",
      centered: true,
      onConfirm: function () {
        const button = document.querySelector("[data-system-backfill]");
        if (button) button.disabled = true;
        global.MobileAPI.request("/api/backfill", "POST", {
          start_date: startDate,
          end_date: endDate,
        }).then(function (payload) {
          const count = payload && payload.results ? payload.results.length : 0;
          showToast("历史回补完成，共处理 " + count + " 个工作日");
          loadSystem();
        }).catch(function (error) {
          showToast(error && error.message ? error.message : "回补失败");
        }).then(function () {
          if (button) button.disabled = false;
        });
      }
    });
  }

  function saveSystemMemberLimit() {
    const button = document.querySelector("[data-system-save-limit]");
    if (button) button.disabled = true;
    global.MobileAPI.request("/api/admin/settings", "POST", {
      member_daily_limit: number((document.getElementById("m-sys-member-limit") || {}).value),
    }).then(function () {
      showToast("会员调用额度已保存");
      loadSystem();
    }).catch(function (error) {
      showToast(error && error.message ? error.message : "会员调用额度保存失败");
    }).then(function () {
      if (button) button.disabled = false;
    });
  }

  function saveSystemMember(card) {
    if (!card) return;
    const status = (card.querySelector("[data-member-status]") || {}).value;
    const apply = function () {
      const button = card.querySelector("[data-system-save-member]");
      if (button) button.disabled = true;
      global.MobileAPI.request("/api/admin/membership", "POST", {
        user_id: card.dataset.adminUser,
        status: status,
        duration: (card.querySelector("[data-member-duration]") || {}).value,
      }).then(function (payload) {
        if (state.system.admin) state.system.admin.users = payload.users || [];
        closeSheet();
        showToast("会员状态已更新");
        renderSystemMembers();
      }).catch(function (error) {
        showToast(error && error.message ? error.message : "会员状态保存失败");
      }).then(function () {
        if (button) button.disabled = false;
      });
    };
    if (status === "suspended") {
      openConfirmSheet("停用该会员？", "停用后该账号将无法使用会员智能功能，可稍后重新开通。", {
        danger: true,
        centered: true,
        confirmLabel: "停用",
        onConfirm: apply
      });
      return;
    }
    apply();
  }

  /* ---------------------------------------------------------------- events */

  function bindEvents() {
    window.addEventListener("hashchange", closeSheet);

    // 全屏表单页的返回：优先回到列表/查看态，而非离开路由（capture 阶段先于路由层返回）
    document.getElementById("m-back").addEventListener("click", function (event) {
      if (state.review && state.review.form) {
        event.preventDefault();
        event.stopPropagation();
        closeForm();
      }
    }, true);

    document.addEventListener("click", function (event) {
      const dateBtn = event.target.closest("[data-action=date]");
      if (dateBtn) { openDateSheet(); return; }

      const retry = event.target.closest("[data-action=retry]");
      if (retry) { reloadCurrent(); return; }

      const sortHead = event.target.closest("[data-sort-key]");
      if (sortHead) { toggleSort(sortHead.dataset.sortKey); return; }

      const rangeTab = event.target.closest("[data-sentiment-range]");
      if (rangeTab) {
        state.sentimentRange = number(rangeTab.dataset.sentimentRange) || 20;
        document.querySelectorAll("[data-sentiment-range]").forEach(function (tab) {
          const active = number(tab.dataset.sentimentRange) === state.sentimentRange;
          tab.classList.toggle("active", active);
          tab.setAttribute("aria-pressed", String(active));
        });
        loadSentiment();
        return;
      }

      const auctionDataset = event.target.closest("[data-action=auction-dataset]");
      if (auctionDataset) {
        state.auctionDataset = auctionDataset.dataset.dataset || "focus";
        document.querySelectorAll("[data-action=auction-dataset]").forEach(function (tab) {
          const active = tab.dataset.dataset === state.auctionDataset;
          tab.classList.toggle("active", active);
          tab.setAttribute("aria-selected", String(active));
        });
        renderAuctionTable();
        return;
      }

      const dragonMode = event.target.closest("[data-action=dragon-mode]");
      if (dragonMode) {
        const mode = dragonMode.dataset.mode;
        state.dragonViewMode = mode === "profiles" ? "profiles" : "daily";
        if (mode === "profiles") loadDragonProfiles();
        else if (state.dragon) renderDragon();
        else loadDragon();
        return;
      }

      const ladderStock = event.target.closest("[data-ladder-stock]");
      if (ladderStock) { openDetailSheet(ladderStock.dataset.ladderStock); return; }

      const ladderExpand = event.target.closest("[data-ladder-expand]");
      if (ladderExpand) {
        const level = number(ladderExpand.dataset.ladderExpand);
        if (expandedLadder[level]) delete expandedLadder[level];
        else expandedLadder[level] = true;
        renderLadder();
        return;
      }

      const rotationSector = event.target.closest("[data-rotation-sector]");
      if (rotationSector) {
        openRotationMembersSheet(rotationSector.dataset.rotationSector, rotationSector.dataset.rotationDate);
        return;
      }

      const themeCode = event.target.closest("[data-theme-code]");
      if (themeCode) { openThemeDetailSheet(themeCode.dataset.themeCode); return; }

      const dragonTrader = event.target.closest("[data-dragon-trader]");
      if (dragonTrader) { openDragonTraderSheet(dragonTrader.dataset.dragonTrader); return; }

      const profileId = event.target.closest("[data-profile-id]");
      if (profileId) { openProfileSheet(profileId.dataset.profileId); return; }

      const watchBtn = event.target.closest("[data-action=watch]");
      if (watchBtn) { toggleWatch(); return; }

      const chartTab = event.target.closest("[data-chart-tab]");
      if (chartTab) { switchChartTab(chartTab.dataset.chartTab); return; }

      const sourceTab = event.target.closest("[data-action=source]");
      if (sourceTab) {
        state.popularitySource = sourceTab.dataset.source || "combined";
        renderTableBody();
        document.querySelectorAll(".m-source-tab").forEach(function (tab) {
          const active = tab.dataset.source === state.popularitySource;
          tab.classList.toggle("active", active);
          tab.setAttribute("aria-selected", String(active));
        });
        return;
      }

      const closeBtn = event.target.closest("[data-sheet-close]");
      if (closeBtn) { closeSheet(); return; }

      const backdrop = event.target.closest("[data-sheet-backdrop]");
      if (backdrop) { closeSheet(); return; }

      const quick = event.target.closest("[data-quick-date]");
      if (quick) { selectDate(quick.dataset.quickDate); return; }

      const calNav = event.target.closest("[data-cal-nav]");
      if (calNav) {
        if (calNav.disabled) return;
        const dir = calNav.dataset.calNav === "next" ? 1 : -1;
        const monthIndex = state.calCursor.year * 12 + state.calCursor.month + dir;
        state.calCursor = { year: Math.floor(monthIndex / 12), month: ((monthIndex % 12) + 12) % 12 };
        renderDateSheetBody();
        return;
      }

      const calCell = event.target.closest("[data-date]");
      if (calCell) {
        if (calCell.disabled) return;
        selectDate(calCell.dataset.date);
        return;
      }

      const systemAdminTab = event.target.closest("[data-system-admin-tab]");
      if (systemAdminTab) {
        state.system.adminTab = systemAdminTab.dataset.systemAdminTab === "models" ? "models" : "market";
        renderSystemAdmin("system/admin");
        return;
      }
      if (event.target.closest("[data-system-save-birth]")) { saveSystemBirth(); return; }
      if (event.target.closest("[data-system-delete-birth]")) { deleteSystemBirth(); return; }
      if (event.target.closest("[data-system-save-password]")) { saveSystemPassword(); return; }
      if (event.target.closest("[data-system-switch]")) { switchSystemAccount(); return; }
      if (event.target.closest("[data-system-logout]")) { logoutSystemAccount(); return; }
      if (event.target.closest("[data-system-save-market]")) { saveSystemMarket(); return; }
      if (event.target.closest("[data-system-toggle-refresh]")) { toggleSystemRefresh(); return; }
      if (event.target.closest("[data-system-save-models]")) { saveSystemModels(); return; }
      if (event.target.closest("[data-system-save-model]")) { saveEditedSystemModel(); return; }
      if (event.target.closest("[data-system-add-model]")) { addSystemModel(); return; }
      const editModel = event.target.closest("[data-system-edit-model]");
      if (editModel) { openModelEditSheet(editModel.dataset.modelId); return; }
      const deleteModel = event.target.closest("[data-system-delete-model]");
      if (deleteModel) { deleteSystemModel(deleteModel.closest("[data-model-id]")); return; }
      const testModel = event.target.closest("[data-system-test-model]");
      if (testModel) { testSystemModel(testModel.closest("[data-model-id]")); return; }
      if (event.target.closest("[data-system-refresh]")) { startSystemRefresh(); return; }
      if (event.target.closest("[data-system-backfill]")) { startSystemBackfill(); return; }
      if (event.target.closest("[data-system-save-limit]")) { saveSystemMemberLimit(); return; }
      const openMember = event.target.closest("[data-system-open-member]");
      if (openMember) { openMemberManageSheet(openMember.closest("[data-admin-user]").dataset.adminUser); return; }
      const saveMember = event.target.closest("[data-system-save-member]");
      if (saveMember) { saveSystemMember(saveMember.closest("[data-admin-user]")); return; }

      // 我的复盘：新增/编辑/删除/筛选/提交等操作
      const reviewAddTrade = event.target.closest("[data-review-add-trade]");
      if (reviewAddTrade) { openForm("trade"); return; }
      const reviewAddNote = event.target.closest("[data-review-add-note]");
      if (reviewAddNote) { openForm("note"); return; }
      const reviewAddAlert = event.target.closest("[data-review-add-alert]");
      if (reviewAddAlert) { openAlertAddDrawer(); return; }
      const reviewReadAll = event.target.closest("[data-review-read-all]");
      if (reviewReadAll) { markAllAlertsRead(); return; }
      const reviewEditTrade = event.target.closest("[data-review-edit-trade]");
      if (reviewEditTrade) {
        const id = reviewEditTrade.dataset.tradeId;
        const data = state.review.trades || { items: [] };
        const item = (data.items || []).find(function (it) { return String(it.id) === String(id); });
        closeSheet();
        if (item) openForm("trade", { trade: item });
        return;
      }
      const reviewDeleteTrade = event.target.closest("[data-review-delete-trade]");
      if (reviewDeleteTrade) { deleteTrade(reviewDeleteTrade.dataset.tradeId); return; }
      const reviewEditDaily = event.target.closest("[data-review-edit-daily]");
      if (reviewEditDaily) {
        const note = dailyNoteFor(state.requestedDate || todayString());
        openForm("daily", { note: note });
        return;
      }
      const reviewDeleteDaily = event.target.closest("[data-review-delete-daily]");
      if (reviewDeleteDaily) { deleteDailyNote(reviewDeleteDaily.dataset.noteId); return; }
      const dailyDateRow = event.target.closest("[data-daily-date]");
      if (dailyDateRow) {
        state.requestedDate = dailyDateRow.dataset.dailyDate;
        updateHeader("每日复盘", state.requestedDate);
        renderReviewDaily();
        return;
      }
      const reviewDeleteNote = event.target.closest("[data-review-delete-note]");
      if (reviewDeleteNote) { deleteNote(reviewDeleteNote.dataset.noteId); return; }
      const noteCard = event.target.closest("[data-note-id]");
      if (noteCard) { openNoteDetail(noteCard.dataset.noteId); return; }
      const alertRead = event.target.closest("[data-review-alert-read]");
      if (alertRead) { markAlertRead(alertRead.dataset.alertId); return; }
      const alertDelete = event.target.closest("[data-review-alert-delete]");
      if (alertDelete) { deleteAlert(alertDelete.dataset.alertId); return; }
      const alertRow = event.target.closest(".m-alert-row[data-alert-id]");
      if (alertRow) { openAlertDetail(alertRow.dataset.alertId); return; }
      const alertFilter = event.target.closest("[data-review-alert-filter]");
      if (alertFilter) {
        state.review.alertStatus = alertFilter.dataset.reviewAlertFilter === "unread" ? "unread" : "all";
        loadReviewAlerts();
        return;
      }
      const reviewSubmit = event.target.closest("[data-review-submit]");
      if (reviewSubmit) { submitForm(); return; }
      const alertSave = event.target.closest("[data-alert-save]");
      if (alertSave) { submitAlertAdd(); return; }
      const alertStock = event.target.closest("[data-alert-stock]");
      if (alertStock) { openDetailSheet(alertStock.dataset.code); return; }
      const remarkEdit = event.target.closest("[data-remark-edit]");
      if (remarkEdit) { openRemarkEditSheet(); return; }
      const remarkSave = event.target.closest("[data-remark-save]");
      if (remarkSave) { saveRemark(); return; }

      const row = event.target.closest("[data-code]");
      if (row) {
        const interactive = event.target.closest("button, a, input, select, textarea");
        if (!interactive) {
          if (state.key === "review/trades" && row.dataset.tradeId) {
            openTradeDetail(row.dataset.tradeId);
          } else if (state.key === "tools/tracking" && row.dataset.trackId) {
            openTrackingDetailSheet(row.dataset.code, row.dataset.trackId);
          } else if (state.key === "tools/screener") {
            openScreenerDetailSheet(row.dataset.code);
          } else {
            openDetailSheet(row.dataset.code);
          }
        }
      }

      // 智能选股：视图切换
      const screenerView = event.target.closest("[data-screener-view]");
      if (screenerView) {
        const view = screenerView.dataset.screenerView;
        if (view && view !== state.screener.view) {
          state.screener.view = view;
          state.sort = { key: "", dir: null };
          renderScreener();
        }
        return;
      }
      // 智能选股：策略胶囊行（开抽屉）
      const screenerOpen = event.target.closest("[data-screener-open-drawer]");
      if (screenerOpen) { openScreenerStrategyDrawer(); return; }
      // 智能选股：日期按钮
      const screenerDate = event.target.closest("[data-screener-date]");
      if (screenerDate) { openDateSheet(); return; }
      // 策略跟踪：刷新按钮
      const trackingRefresh = event.target.closest("[data-tracking-refresh]");
      if (trackingRefresh) {
        const icon = trackingRefresh.querySelector("svg");
        if (icon) icon.classList.add("is-spinning");
        refreshTracking();
        return;
      }

      // 聊天工作台（复盘助手 / 问师）：标题区操作位
      const chatClear = event.target.closest("[data-chat-clear]");
      if (chatClear) {
        if (state.chat.page === "assistant/chat") {
          openConfirmSheet("清空对话", "将删除当前账号的全部复盘助手对话历史，无法恢复。", {
            danger: true,
            confirmLabel: "清空",
            onConfirm: clearAssistantMessages
          });
        } else if (state.chat.page === "tools/mentor") {
          if (!state.chat.mentorId) { showToast("请先选择导师"); return; }
          openConfirmSheet("清空当日对话", "将清空该导师今天的对话记录，无法恢复。", {
            danger: true,
            confirmLabel: "清空",
            onConfirm: function () { clearMentorMessages(state.chat.mentorId, todayString()); }
          });
        }
        return;
      }
      const chatMentors = event.target.closest("[data-chat-mentors]");
      if (chatMentors) { openMentorDrawer(); return; }
      const chatSend = event.target.closest("[data-chat-send]");
      if (chatSend) {
        if (state.chat.streaming) stopChatStream();
        else submitChatMessage();
        return;
      }
      const chatPreset = event.target.closest("[data-chat-preset]");
      if (chatPreset) { sendChatMessage(chatPreset.dataset.chatPreset); return; }
      const chatFollowup = event.target.closest("[data-chat-followup]");
      if (chatFollowup) { sendChatMessage(chatFollowup.dataset.chatFollowup); return; }
      const chatRemoveMentor = event.target.closest("[data-chat-remove-mentor]");
      if (chatRemoveMentor) {
        state.chat.mentorId = "";
        state.chat.mentorName = "";
        state.chat.mentorTagline = "";
        state.chat.mentorGrade = "";
        state.chat.followUps = [];
        state.chat.messages = [];
        renderChatStream();
        renderChatMentorBar();
        return;
      }
    });
  }

  /* ---------------------------------------------------------------- init */

  bindEvents();

  global.MobilePages = {
    render: renderPage,
    has: function (key) { return Boolean(pageConfig(key) || isComplexPage(key)); },
    renderSystemHome: renderSystemHome,
  };
})(window);
