(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"/>',
    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"/>',
  };

  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: "",
  };

  let sheetToken = 0;

  function pageConfig(key) {
    const cfg = global.MobileNav && global.MobileNav.tableColumns ? global.MobileNav.tableColumns[key] : null;
    return cfg || 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 = 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 = { key: "", dir: null };
    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 renderTopArea(key) {
    const page = document.querySelector(".m-page");
    if (!page) return;
    let top = page.querySelector(".m-top");
    let html = buildStrip();
    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 = 240, padL = 8, padR = 30, padT = 12, padB = 22;
    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;

    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 + " 日）</div>" +
        '<div class="m-chart-card">' + sentimentTrendChart(rawRows) + "</div>" +
        '<div class="m-section-title">五维分项</div>' +
        sentimentComponentList(components);
    }

    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("");

    scroll.innerHTML = card +
      '<div class="m-section-title">历史明细 <span class="m-range-tabs">' + rangeTabs + "</span></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) });
      });
    }, {});
  }

  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();
    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 = "";
    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("");
      const remaining = !expanded && cap && stocks.length > cap ? stocks.length - cap : 0;
      if (remaining) {
        stocksHtml += '<button class="m-ladder-more" type="button" data-ladder-expand="' + level + '">展开剩余 ' + remaining + " 只</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 + "</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 spaceStocks = (groupMap[maxLevel] || {}).stocks || [];
    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>" +
      '<p>' + (spaceStocks.length ? spaceStocks.map(function (s) { return "<b>" + escapeHtml(s.name) + "</b>（" + escapeHtml(s.sector || "其他") + "）"; }).join(" · ") : "暂无空间板") + "</p>" +
      "<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>" +
          "<strong>" + escapeHtml(sector.name) + "</strong>" +
          "<small>" + number(sector.count) + " 家 · " + formatNumber(number(sector.strength), 0) + "</small>" +
          "</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) || [];
    }, { noLink: true });
  }

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

  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) || [];
    }, { noLink: true });
  }

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

  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; }, { noLink: true });
  }

  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>" : "";
  }

  /* ----- 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,
  };

  const COMPLEX_LOADERS = {
    "market/sentiment": loadSentiment,
    "market/ladder": loadLadder,
    "market/rotation": loadRotation,
    "market/auction": loadAuction,
    "market/themes": loadThemes,
    "market/dragon": loadDragon,
  };

  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.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");
    global.setTimeout(function () {
      if (sheetToken === token && !root.classList.contains("is-open")) root.innerHTML = "";
    }, 340);
  }

  function bindSheetDrag(root) {
    const sheet = root.querySelector(".m-sheet");
    const handle = root.querySelector(".m-sheet-handle");
    if (!sheet || !handle) return;
    let startY = 0;
    let dragging = false;
    handle.addEventListener("touchstart", function (event) {
      dragging = true;
      startY = event.touches[0].clientY;
      sheet.style.transition = "none";
    }, { passive: true });
    handle.addEventListener("touchmove", function (event) {
      if (!dragging) return;
      const dy = event.touches[0].clientY - startY;
      if (dy > 0) sheet.style.transform = "translateY(" + dy + "px)";
    }, { passive: true });
    handle.addEventListener("touchend", function (event) {
      if (!dragging) return;
      dragging = false;
      sheet.style.transition = "";
      const dy = event.changedTouches[0].clientY - startY;
      if (dy > sheet.offsetHeight / 3) {
        closeSheet();
      } else {
        sheet.style.transform = "";
      }
    });
  }

  function selectDate(dateStr) {
    if (!dateStr) return;
    state.requestedDate = dateStr;
    closeSheet();
    reloadCurrent();
  }

  function openDateSheet() {
    state.calCursor = {
      year: parseLocalDate(state.requestedDate).getFullYear(),
      month: parseLocalDate(state.requestedDate).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 ? "已加入自选" : "已移出自选");
    }).catch(function () {
      updateStarButton();
      showToast("操作失败，请重试");
    });
  }

  function showToast(message) {
    const root = document.getElementById("m-sheet-root");
    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),
      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 || "分时数据暂不可用");

    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>" +
      '<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>";
  }

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

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

    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 row = event.target.closest("[data-code]");
      if (row) {
        const interactive = event.target.closest("button, a, input, select, textarea");
        if (!interactive) openDetailSheet(row.dataset.code);
      }
    });
  }

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

  bindEvents();

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